Mundarija (26)
- Bu darsda
- 1. Nega bu kerak?
- 2. Content-Disposition
- 2.1 Ikki rejim va nom
- 2.2 Kanon funksiyasi
- 2.3 Haqiqiy brauzer nima qildi
- 3. Faylni berish: oqim va sarlavhalar
- 3.1 Server orqali
- 3.2 Server orqalimi, presigned'mi?
- 4. Range: qismlab olish
- 4.1 Nega kerak
- 4.2 Tahlil
- 5. ZIP: bir nechta fayl bir yuklab olishda
- 5.1 Oqim bilan arxiv
- 5.2 Kanon o'lchovi
- 5.3 Takror nomlar va zip slip
- 6. Hujumchi nigohi
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): javob sarlavhalari
- 3-mashq (qiyin): ZIP rejasi
- 9. Loyiha qadami: berish.ts moduli
- 10. Real ishda
- Xulosa
- Manbalar
Fayl yuklab berish va arxivlash: Content-Disposition, Range va ZIP oqimi
Qisqacha: Faylni foydalanuvchiga berish — uchta sarlavha masalasi:
Content-Type(nima),Content-Disposition(ochish yoki yuklab olish, qaysi nom bilan) vaX-Content-Type-Options: nosniff(brauzer turni taxmin qilmasin). O'zbekcha nom uchunfilename*=UTF-8''…kerak: kanon tajribasida Node xom UTF-8 nomli sarlavhaniERR_INVALID_CHARbilan umuman yubormadi. Katta fayl S3 dan oqim bilan uzatiladi,Rangeso'rovi qismni beradi (206). Bir nechta fayl — ZIP oqimi: 1 000 MB arxiv server xotirasini atigi ~100 MB oshirdi.
Bu darsda
Content-Dispositionsarlavhasini RFC 6266 bo'yicha UTF-8 nom va ASCII zaxira bilan yasaysiz.- Faylni S3 dan oqim bilan, huquq tekshiruvi bilan berasiz.
Rangeso'rovlarini tahlil qilib, 206 va 416 javoblarini qaytarasiz.- Bir nechta faylni diskka ham, xotiraga ham yig'masdan ZIP qilib berasiz va zip slip'dan himoyalanasiz.
- Server orqali berish va presigned havola orasida tanlaysiz.
Oldin bilishingiz kerak: Static fayl server, Presigned URL'lar, Fayl metama'lumotini DB'da boshqarish, pipeline, finished va stream xatolari.
Oldingi uch darsda server fayl yasadi: rasm ko'rinishlari, PDF, Excel. Endi ularni — va foydalanuvchi yuklagan fayllarni — qaytarib beramiz.
1. Nega bu kerak?
Malika vazifasidagi Oʻzbekcha hisobot (oktabr).pdf ni yuklab oldi. Kompyuterida fayl download yoki 1f3c…b2 nomi bilan, kengaytmasiz saqlandi. U uni ocholmadi. Sabab — bitta sarlavha.
Static fayl server darsida Content-Type ni kengaytmadan aniqlagan edik. Ilova fayllari uchun bu yetarli emas:
- fayl omborda tasodifiy kalit bilan (
fayllar/7/1f3c….pdf) — asl nom bazada; - nom o'zbekcha yoki kirill;
- ba'zi fayllar brauzerda ochilishi (rasm), ba'zilari yuklab olinishi (hujjat) kerak;
- foydalanuvchi yuklagan HTML yoki SVG brauzerda ochilsa — XSS (Fayl yuklash xavfsizligi).
2. Content-Disposition
2.1 Ikki rejim va nom
Content-Disposition: inline
Content-Disposition: attachment; filename="hisobot.pdf"inline— brauzer ko'rsata olsa, ko'rsatadi (rasm, PDF).attachment— har doim yuklab olinadi;filename— saqlanadigan nom.
HTTP sarlavhalari tarixan ASCII. O'zbekcha nom uchun RFC 6266 (RFC 8187 kodlashi bilan) filename* parametrini belgilaydi: filename*=UTF-8'' + foiz bilan kodlangan nom. Eski mijozlar uchun yoniga ASCII filename ham qo'yiladi; ikkalasi bo'lsa, zamonaviy brauzer filename* ni oladi.
2.2 Kanon funksiyasi
function contentDisposition(nom, tur = "attachment") {
let ascii = nom.normalize("NFKD").replace(/[^\x20-\x7E]/g, "")
.replace(/["\\]/g, "_").trim();
// "Ўқиш.txt" → ".txt" bo'lib qolmasin
if (ascii === "" || ascii.startsWith(".")) ascii = `fayl${ascii}`;
const utf8 = encodeURIComponent(nom).replace(/['()*]/g,
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase());
return `${tur}; filename="${ascii}"; filename*=UTF-8''${utf8}`;
}
for (const nom of ["hisobot.txt", "Oʻzbekcha hisobot (oktabr).txt",
"Ўқиш режаси.txt", 'a"b;c.txt']) {
console.log(contentDisposition(nom));
}Konsolda:
attachment; filename="hisobot.txt"; filename*=UTF-8''hisobot.txt
attachment; filename="Ozbekcha hisobot (oktabr).txt"; filename*=UTF-8''O%CA%BBzbekcha%20hisobot%20%28oktabr%29.txt
attachment; filename="fayl.txt"; filename*=UTF-8''%D0%8E%D2%9B%D0%B8%D1%88%20%D1%80%D0%B5%D0%B6%D0%B0%D1%81%D0%B8.txt
attachment; filename="a_b;c.txt"; filename*=UTF-8''a%22b%3Bc.txtQatorma-qator:
- ASCII zaxira:
normalize("NFKD")harflarni tarkibiy qismlarga ajratadi, keyin ASCII bo'lmaganlar olib tashlanadi:Oʻzbekcha→Ozbekcha."va\—_(qo'shtirnoq ichida ular sarlavhani buzadi). - To'liq kirill nom — ASCII'da faqat
.txtqoladi. Kanon buni tajribada topdi va tuzatdi: endifayl.txt. filename*—encodeURIComponent+',(,),*ham kodlanadi (RFC 8187 bu belgilarni ruxsat etmaydi,encodeURIComponentesa ularni qoldiradi).
2.3 Haqiqiy brauzer nima qildi
Kanon Chromium 151 (Playwright, headless shell) da har nomni yuklab olib, brauzer taklif qilgan nomni yozdi. Ikki variant: kanon funksiyasi bilan va faqat filename="<xom nom>" bilan. Har juftning ikkinchi qatoriga qarang: → dan keyin brauzer diskka qaysi nom bilan saqladi:
ascii | sarlavha: attachment; filename="hisobot.txt"; filename*=UTF-8''hisobot.txt
| filename* bilan → "hisobot.txt" | faqat filename="…" (xom UTF-8) → "hisobot.txt"
ozbek | sarlavha: attachment; filename="Ozbekcha hisobot (oktabr).txt"; filename*=UTF-8''O%CA%BBzbekcha%20hisobot%20%28oktabr%29.txt
| filename* bilan → "Oʻzbekcha hisobot (oktabr).txt" | faqat filename="…" (xom UTF-8) → "yuklanmadi (Node: ERR_INVALID_CHAR)"
kirill | sarlavha: attachment; filename=".txt"; filename*=UTF-8''%D0%8E%D2%9B%D0%B8%D1%88%20%D1%80%D0%B5%D0%B6%D0%B0%D1%81%D0%B8.txt
| filename* bilan → "Ўқиш режаси.txt" | faqat filename="…" (xom UTF-8) → "yuklanmadi (Node: ERR_INVALID_CHAR)"
qoshtirnoq | sarlavha: attachment; filename="a_b;c.txt"; filename*=UTF-8''a%22b%3Bc.txt
| filename* bilan → "a_b;c.txt" | faqat filename="…" (xom UTF-8) → "a_b"Nima bo'ldi? filename* bor joyda brauzer har doim to'g'ri nomni oldi. filename* siz o'zbekcha va kirill nom umuman yetib bormadi, qo'shtirnoqli nom esa kesildi.
E'tibor bering: kirill qatorida filename=".txt" — bu tajriba tuzatishdan oldin yozilgan (yuqoridagi jonli blok tuzatilgan funksiya natijasi). Brauzer baribir filename* ni olgani uchun to'g'ri nom saqlandi; .txt faqat eski mijozlarda chiqardi.
Uchta xulosa:
filename*bilan o'zbekcha va kirill nomlar aynan saqlandi.- Xom UTF-8 nomli sarlavhani Node umuman yubormadi —
res.setHeaderxato tashladi, javob ketmadi. a"b;c.txt— xom variantda brauzer;ni parametr ajratgichi deb, nomnia_bgacha kesdi. Kanon funksiyasi bilan esaa_b;c.txt(Chromium"ni o'zi_ga almashtirdi — Windows fayl nomida"taqiqlangan).
Node xatosini o'zimiz takrorlaymiz:
import http from "node:http";
try {
http.validateHeaderValue("Content-Disposition",
'attachment; filename="Oʻzbekcha.txt"');
} catch (e) {
console.log(e.code);
console.log(e.message);
}
http.validateHeaderValue("Content-Disposition",
'attachment; filename="Ozbekcha.txt"');
console.log("ASCII: OK");Konsolda:
ERR_INVALID_CHAR
Invalid character in header content ["Content-Disposition"]
ASCII: OK"Sarlavha mazmunida yaroqsiz belgi". res.setHeader ichida aynan shu tekshiruv ishlaydi. Express'da xato next(err) ga ketadi — foydalanuvchi 500 oladi, fayl emas.
Tekshirib ko'ring:
res.attachment("Oʻzbekcha.pdf")(Express) bilan ham xato bo'ladimi?
Javob
Yo'q — Express ichida content-disposition paketi ishlaydi, u ham filename* ni o'zi yasaydi. Xato faqat sarlavhani qo'lda satr bilan yozganda (res.set("Content-Disposition", \attachment; filename="${nom}"`)). Kanon funksiyasi kerak bo'lgan joy — S3 presigned havolasi (ResponseContentDisposition`) va Express'siz kod: u yerda Express yordami yo'q.
3. Faylni berish: oqim va sarlavhalar
3.1 Server orqali
Kanon modulining faylniBer funksiyasi (telefon uchun bo'lingan):
export async function faylniBer(
res: Response, ombor: FaylOmbori,
f: { kalit: string; aslNom: string; mime: string; hajm: number },
range: string | undefined, inline = false,
): Promise<void> {
const o = oraliqniTahlil(range, f.hajm);
res.set({
"Content-Type": f.mime,
"Content-Disposition": contentDisposition(f.aslNom,
inline && f.mime.startsWith("image/")
? "inline" : "attachment"),
"X-Content-Type-Options": "nosniff",
"Accept-Ranges": "bytes",
"Cache-Control": "private, max-age=0, must-revalidate",
});
if (o === "416") {
res.status(416)
.set("Content-Range", `bytes */${String(f.hajm)}`).end();
return;
}
const { oqim } = await ombor.oqi(f.kalit,
o ? `bytes=${String(o.bosh)}-${String(o.oxir)}` : undefined);
if (o) {
res.status(206).set({
"Content-Range":
`bytes ${String(o.bosh)}-${String(o.oxir)}/${String(f.hajm)}`,
"Content-Length": String(o.oxir - o.bosh + 1),
});
} else {
res.status(200).set("Content-Length", String(f.hajm));
}
// mijoz uzilsa — S3 oqimi ham yopiladi
await pipeline(oqim, res);
}Har sarlavha nimaga:
| Sarlavha | Nega |
|---|---|
Content-Type: f.mime |
bazadagi tur — sehrli baytlardan aniqlangan, mijoz aytgani emas |
inline faqat image/* |
PDF/matn ham brauzerda ochilishi mumkin, lekin xavfsizroq — yuklab olish |
nosniff |
brauzer text/plain ichidagi HTML'ni "taxmin qilib" bajarmasin |
Accept-Ranges: bytes |
mijozga "qismlab so'rasa bo'ladi" deydi |
Cache-Control: private |
shaxsiy fayl umumiy keshlarda (proxy, CDN) qolmasin |
pipeline(oqim, res) |
backpressure, va mijoz uzilsa S3 oqimi yopiladi |
f — chaqiruvchi bazadan egasini tekshirib olgan yozuv (Fayl metama'lumotini DB'da boshqarish). Funksiya o'zi huquqni bilmaydi.
3.2 Server orqalimi, presigned'mi?
server orqali (Express → S3 → mijoz): 62.1 ms mediana
presigned (mijoz → S3, havola yasash ham ichida): 59.4 ms medianaBu — Presigned URL'lar darsida ko'rgan o'lchovimiz: kanon 5 MB faylni 20 marta yukladi, lokal mashinada farq yo'q. Farq production'da chiqadi:
| Server orqali | Presigned havola | |
|---|---|---|
| Server trafigi | har bayt server orqali | yo'q |
| Huquq tekshiruvi | har so'rovda | havola berishda, keyin muddat tugaguncha |
| Havola sizsa | ishlamaydi (sessiya kerak) | muddat tugaguncha ishlaydi |
| Range, kesh | o'zingiz | ombor o'zi |
| Qachon | kichik fayllar, qat'iy nazorat | katta fayllar, video, ko'p yuklab olish |
4. Range: qismlab olish
4.1 Nega kerak
- Video pleyer — foydalanuvchi 5-daqiqaga o'tdi: brauzer
Range: bytes=31457280-so'raydi. - Uzilgan yuklab olish — brauzer qolgan qismini so'raydi.
- PDF ko'ruvchi — kerakli sahifani.
So'rov: Range: bytes=10-19. Javob: 206 Partial Content + Content-Range: bytes 10-19/100. Oraliq fayldan tashqarida — 416 Range Not Satisfiable + Content-Range: bytes */100.
4.2 Tahlil
function oraliqniTahlil(sarlavha, hajm) {
if (!sarlavha) return undefined;
const m = /^bytes=(\d*)-(\d*)$/.exec(sarlavha.trim());
if (!m) return undefined;
const [, a = "", b = ""] = m;
if (a === "" && b === "") return undefined;
if (a === "") {
const n = Number(b);
if (n === 0) return "416";
return { bosh: Math.max(0, hajm - n), oxir: hajm - 1 };
}
const bosh = Number(a);
const oxir = b === "" ? hajm - 1 : Math.min(Number(b), hajm - 1);
if (bosh >= hajm || oxir < bosh) return "416";
return { bosh, oxir };
}
for (const s of ["bytes=0-9", "bytes=90-", "bytes=-10", "bytes=-500",
"bytes=50-1000", "bytes=100-", "bytes=5-1", "bytes=0-1,5-6",
"items=0-1"]) {
console.log(s.padEnd(14), JSON.stringify(oraliqniTahlil(s, 100)));
}Konsolda:
bytes=0-9 {"bosh":0,"oxir":9}
bytes=90- {"bosh":90,"oxir":99}
bytes=-10 {"bosh":90,"oxir":99}
bytes=-500 {"bosh":0,"oxir":99}
bytes=50-1000 {"bosh":50,"oxir":99}
bytes=100- "416"
bytes=5-1 "416"
bytes=0-1,5-6 undefined
items=0-1 undefinedRFC 9110 qoidalari:
bytes=90-— 90 dan oxirigacha;bytes=-10— oxirgi 10 bayt (90–99).- Oxiri fayldan katta (
-1000,-500) — fayl chegarasiga qisqartiriladi, xato emas. - Boshi fayldan tashqarida (
100-) yoki teskari (5-1) — 416. - Ko'p oraliq (
0-1,5-6) va noma'lum birlik (items) —undefined: sarlavha e'tiborsiz, to'liq fayl 200 bilan. RFC bunga ruxsat beradi; ko'p oraliqli javob (multipart/byteranges) murakkab va kam ishlatiladi.
Kanon testlari bu jadvalni to'liq tekshiradi; S3 bilan esa bytes=10-19 → 206 va "0123456789", bytes=-5 → "56789", bytes=500- → 416 bytes */100.
sequenceDiagram
participant P as Video pleyer
participant A as vazifalar-api
participant S as Ombor
P->>A: GET /fayllar/7 (Range bytes=0-)
A->>A: egasi tekshiruvi, oraliqniTahlil
A->>S: GetObject Range bytes=0-…
A-->>P: 206, Content-Range bytes 0-…/hajm
P->>A: GET /fayllar/7 (Range bytes=31457280-)
A->>S: GetObject Range bytes=31457280-…
A-->>P: 206, qolgan qism5. ZIP: bir nechta fayl bir yuklab olishda
5.1 Oqim bilan arxiv
"Vazifaning barcha ilovalarini yuklab olish" — 10 ta fayl, bitta ZIP. Yomon yo'l: hammasini diskka yuklab, arxivlab, keyin berish. Kanon yo'li — archiver 8 bilan oqim: har fayl S3 dan o'qilib, arxivga, arxiv esa to'g'ridan javobga:
import { ZipArchive } from "archiver";
export async function zipBer(
res: Response, ombor: FaylOmbori, zipNomi: string,
fayllar: { kalit: string; aslNom: string; mime: string }[],
): Promise<void> {
res.status(200).set({
"Content-Type": "application/zip",
"Content-Disposition": contentDisposition(zipNomi),
"X-Content-Type-Options": "nosniff",
});
const zip = new ZipArchive({ zlib: { level: 6 } });
const ish = pipeline(zip, res);
const nomlar = new Set<string>();
for (const f of fayllar) {
const { oqim } = await ombor.oqi(f.kalit);
zip.append(oqim, {
name: noyobNom(f.aslNom, nomlar),
// rasm/PDF allaqachon siqilgan — qayta siqish befoyda
store: !f.mime.startsWith("text/"),
});
}
await zip.finalize();
await ish;
}- archiver 8 — faqat ESM; eski
archiver("zip")fabrikasi yo'q,new ZipArchive()(kanon topilmasi; Fayl yuklash xavfsizligi darsida ham ko'rdik). store— siqmasdan saqlash. JPEG, PNG, PDF allaqachon siqilgan; qayta siqish CPU yeydi va hajmni deyarli kamaytirmaydi. Matn — siqiladi.- ZIP hajmi oldindan noma'lum —
Content-Lengthyo'q, javob chunked bo'ladi (HTTP server tomonda). Brauzer progress foizini ko'rsata olmaydi — bu kelishuv.
5.2 Kanon o'lchovi
ZIP 1000.0 MB (store — siqilmagan), 12424 ms, RSS 129.7 MB → 230.5 MB (+100.7 MB; mijoz ham shu jarayonda)20 ta 5 MB obyektdan 200 yozuvli, 1 000 MB arxiv — server xotirasi +100.7 MB (bu raqamga o'lchovchi mijoz ham kiradi — u ham shu jarayonda edi). 1 GB arxiv xotiraga hech qachon to'liq yig'ilmadi.
5.3 Takror nomlar va zip slip
ZIP ichida bitta papkada ikki chek.png — arxivni ochganda biri ikkinchisini yo'q qiladi. Yana bir xavf — zip slip: arxivdagi nom ../../etc/x bo'lsa, ba'zi arxivatorlar faylni papkadan tashqariga yozadi. Bizning nomlar — foydalanuvchi bergan asl_nom, shuning uchun tozalaymiz:
function noyobNom(nom, band) {
const toza =
nom.replace(/[/\\]/g, "_").replace(/^\.+/, "") || "fayl";
let n = toza;
for (let i = 2; band.has(n.toLowerCase()); i++) {
const nuqta = toza.lastIndexOf(".");
n = nuqta > 0
? `${toza.slice(0, nuqta)} (${i})${toza.slice(nuqta)}`
: `${toza} (${i})`;
}
band.add(n.toLowerCase());
return n;
}
const band = new Set();
const nomlar = ["a.png", "A.PNG", "a.png", "../../etc/x",
"README", ".env"];
console.log(nomlar.map((n) => noyobNom(n, band)));Konsolda:
[ 'a.png', 'A (2).PNG', 'a (3).png', '_.._etc_x', 'README', 'env' ]- Solishtirish
toLowerCasebilan: Windows va macOS fayl tizimidaa.pngvaA.PNG— bitta fayl. /va\—_: yo'l yo'q, faqat nom.- Boshidagi nuqtalar olib tashlanadi:
.env→env— yashirin fayl bo'lib qolmasin.
Kanon testi ZIP'ni yauzl bilan ochib tekshiradi: ikki fayl, ikkinchisi a (2).txt, matn usul: 8 (deflate), rasm usul: 0 (store).
6. Hujumchi nigohi
- Sarlavha injection.
asl_nom=x.pdf"; filename="evil.exe. Qo'lda yozilgan sarlavhada nom almashadi. Himoya:contentDisposition—"va\almashtiriladi,filename*foiz bilan kodlanadi. - Ishonchsiz faylni
inline. HTML yoki SVG ilovainlineberilsa — sizning domeningizda skript. Himoya:attachment,nosniff,inlinefaqat tekshirilgan rasm turlariga; yaxshisi — fayllar alohida domendan. - IDOR (Insecure Direct Object Reference — boshqa odamning obyektiga ID'ni o'zgartirib kirish).
GET /fayllar/123sizniki bo'lsa, hujumchi124ni ham sinaydi — ID ketma-ket. Himoya: har so'rovda egasi yoki vazifa orqali huquq (RBAC va resurs egaligi — 28-qism). - Zip slip. Himoya:
noyobNom. Import qilinadigan ZIP'lar uchun teskari tomon — ochishda ham har yo'lni tekshirish. - Range DoS. Minglab mayda oraliq (
bytes=0-0,2-2,4-4,…). Himoya: kanon ko'p oraliqni umuman qo'llamaydi — to'liq 200.
7. Ko'p uchraydigan xatolar
| Xato | Sabab | Davosi |
|---|---|---|
ERR_INVALID_CHAR |
xom UTF-8 nom sarlavhada | filename*=UTF-8''… |
Fayl download nomi bilan saqlandi |
Content-Disposition yo'q |
attachment; filename… |
Kirill nom .txt bo'ldi (eski mijoz) |
ASCII zaxira bo'sh | fayl + kengaytma |
Nom ; da kesildi |
qo'shtirnoqsiz yoki xom nom | kanon funksiyasi |
| Video oldinga o'tkazilmaydi | Range yo'q | Accept-Ranges, 206 |
| Server xotirasi to'ldi (ZIP) | arxiv xotirada yig'ildi | archiver oqimi + pipeline |
archiver is not a function |
archiver 8 ESM | new ZipArchive() |
| ZIP ichida fayl yo'qoldi | takror nom | noyobNom |
8. Mashqlar
Mashqlar kurs/mashqlar/29/11-yuklab-berish/ da.
1-mashq (oson): bo'shliqlarni to'ldiring
- UTF-8 nom uchun parametr —
- Brauzer turni taxmin qilmasligi uchun sarlavha qiymati —
- Qisman javob statusi —
- Qoniqtirib bo'lmaydigan oraliq statusi —
bytes=-10100 baytli faylda qaysi baytlarni beradi —
Yechim
- filename* —
UTF-8''+ foiz kodlash (RFC 8187). - nosniff —
X-Content-Type-Options. - 206 — Partial Content.
- 416 —
Content-Range: bytes */hajmbilan. - 90-99 — oxirgi 10 bayt.
2-mashq (o'rta): javob sarlavhalari
javob(o, hajm) funksiyasini yozing: oraliqniTahlil natijasidan ({ bosh, oxir }, "416" yoki undefined) status va sarlavhalar obyektini qaytarsin: 206 — Content-Range va Content-Length, 416 — Content-Range: bytes */hajm, 200 — Content-Length. 100 baytli fayl uchun to'rt holatni sinang.
Yechim
function javob(o, hajm) {
if (o === "416") {
return { status: 416, "Content-Range": `bytes */${hajm}` };
}
if (o === undefined) {
return { status: 200, "Content-Length": String(hajm) };
}
return {
status: 206,
"Content-Range": `bytes ${o.bosh}-${o.oxir}/${hajm}`,
"Content-Length": String(o.oxir - o.bosh + 1),
};
}
console.log(javob({ bosh: 10, oxir: 19 }, 100));
console.log(javob({ bosh: 95, oxir: 99 }, 100));
console.log(javob("416", 100));
console.log(javob(undefined, 100));Konsolda:
{
status: 206,
'Content-Range': 'bytes 10-19/100',
'Content-Length': '10'
}
{
status: 206,
'Content-Range': 'bytes 95-99/100',
'Content-Length': '5'
}
{ status: 416, 'Content-Range': 'bytes */100' }
{ status: 200, 'Content-Length': '100' }oxir - bosh + 1 — oraliq ikki uchi bilan (10–19 — 10 bayt). Eng ko'p uchraydigan xato — + 1 ni unutish: brauzer bitta bayt kam kutadi va ulanishni uzadi. Bu natijalar kanon testidagi bytes 10-19/100, "10" va bytes */100 bilan bir xil.
3-mashq (qiyin): ZIP rejasi
zipRejasi(fayllar) funksiyasini yozing: { aslNom, mime, hajm } massividan { yozuvlar, jamiMB } qaytarsin. Har yozuv — { nom, usul }: nom noyobNom bilan (darsdagi funksiya), usul — text/* uchun deflate, qolganlariga store. jamiMB — jami hajm MB da, bir xona aniqlikda (son). To'rt fayl bilan sinang, ulardan biri ../shartnoma.pdf.
Yechim
function noyobNom(nom, band) {
const toza =
nom.replace(/[/\\]/g, "_").replace(/^\.+/, "") || "fayl";
let n = toza;
for (let i = 2; band.has(n.toLowerCase()); i++) {
const nuqta = toza.lastIndexOf(".");
n = nuqta > 0
? `${toza.slice(0, nuqta)} (${i})${toza.slice(nuqta)}`
: `${toza} (${i})`;
}
band.add(n.toLowerCase());
return n;
}
function zipRejasi(fayllar) {
const band = new Set();
const yozuvlar = fayllar.map((f) => ({
nom: noyobNom(f.aslNom, band),
usul: f.mime.startsWith("text/") ? "deflate" : "store",
}));
const jami = fayllar.reduce((a, f) => a + f.hajm, 0);
return { yozuvlar, jamiMB: +(jami / 1024 / 1024).toFixed(1) };
}
console.log(zipRejasi([
{ aslNom: "chek.png", mime: "image/png", hajm: 2_000_000 },
{ aslNom: "Chek.png", mime: "image/png", hajm: 1_500_000 },
{ aslNom: "izoh.txt", mime: "text/plain", hajm: 12_000 },
{ aslNom: "../shartnoma.pdf", mime: "application/pdf",
hajm: 800_000 },
]));Konsolda:
{
yozuvlar: [
{ nom: 'chek.png', usul: 'store' },
{ nom: 'Chek (2).png', usul: 'store' },
{ nom: 'izoh.txt', usul: 'deflate' },
{ nom: '_shartnoma.pdf', usul: 'store' }
],
jamiMB: 4.1
}../shartnoma.pdf → / _ ga → .._shartnoma.pdf → boshidagi nuqtalar olindi → _shartnoma.pdf. jamiMB ni oldindan hisoblash foydali: masalan 500 MB dan katta arxivni rad etish yoki fon vazifaga berish (natija — omborda va muddatli havola bilan). +(…) — toFixed satrini songa qaytaradi.
9. Loyiha qadami: berish.ts moduli
Kanon modulida to'rtta funksiya: oraliqniTahlil, faylniBer, zipBer, noyobNom — hammasi darsda to'liq ko'rsatildi (contentDisposition — s3.ts da). Kanon tuzatgan topilma: to'liq kirill nomda ASCII zaxira .txt bo'lib qolardi — endi fayl.txt.
Testlar test/fayl-berish.test.ts da — 5 ta. Ikkitasi (Range tahlili, noyobNom) bazasiz, uchtasi (200 sarlavhalari, 206/416, ZIP tarkibi) MinIO bilan. Kanonda hammasi o'tdi (82/82). Bizda Docker yoqilmagan (biz ishga tushirdik, Node 24.21.0):
✔ oraliqniTahlil: RFC 9110 bitta oraliq (5.1968ms)
✔ noyobNom: takror nomlar va zip slip (0.3706ms)
﹣ S3 dan berish (MinIO) (0.1585ms) # S3_ENDPOINT yo'q
ℹ tests 2
ℹ suites 1
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 569.3039﹣ — MinIO to'plami o'tkazildi. Range va nom mantiqi yuqoridagi jonli bloklarda ham tekshirildi.
Commit xabari (modul uchun):
git commit -m "feat(fayl): faylniBer (Range, UTF-8 nom) va zipBer"vazifalar-api ga ulash 28-qism tugagach: ilovani berish, ZIP va katta fayl uchun presigned havolaga yo'naltirish.
10. Real ishda
- Google Drive, Dropbox — "papkani yuklab olish" oqimli ZIP bilan; katta papkada fon vazifa va xatga havola.
- Video xizmatlari — Range yoki HLS (videoni bir necha soniyalik mayda fayllarga bo'lib berish) bo'laklari (Video va audio: ffmpeg).
- Banklar — ko'chirma PDF'lari
attachmentvaCache-Control: no-storebilan (umumiy kompyuterda keshda qolmasin). - Intervyuda: "O'zbekcha nomli faylni qanday berasiz?", "206 status qachon?", "1 GB ZIP'ni server xotirasini to'ldirmasdan qanday berasiz?"
Xulosa
Content-Disposition: attachment; filename="ascii"; filename*=UTF-8''…— o'zbekcha va kirill nomlar brauzerda aynan saqlandi; xom UTF-8 nomni NodeERR_INVALID_CHARbilan yubormaydi.- Ilova berishda: bazadagi tur,
nosniff,attachment(inline — faqat rasm),privatekesh. - S3 dan
pipelinebilan oqim; mijoz uzilsa S3 oqimi ham yopiladi. - Range:
bytes=a-b,a-,-n→ 206; tashqarida → 416bytes */hajm; ko'p oraliq — e'tiborsiz, 200. - ZIP — archiver 8
ZipArchiveoqimi, rasm/PDFstore; 1 000 MB arxiv +100.7 MB xotira. noyobNom— takror nom va zip slip'dan himoya.
Keyingi dars: QR kod va shtrix-kod generatsiyasi — chiptalar va to'lovlar uchun QR, xato tuzatish darajasi va imzolangan payload.
Manbalar
- RFC 6266 — Content-Disposition HTTP'da: https://www.rfc-editor.org/rfc/rfc6266
- RFC 8187 — sarlavha parametrlarida belgilar to'plami (
filename*): https://www.rfc-editor.org/rfc/rfc8187 - RFC 9110 — Range so'rovlari, 206, 416: https://www.rfc-editor.org/rfc/rfc9110#name-range-requests
- Node.js
http.validateHeaderValue: https://nodejs.org/api/http.html#httpvalidateheadervaluename-value - archiver (8, ESM): https://github.com/archiverjs/node-archiver
- Snyk — Zip Slip: https://security.snyk.io/research/zip-slip-vulnerability
- Kanon tajribasi (IlmHamroh, 2026-10-27): archiver 8.0.0, playwright-core 1.62.0 (Chromium 151), @aws-sdk 3.1153.0, MinIO RELEASE.2025-09-07, Node 24.21.0. Jonli bloklar,
ERR_INVALID_CHAR, testlar va mashq yechimlari — biz ishga tushirdik (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!