IlmHamroh
JavaScript Full-stack/29-qism. Backend integratsiyalar: fayl, to'lov, bot, realtime11/69-dars17 daqiqa
Mundarija (26)

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) va X-Content-Type-Options: nosniff (brauzer turni taxmin qilmasin). O'zbekcha nom uchun filename*=UTF-8''… kerak: kanon tajribasida Node xom UTF-8 nomli sarlavhani ERR_INVALID_CHAR bilan umuman yubormadi. Katta fayl S3 dan oqim bilan uzatiladi, Range so'rovi qismni beradi (206). Bir nechta fayl — ZIP oqimi: 1 000 MB arxiv server xotirasini atigi ~100 MB oshirdi.

Bu darsda

  • Content-Disposition sarlavhasini RFC 6266 bo'yicha UTF-8 nom va ASCII zaxira bilan yasaysiz.
  • Faylni S3 dan oqim bilan, huquq tekshiruvi bilan berasiz.
  • Range so'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

http
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

js
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:

text
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.txt

Qatorma-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 .txt qoladi. Kanon buni tajribada topdi va tuzatdi: endi fayl.txt.
  • filename* — encodeURIComponent + ', (, ), * ham kodlanadi (RFC 8187 bu belgilarni ruxsat etmaydi, encodeURIComponent esa 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:

text
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:

  1. filename* bilan o'zbekcha va kirill nomlar aynan saqlandi.
  2. Xom UTF-8 nomli sarlavhani Node umuman yubormadi — res.setHeader xato tashladi, javob ketmadi.
  3. a"b;c.txt — xom variantda brauzer ; ni parametr ajratgichi deb, nomni a_b gacha kesdi. Kanon funksiyasi bilan esa a_b;c.txt (Chromium " ni o'zi _ ga almashtirdi — Windows fayl nomida " taqiqlangan).

Node xatosini o'zimiz takrorlaymiz:

js
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:

text
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):

ts
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?

text
server orqali (Express → S3 → mijoz): 62.1 ms mediana
presigned (mijoz → S3, havola yasash ham ichida): 59.4 ms mediana

Bu — 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

js
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:

text
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      undefined

RFC 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 qism

5. 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:

ts
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-Length yo'q, javob chunked bo'ladi (HTTP server tomonda). Brauzer progress foizini ko'rsata olmaydi — bu kelishuv.

5.2 Kanon o'lchovi

text
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:

js
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:

text
[ 'a.png', 'A (2).PNG', 'a (3).png', '_.._etc_x', 'README', 'env' ]
  • Solishtirish toLowerCase bilan: Windows va macOS fayl tizimida a.png va A.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

  1. Sarlavha injection. asl_nom = x.pdf"; filename="evil.exe. Qo'lda yozilgan sarlavhada nom almashadi. Himoya: contentDisposition — " va \ almashtiriladi, filename* foiz bilan kodlanadi.
  2. Ishonchsiz faylni inline. HTML yoki SVG ilova inline berilsa — sizning domeningizda skript. Himoya: attachment, nosniff, inline faqat tekshirilgan rasm turlariga; yaxshisi — fayllar alohida domendan.
  3. IDOR (Insecure Direct Object Reference — boshqa odamning obyektiga ID'ni o'zgartirib kirish). GET /fayllar/123 sizniki bo'lsa, hujumchi 124 ni ham sinaydi — ID ketma-ket. Himoya: har so'rovda egasi yoki vazifa orqali huquq (RBAC va resurs egaligi — 28-qism).
  4. Zip slip. Himoya: noyobNom. Import qilinadigan ZIP'lar uchun teskari tomon — ochishda ham har yo'lni tekshirish.
  5. 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=-10 100 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 */hajm bilan.
  • 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
js
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:

text
{
  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
js
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:

text
{
  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):

text
✔ 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):

bash
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 attachment va Cache-Control: no-store bilan (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 Node ERR_INVALID_CHAR bilan yubormaydi.
  • Ilova berishda: bazadagi tur, nosniff, attachment (inline — faqat rasm), private kesh.
  • S3 dan pipeline bilan oqim; mijoz uzilsa S3 oqimi ham yopiladi.
  • Range: bytes=a-b, a-, -n → 206; tashqarida → 416 bytes */hajm; ko'p oraliq — e'tiborsiz, 200.
  • ZIP — archiver 8 ZipArchive oqimi, rasm/PDF store; 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

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!