IlmHamroh
JavaScript Full-stack/30-qism. NestJS62/65-dars19 daqiqa
Mundarija (32)

Fayl yuklash NestJS'da: FileInterceptor, ParseFilePipe va oqimli yuklash

Qisqacha: Nest'da fayl yuklashning standart yo'li — Multer: @UseInterceptors(FileInterceptor("fayl")) va @UploadedFile(new ParseFilePipe({ validators: [...] })). MaxFileSizeValidator hajmni, FileTypeValidator esa faylning birinchi baytlarini (magic bytes) tekshiradi. Faylni qaytarish — StreamableFile. Multer sukut bo'yicha faylni to'liq xotiraga yig'adi: biz o'lchaganda 100 MB fayl server xotirasini ~200 MB ga oshirdi, oqim bilan o'qish esa ~50 MB. Shuning uchun vazifalar-api 29-qismdagi oqimli yuklashni (busboy → S3) saqladi va Nest'da uni @Req() orqali ishlatadi.

Bu darsda

  • FileInterceptor, FilesInterceptor va @UploadedFile bilan fayl qabul qilasiz.
  • ParseFilePipe validatorlarining haqiqiy xabarlarini va FileTypeValidator tuzoqlarini bilasiz.
  • StreamableFile bilan faylni yuklab berasiz.
  • Multer va oqimli yuklashning xotira narxini o'lchov bilan solishtirasiz.
  • vazifalar-api ning fayl controller'ini va kanon ko'chishda topgan ikki xatoni ko'rasiz.

Oldin bilishingiz kerak: Fayl yuklash: multipart va Multer, Fayl yuklash xavfsizligi, Katta fayllarni stream bilan yuklash, Pipe'lar, Interceptor'lar va RxJS.

Auth endi loyihada: 28-qismdagi sessiya, rollar va RLS Nest'ga ko'chdi, har so'rov egasi bilan keladi. Endi 29-qismdagi integratsiyalar navbati — birinchisi fayllar.

1. Nega bu kerak?

29-qismda vazifalar-api ga fayl ilovalari qo'shilgandi: vazifaga chizma, chek yoki PDF biriktirish (Fayl yuklash: multipart va Multer). U yerda ikki yo'lni ko'rgandik: Multer (oddiy, faylni yig'adi) va oqim (busboy → S3, xotiraga yig'maydi). Kanon oqimni tanlagandi.

Nest'da fayl yuklashning o'z "rasmiy" yo'li bor — u Multer ustiga qurilgan. Nest loyihasiga ishga kirsangiz, 90 % holatda FileInterceptor ni ko'rasiz. Shuning uchun uni to'liq o'rganamiz. Keyin o'lchaymiz va vazifalar-api nega boshqa yo'ldan borganini ko'ramiz.

O'xshatish: pochta bo'limiga katta posilka keldi. Birinchi usul — posilkani to'liq omborga olib kirib, keyin tekshirish va jo'natish. Ikkinchi usul — konveyer: quti kirishi bilan tekshiriladi va to'g'ri yuk mashinasiga o'tadi. Kichik posilkalarda farq yo'q. Yuz kilogrammlik yuklar kunda yuzlab kelsa — ombor to'ladi.

2. FileInterceptor — Multer Nest'da

2.1 Birinchi yuklash

Multer @nestjs/platform-express ichida keladi — alohida o'rnatish shart emas (biz o'rnatganda multer 2.4.0 keldi). Tip uchun @types/multer kerak. Demo controller (biz ishga tushirdik: NestJS 12.2.0, Node 24.21.0):

ts
const MB = 1024 * 1024;

@Controller()
class FaylController {
  @Post("rasm")
  @UseInterceptors(FileInterceptor("fayl", {
    limits: { fileSize: 5 * MB, files: 1 },
  }))
  rasm(
    @UploadedFile(new ParseFilePipe({
      validators: [
        new MaxFileSizeValidator({ maxSize: 1 * MB }),
        new FileTypeValidator({ fileType: "image/png" }),
      ],
    }))
    fayl: Express.Multer.File,
  ) {
    return { nom: fayl.originalname, hajm: fayl.size,
      mime: fayl.mimetype };
  }
}

Uch qism:

  • FileInterceptor("fayl", sozlama) — interceptor (Interceptor'lar va RxJS): handler'dan oldin Multer'ni chaqiradi va multipart tanadan fayl nomli maydonni o'qiydi. Ikkinchi argument — Multer sozlamalari: limits, storage, fileFilter.
  • @UploadedFile() — parametr dekoratori, req.file ni beradi. Ichiga pipe qo'yish mumkin.
  • Express.Multer.File — fayl obyekti turi: originalname, mimetype, size, buffer (xotirada) yoki path (diskda).

limits ni unutmang. Busiz Multer istalgan hajmdagi faylni qabul qiladi — 29-qismdagi DoS xavfi (Fayl yuklash xavfsizligi).

2.2 Fayl qachon o'qiladi

FileInterceptor — interceptor (ushlab qoluvchi). Demak u so'rov hayot sikli dagi o'z joyida ishlaydi. Fayl baytlari aynan shu joyda o'qiladi:

Qadam Kim Fayl bilan nima bo'ladi
1 middleware (express.json) multipart/form-data ni tanimaydi — tegmaydi
2 guard (KirishGuard, CheklovGuard) tana hali o'qilmagan; rad etsa — Multer umuman ishga tushmaydi
3 interceptor (FileInterceptor) Multer tanani o'qiydi, limits shu yerda — 413
4 pipe (ParseFilePipe) tayyor fayl obyektini tekshiradi — 400
5 handler fayl.buffer bilan ishlaydi

Bu tartib xavfsizlik uchun foydali. Anonim so'rov yoki limitdan oshgan foydalanuvchi 2-qadamda to'xtaydi — server uning 100 MB faylini xotiraga yig'ib o'tirmaydi. Agar faylni middleware'da o'qisangiz (masalan, app.use(multer().any())), hamma yuklash guard'dan oldin xotiraga tushardi: anonim ham serverni to'ldira olardi.

Tekshirib ko'ring: Validatorlar pipe'da ishlaydi. Unda nega Multer limits baribir kerak?

Javob

Pipe 4-qadamda, fayl to'liq o'qilgandan keyin ishlaydi. 10 GB fayl yuborilsa, MaxFileSizeValidator uni ko'rguncha xotira allaqachon to'lgan bo'ladi. limits.fileSize esa 3-qadamda, o'qish paytida to'xtatadi. Biri xotirani himoya qiladi, ikkinchisi — biznes qoidasini.

2.3 Bir nechta fayl

Interceptor Dekorator Nima
FileInterceptor("fayl") @UploadedFile() bitta maydon, bitta fayl
FilesInterceptor("fayllar", 2) @UploadedFiles() bitta maydon, ko'pi bilan 2 ta
FileFieldsInterceptor([{ name, maxCount }]) @UploadedFiles() bir nechta nomli maydon
AnyFilesInterceptor() @UploadedFiles() istalgan maydon (ehtiyot bo'ling)

Global sozlama — MulterModule.register({ dest, limits }) modulda. Har interceptor'da sozlama berilsa, u ustun.

2.4 Xotira yoki disk

Multer'da ikki saqlash usuli bor:

  • memoryStorage (sukut) — fayl Buffer sifatida fayl.buffer da. Oson, lekin butun fayl RAM'da.
  • diskStorage — fayl vaqtinchalik papkaga yoziladi, fayl.path beriladi. RAM tejaladi, lekin disk ishi va keyin o'chirish kerak.

FileInterceptor("fayl", { storage: diskStorage({ destination, filename }) }). filename da foydalanuvchi bergan originalname ni to'g'ridan ishlatmang — path traversal xavfi (Fayl yuklash xavfsizligi).

3. ParseFilePipe va validatorlar

3.1 Haqiqiy xabarlar

Demo controller'ga oltita so'rov yubordik:

text
png: 201 {"nom":"chizma.png","hajm":16,"mime":"image/png"}
soxta png: 400 {"message":"Validation failed (current file type is image/png, expected type is image/png)","error":"Bad Request","statusCode":400}
2 MB: 400 {"message":"Validation failed (current file size is 2097152, expected size is less than 1048576)","error":"Bad Request","statusCode":400}
6 MB: 413 {"message":"File too large","error":"Payload Too Large","statusCode":413}
faylsiz: 400 {"message":"File is required","error":"Bad Request","statusCode":400}
3 ta fayl: 400 {"message":"Unexpected file field - fayllar","error":"Bad Request","statusCode":400}

"Soxta png" — rasm.png nomli, image/png turli, lekin ichida "salom" matni. Qatorma-qator:

So'rov Kim rad etdi Status
haqiqiy PNG — 201
soxta PNG FileTypeValidator (magic bytes) 400
2 MB MaxFileSizeValidator (1 MB) 400
6 MB Multer limits.fileSize (5 MB) 413
fayl yo'q ParseFilePipe (sukut fileIsRequired: true) 400
3 ta fayl (chegara 2) Multer (FilesInterceptor ning maxCount) 400

Ikki xil chegara bor: Multer limits — o'qish paytida, ortiqcha baytlar o'qilmaydi (413). MaxFileSizeValidator — fayl to'liq o'qilgandan keyin (400). Xavfsizlik uchun birinchisi muhim: u xotirani himoya qiladi. Ikkinchisi — chiroyli xabar va biznes qoidasi uchun.

3.2 Chalkash xabarlar

Ikki xabarni sekin o'qiymiz:

  • "Validation failed (current file type is image/png, expected type is image/png)" — "tekshiruv o'tmadi (joriy tur image/png, kutilgan tur image/png)". Ikkala tur bir xil — nega xato? Chunki validator mimetype'ni emas, baytlarni tekshirdi va PNG imzosini topmadi. Xabar esa mijoz yuborgan mimetype'ni ko'rsatadi. Bunday xabarni ko'rsangiz — fayl ichini tekshiring.
  • "Unexpected file field - fayllar" — "kutilmagan fayl maydoni". Maydon nomi to'g'ri, lekin fayllar soni maxCount dan oshdi. Multer bu holatni "kutilmagan maydon" deb ataydi (LIMIT_UNEXPECTED_FILE).

3.3 FileTypeValidator ichida

Nest 12.2.0 manba kodiga qaradik: FileTypeValidator file-type paketi (22.1.1) bilan fayl.buffer ning birinchi baytlarini o'qiydi. Bu 29-qismdagi magic bytes tekshiruvi (Fayl yuklash xavfsizligi). Sozlamalari:

Sozlama Nima qiladi
fileType kutilgan tur — satr yoki RegExp
skipMagicNumbersValidation baytlarni tekshirmaydi, faqat mimetype (xavfli)
fallbackToMimetype imzo topilmasa — mimetype'ga ishonadi
overrideMimeType aniqlangan haqiqiy turni fayl.mimetype ga yozadi

Ikki tuzoq kodda yozilgan:

  1. diskStorage bilan fayl.buffer yo'q. Validator baytlarni o'qiy olmaydi va sukut bo'yicha rad etadi: "file buffer is not available; file type validation could not be performed".
  2. CSV, TXT kabi matn fayllarida imzo yo'q. file-type ularni tanimaydi va validator rad etadi. fallbackToMimetype: true ularni o'tkazadi — lekin soxta PNG'ni ham.

Buni kichik modelda ko'ramiz (brauzerda ishlaydi):

js
// FileTypeValidator modeli: tur — mijoz aytgan mimetype'dan emas,
// faylning birinchi baytlaridan (magic bytes) aniqlanadi
const IMZOLAR = [
  { mime: "image/png", bosh: [0x89, 0x50, 0x4e, 0x47] },
  { mime: "image/jpeg", bosh: [0xff, 0xd8, 0xff] },
  { mime: "application/pdf", bosh: [0x25, 0x50, 0x44, 0x46] },
];

function turniAniqla(baytlar) {
  const mos = (i) => i.bosh.every((b, n) => baytlar[n] === b);
  return IMZOLAR.find(mos)?.mime;
}

function tekshir(fayl, kutilgan, sozlama = {}) {
  const { fallbackToMimetype = false } = sozlama;
  const haqiqiy = turniAniqla(fayl.baytlar);
  if (haqiqiy !== undefined) return haqiqiy === kutilgan;
  return fallbackToMimetype && fayl.mimetype === kutilgan;
}

const matn = (s) => [...new TextEncoder().encode(s)];
const fayllar = [
  { nom: "chizma.png", mimetype: "image/png",
    baytlar: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a] },
  { nom: "rasm.png", mimetype: "image/png", baytlar: matn("salom") },
  { nom: "royxat.csv", mimetype: "text/csv",
    baytlar: matn("id,matn") },
];
for (const f of fayllar) {
  const kutilgan = f.nom.endsWith(".csv") ? "text/csv" : "image/png";
  const qattiq = tekshir(f, kutilgan);
  const yumshoq = tekshir(f, kutilgan, { fallbackToMimetype: true });
  console.log(f.nom, "→", turniAniqla(f.baytlar) ?? "yo'q",
    "| qattiq:", qattiq, "| fallback:", yumshoq);
}

Konsolda:

text
chizma.png → image/png | qattiq: true | fallback: true
rasm.png → yo'q | qattiq: false | fallback: true
royxat.csv → yo'q | qattiq: false | fallback: true

Ikkinchi qator — xavf: fallbackToMimetype soxta PNG'ni o'tkazib yubordi. CSV kabi imzosiz turlar uchun fallback kerak bo'lsa, rasm uchun alohida, qattiq validator yozing. Model faqat uch imzoni biladi; file-type yuzlabini.

Tekshirib ko'ring: Validatorlar tartibi [MaxFileSizeValidator, FileTypeValidator]. 6 MB soxta PNG yuborildi, Multer limits.fileSize — 5 MB. Qaysi xabar keladi?

Javob

413 "File too large". Multer limitdan oshgan faylni o'qish paytida to'xtatadi — handler va ParseFilePipe umuman ishlamaydi. Validatorlar faqat Multer o'tkazgan faylni ko'radi.

3.4 ParseFilePipeBuilder

Xuddi shu sozlamaning zanjirli shakli ham bor:

ts
@UploadedFile(
  new ParseFilePipeBuilder()
    .addFileTypeValidator({ fileType: "image/png" })
    .addMaxSizeValidator({ maxSize: 1 * MB })
    .build({ errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY }),
)
fayl: Express.Multer.File,

errorHttpStatusCode — xato statusi (sukut 400). vazifalar-api validatsiya xatolariga 422 beradi; Multer yo'li bilan yozsak, shu sozlama kerak bo'lardi. Bu blokni ishga tushirmadik — sintaksisi Nest hujjatidan.

4. StreamableFile — faylni berish

Fayl qaytarish uchun @Res() shart emas. StreamableFile — oqim yoki Buffer ni o'raydigan klass. Nest uni ko'rib, javobni oqim bilan yuboradi:

ts
@Get("yukla")
ber() {
  return new StreamableFile(Readable.from(["id,matn\n1,Non\n"]), {
    type: "text/csv; charset=utf-8",
    disposition: 'attachment; filename="vazifalar.csv"',
  });
}
text
StreamableFile: 200 attachment; filename="vazifalar.csv" "id,matn\n1,Non\n"

Nima bo'ldi? Nest qaytarilgan obyekt StreamableFile ekanini ko'rdi: type dan Content-Type ni, disposition dan Content-Disposition ni qo'ydi, keyin oqimni javobga uladi. attachment so'zi brauzerga "faylni ochma, saqla" deydi — 29-qismdagi XSS himoyasining bir qismi. Handler @Res() ni umuman olmadi, javob yuborishni Nest o'zi qildi.

Afzalligi: interceptor va filtrlar ishlaydi, @Res() tuzog'i yo'q (Controller'lar va route dekoratorlari). Kamchiligi: Range so'rovlari (206, videoni o'rtasidan ochish) va ETag'ni o'zingiz qilishingiz kerak. Express res.sendFile ularni o'zi qiladi.

5. O'lchov: Multer xotirasi

5.1 Tajriba

Serverni alohida jarayonda ishga tushirdik, mijoz — curl (biz ishga tushirdik, bitta mashina, bitta o'lchov). Ikki endpoint: /multer — FileInterceptor (memoryStorage), /oqim — busboy bilan oqim (baytlar sanaladi, xotiraga yig'ilmaydi). Har so'rov oldidan gc() va server RSS'i (jarayon egallagan RAM) o'lchandi, so'rov paytida har 5 ms da eng katta RSS yozildi. Fayl — 100 MB. Chiqishni juft-juft o'qing: usul qatori — server javobi, undan keyingi engKatta qatori — shu so'rov paytidagi o'lchov. Eng birinchi qator — hech narsa yuklanmagandagi boshlang'ich holat:

text
{"engKatta":90,"gcDanKeyin":91}
{"usul":"oqim","hajm":104857600}
{"engKatta":140,"gcDanKeyin":91}
{"usul":"multer","hajm":104857600}
{"engKatta":293,"gcDanKeyin":292}
100 MB fayl: server RSS (MB)
  • boshlang'ich91 MB
  • oqim (busboy), eng katta+49 MB, gc'dan keyin 91140 MB
  • Multer, eng katta+202 MB, gc'dan keyin 292293 MB

Manba: Biz ishga tushirdik (2026-11-02): NestJS 12.2.0, multer 2.4.0, busboy 1.6.0, Node 24.21.0, bitta mashina, bitta o'lchov

5.2 O'qish

  • Multer 100 MB fayl uchun ~200 MB qo'shdi — fayldan ham ko'p. Multer kelgan bo'laklarni yig'ib, oxirida bitta Buffer ga birlashtiradi: bir lahza ikkala nusxa ham xotirada.
  • gc'dan keyin ham 292 MB. JS obyektlari bo'shadi, lekin jarayon xotirani operatsion tizimga qaytarmadi. Keyingi so'rovlar shu joyni qayta ishlatadi, lekin konteyner xotira chegarasi (masalan, 512 MB) shu "cho'qqi" bo'yicha ishlaydi.
  • Oqim ~50 MB vaqtinchalik bo'laklar bilan o'tdi va gc'dan keyin boshlang'ich holatga qaytdi.

Endi tasavvur qiling: to'rtta foydalanuvchi bir vaqtda 100 MB video yuklaydi. Multer bilan — ~800 MB, oqim bilan — bir necha o'n MB. 29-qismdagi Express o'lchovi ham shu tomonga ko'rsatgandi (Katta fayllarni stream bilan yuklash).

Tekshirib ko'ring: diskStorage xotira muammosini hal qiladimi? Nega vazifalar-api baribir undan foydalanmaydi?

Javob

RAM muammosini hal qiladi — fayl diskka yoziladi. Lekin vazifalar-api fayllarni S3 (MinIO) ga saqlaydi. diskStorage bilan fayl avval diskka, keyin diskdan S3 ga ketadi: ikki marta yozish, vaqtinchalik fayllarni tozalash, disk to'lishi xavfi. Oqim esa baytlarni to'g'ridan S3 multipart yuklashiga uzatadi. Ustiga, diskStorage bilan FileTypeValidator sukut bo'yicha ishlamaydi — buffer yo'q.

6. vazifalar-api da: oqim @Req() orqali

6.1 Controller

29-qismdagi Express routeri Nest controller'iga ko'chdi; SQL, S3 kodi va javoblar aynan qoldi. Yuklash handler'i (qisqartirilgan parcha, qatorlar bo'lingan):

ts
@Controller("vazifalar/:vazifaId")
@Cheklov(API_CHEKLOVI)
export class IlovalarController {
  constructor(
    @Inject(FAYLLAR) private readonly b: FaylBogliqliklari,
  ) {}

  @Post("ilovalar")
  async yukla(
    @Egasi() egasi: number,
    @Req() req: Request,
    @Res() res: Response,
  ): Promise<void> {
    const b = this.b;
    const vazifaId = idOl(req, "vazifaId");
    // Yuklashdan OLDIN: vazifa yo'q bo'lsa baytlarni S3 ga
    // yozmaymiz (aks holda yetim obyekt)
    await vazifaBormi(b.pool, egasi, vazifaId);
    const n = await oqimliYuklash(req, {
      ombor: b.ombor,
      maksHajm: b.maksHajm,
      kalit: (k) => saqlashKaliti(egasi, k, "fayllar"),
    }).catch(xatoniAylantir);
    // … bazaga yozish (RLS ostida), dublikat bo'lsa yangi obyektni
    // o'chirish, 201 + Location
  }
  // … GET ro'yxat, GET ilovalar.zip, GET :faylId (Range), DELETE
}

Nest'ga xos to'rt narsa:

  • @Req() — xom Express so'rovi. oqimliYuklash (29-qismdan, o'zgarmagan) req ni busboy'ga uzatadi va fayl bo'laklari S3 multipart yuklashiga oqadi. FileInterceptor yo'q — u tanani o'zi o'qib qo'yardi.
  • @Res() — javobni o'zimiz yuboramiz: Range (206), ZIP oqimi va Location. Bular res.status().location().json() va 29-qismdagi faylniBer/zipBer funksiyalari.
  • @Egasi() — kanonning param dekoratori: req.egasi ni beradi, yo'q bo'lsa 401 (Custom dekoratorlar). Anonim so'rov bu yerga yetmaydi — global KirishGuard uni oldinroq 401 bilan qaytaradi.
  • @Cheklov(API_CHEKLOVI) — 28-qismdagi so'rov limiti, endi guard.

vazifaBormi — vazifa egasiga tegishlimi, yuklashdan oldin tekshiradi. Aks holda begona vazifaga 100 MB yuklanib, keyin rad etilardi — S3 da yetim fayl qolardi.

6.2 Jonli natija

Kanon tekshiruvidan (haqiqiy MinIO, sessiya cookie'si bilan; token yashirilgan, mahalliy yo'l qisqartirildi, Permissions-Policy va ETag qatorlari olib tashlandi):

text
$ curl -H Cookie: __Host-sessiya=<token> -F fayl=@C:/Users/…/.ilova.png;filename=chizma.png;type=application/octet-stream http://127.0.0.1:4552/v1/vazifalar/9/ilovalar
HTTP/1.1 201 Created
Location: /v1/vazifalar/9/ilovalar/1
Content-Type: application/json; charset=utf-8
Content-Length: 80

{"faylId":1,"aslNom":"chizma.png","mime":"image/png","hajm":70,"dublikat":false}

$ curl -H Cookie: __Host-sessiya=<token> -H Range: bytes=0-7 -o NUL -D - http://127.0.0.1:4552/v1/vazifalar/9/ilovalar/1
HTTP/1.1 206 Partial Content
Content-Type: image/png
Content-Disposition: attachment; filename="chizma.png"; filename*=UTF-8''chizma.png
Accept-Ranges: bytes
Content-Range: bytes 0-7/70
Content-Length: 8

Birinchi so'rovga qarang: mijoz type=application/octet-stream yubordi, javobda esa "mime":"image/png". Server turni baytlardan aniqladi — mijoz aytganiga ishonmadi. Ikkinchisi — Range: 70 baytdan faqat 0–7 berildi (206).

7. Kanon topgan xatolar

7.1 POST sukuti 201 — @Res() bilan ham

Brauzer to'g'ridan S3 ga yuklagandan keyin faylni tasdiqlash yo'li bor: POST /v1/fayllar/:faylId/tasdiq. 29-qismda u 200 qaytarardi — Express sukuti. Nest'ga ko'chgach, 29-qismning fayl.test testi yiqildi: javob 201.

Sabab: Nest POST handler'lari uchun statusni (201) handler'dan oldin qo'yadi — @Res() bilan javobni o'zingiz yuborsangiz ham. res.json(...) status bermadi, demak oldindan qo'yilgan 201 ketdi. Tuzatish — bitta dekorator:

ts
@Post(":faylId/tasdiq")
// Nest POST sukuti 201 — @Res() bilan ham (status handler'dan OLDIN
// qo'yiladi; 29 da Express sukuti 200 edi — test ushladi)
@HttpCode(200)
async tasdiq(
  @Egasi() egasi: number,
  @Req() req: Request,
  @Res() res: Response,
): Promise<void> {
  // … HEAD + Range + magic bytes + sha256 → 'tayyor'
}

Saboq: Express'dan Nest'ga ko'chishda statuslar jim o'zgarishi mumkin. Buni faqat status kodni tekshiradigan test ushlaydi — 29-qism testlari shunday edi.

7.2 Inglizcha sarlavha — kichik nomuvofiqlik

Jonli tekshiruvdagi yana bir javob — begona vazifaga yuklash:

text
{"type":"https://vazifalar-api.example/xatolar/topilmadi","title":"Not Found","status":404,"detail":"Vazifa topilmadi","instance":"/v1/vazifalar/14/ilovalar","kod":"topilmadi","sorovId":"2503258c-e48f-4dd1-ac32-ad50b8399615"}

title — "Not Found", inglizcha. API'ning boshqa xatolarida sarlavha o'zbekcha ("Vazifa topilmadi", "Kirish kerak"). Sabab: fayl kodida new TopilmadiXatosi("Vazifa topilmadi", "topilmadi") — uchinchi argument (sarlavha) berilmagan, Problem Details yasovchisi esa Node'ning STATUS_CODES[404] ni oladi. Bu kod 29-qismdan aynan ko'chgan. Xavfsizlikka ta'sir qilmaydi — kod va status to'g'ri — lekin mijoz title ni foydalanuvchiga ko'rsatsa, inglizcha matn chiqadi. Kanonda bu tuzatilmagan; tuzatish — 3-mashq g'oyasida, sarlavhani doim berish.

8. Hujumchi nigohi

Fayl yuklash — eng ko'p hujum qilinadigan joylardan biri. Faqat o'z lokal loyihangizda sinang.

  • Ulkan fayl (DoS). limits siz FileInterceptor — 10 GB fayl xotirani to'ldiradi. Himoya: Multer limits.fileSize yoki oqimda bayt hisoblagichi (kanonda maksHajm, 413).
  • Soxta tur. .png nomli HTML yoki SVG — keyin brauzerda ochilsa XSS. Himoya: magic bytes (FileTypeValidator yoki kanondagi turniAniqla), berishda Content-Disposition: attachment.
  • Fayl nomi. ../../etc/passwd yoki boshqarish belgilari. Himoya: saqlash kaliti serverda yasaladi (saqlashKaliti), asl nom faqat tozalanib saqlanadi (faylNomiTozala). 29-qismdagi Multer preservePath topilmasini eslang (Fayl yuklash xavfsizligi).
  • Begona vazifa (BOLA). POST /v1/vazifalar/<begona id>/ilovalar. Himoya: RLS + har so'rovda AND egasi_id = $n — jonli natijadagi 404.

9. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
fayl undefined maydon nomi mos emas yoki multipart/form-data emas FileInterceptor("<mijozdagi nom>")
"Unexpected file field" nom noto'g'ri yoki maxCount oshdi nom va sonni tekshiring
"current file type is image/png, expected type is image/png" baytlar PNG emas fayl ichini tekshiring
FileTypeValidator CSV'ni rad etadi matn faylida imzo yo'q alohida qoida; ehtiyot bilan fallbackToMimetype
diskStorage bilan tur tekshiruvi yiqiladi buffer yo'q o'z tekshiruvingiz yoki memoryStorage + kichik limit
POST javobi 201, kutilgani 200 Nest POST sukuti, @Res() bilan ham @HttpCode(200)
Anonim so'rov ham katta faylni xotiraga yuklaydi Multer middleware sifatida ulangan — guard'dan oldin o'qiydi FileInterceptor — guard'dan keyin ishlaydi

Oxirgi qatorni Express'dan ko'chgan loyihalarda tez-tez ko'rasiz: app.use(multer(...)) Express'da odatiy edi, Nest'da esa u himoyani chetlab o'tadi.

10. Mashqlar

Mashqlar kurs/mashqlar/30/62-fayl-yuklash/ da.

1-mashq (oson): kim rad etadi?

Multer limits.fileSize: 5 MB, ParseFilePipe da MaxFileSizeValidator({ maxSize: 1 MB }) va FileTypeValidator({ fileType: "image/png" }).

  • 3 MB haqiqiy PNG — status
  • 8 MB haqiqiy PNG — status
  • 100 KB, ichi JPEG, nomi .png — status
  • Fayl yuborilmadi (sukut sozlama) — status
Yechim
  • 400 — Multer o'tkazdi (5 MB dan kichik), MaxFileSizeValidator (1 MB) rad etdi.
  • 413 — Multer o'qish paytida to'xtatdi, validatorlar ishlamadi.
  • 400 — FileTypeValidator baytlardan JPEG'ni aniqladi, kutilgani PNG.
  • 400 — ParseFilePipe sukuti fileIsRequired: true: "File is required".

2-mashq (o'rta): oqimda hajm chegarasi

Paketsiz model. oqimdaSana(bolaklar, maks) — bo'laklar massivini (har biri Uint8Array) birma-bir "o'qiydi". Jami bayt maks dan oshishi bilan to'xtab, { status: 413, oqildi } qaytarsin (oqildi — to'xtagungacha o'qilgan bayt). Oshmasa — { status: 201, hajm }. Ikki holatni sinang. Ishora: for...of va break (for...of va for...in).

Yechim
js
function oqimdaSana(bolaklar, maks) {
  let jami = 0;
  for (const b of bolaklar) {
    jami += b.length;
    if (jami > maks) return { status: 413, oqildi: jami };
  }
  return { status: 201, hajm: jami };
}

const KB = 1024;
const bolak = new Uint8Array(64 * KB);
const kichik = Array.from({ length: 4 }, () => bolak);   // 256 KB
const katta = Array.from({ length: 100 }, () => bolak);  // 6.25 MB
console.log("kichik:", JSON.stringify(oqimdaSana(kichik, 1024 * KB)));
console.log("katta:", JSON.stringify(oqimdaSana(katta, 1024 * KB)));

Konsolda:

text
kichik: {"status":201,"hajm":262144}
katta: {"status":413,"oqildi":1114112}

Katta faylda 6.25 MB dan faqat ~1.06 MB o'qildi — qolgani umuman o'qilmadi. Kanondagi oqimliYuklash va Multer limits.fileSize shu g'oyada ishlaydi. MaxFileSizeValidator esa butun faylni o'qib bo'lgach tekshiradi.

3-mashq (qiyin): sarlavhasiz xatoni ushlash

Paketsiz model. problemYasa(xato) Problem Details obyektini qaytarsin: title — xato.sarlavha, berilmagan bo'lsa — STATUS jadvalidagi inglizcha nom (kanondagidek). tekshirSarlavha(problem) — title inglizcha jadvalda bo'lsa, "ogohlantirish: <status> uchun o'zbekcha sarlavha yo'q" qaytarsin, aks holda "ok". Ikki xato bilan sinang: sarlavhali va sarlavhasiz 404. Ishora: obyekt qiymatlari ro'yxati — Object.values (Obyekt metodlari).

Yechim
js
const STATUS = {
  404: "Not Found", 409: "Conflict", 413: "Payload Too Large",
};

function problemYasa(xato) {
  return {
    status: xato.status,
    title: xato.sarlavha ?? STATUS[xato.status] ?? "Xato",
    detail: xato.xabar,
  };
}

function tekshirSarlavha(p) {
  const inglizcha = Object.values(STATUS).includes(p.title);
  return inglizcha
    ? `ogohlantirish: ${p.status} uchun o'zbekcha sarlavha yo'q`
    : "ok";
}

const xatolar = [
  { status: 404, xabar: "Vazifa 14 topilmadi",
    sarlavha: "Vazifa topilmadi" },
  { status: 404, xabar: "Vazifa topilmadi" },
];
for (const x of xatolar) {
  const p = problemYasa(x);
  console.log(p.title, "→", tekshirSarlavha(p));
}

Konsolda:

text
Vazifa topilmadi → ok
Not Found → ogohlantirish: 404 uchun o'zbekcha sarlavha yo'q

Haqiqiy loyihada bunday tekshiruvni test sifatida yozish mumkin: hamma xato yo'llarini chaqirib, title inglizcha emasligini tekshirish. Shunda kanondagi nomuvofiqlik darhol ko'rinardi.

11. Loyiha qadami: fayl ilovalari Nest'da

Bu va keyingi uch dars bitta katta qadam: 29-qism integratsiyalari Nest'ga ko'chadi. Commit — WebSocket gateway'lar darsida.

  1. 29-qismdagi fayl modullari o'zgarishsiz ko'chadi: src/fayl/yuklash.ts (oqimli yuklash), xavfsizlik.ts (magic bytes, fayl nomi, kalit), s3.ts (FaylOmbori), metama.ts (baza), berish.ts (Range, ZIP). SQL migratsiyalari ham (29-qismdagi 201–206). Ularni 29-qism yakuniy commit'ingizdan qaytaring — Passport darsidagi kabi: git checkout <29-qism-commit> -- src/fayl/yuklash.ts. Siz hamon nest tarmog'idasiz; 29-qism commit'i esa main ning oxirgi commit'i — git log main --oneline -1 uni ko'rsatadi. Auth fayllaridan farqi: bu safar main dagi eng oxirgi holat kerak, oraliq commit emas.
  2. Yangi fayl — src/fayl/fayl.controller.ts: IlovalarController va FayllarController. Express routeridagi har handler — Nest metodi; ichki kod aynan. «Controller» bo'limidagi naqsh bo'yicha: @Req() oqim, @Res() javob, @Egasi(), @Cheklov(API_CHEKLOVI), tasdiq da @HttpCode(200).
  3. Yangi fayl — src/umumiy/egasi.ts (@Egasi() dekoratori):
ts
export function egasiOl(req: Request): number {
  if (req.egasi === undefined) throw new KirilmaganXatosi();
  return req.egasi;
}

export const Egasi = createParamDecorator(
  (_: unknown, k: ExecutionContext): number =>
    egasiOl(k.switchToHttp().getRequest<Request>()),
);

Controller'larni modulga ulash va S3 klientini yaratish — Tashqi xizmatlarni Nest modullariga o'rash darsida (IntegratsiyaModule). Hozircha fayllar kompilyatsiya bo'ladi, lekin marshrutlar hali ulanmagan.

Tekshiruv:

bash
npm run tip
npm run lint

Testlar (fayl.test) — MinIO bilan, modul ulangandan keyin. Hali commit qilmang.

12. Real ishda

  • Ko'p Nest loyihalarida FileInterceptor + memoryStorage + S3'ga PutObject — kichik fayllar (avatar, chek) uchun yetarli va oddiy. Katta fayllarda (video, arxiv) jamoalar presigned URL'ga o'tadi — brauzer to'g'ridan S3 ga yuklaydi (Presigned URL'lar); kanondagi POST /v1/fayllar/forma shu.
  • Fastify'da FileInterceptor ishlamaydi — @fastify/multipart va o'z ishlovchi (Fastify adapter bilan NestJS).
  • Intervyu: "Nest'da fayl qanday qabul qilinadi?", "Fayl turini qanday tekshirasiz — mimetype yetarlimi?", "Katta fayllarni qanday yuklaysiz?".

Xulosa

  • Nest standarti: FileInterceptor/FilesInterceptor (Multer) + @UploadedFile(new ParseFilePipe({ validators })); limits majburiy.
  • Multer limits o'qish paytida to'xtatadi (413), validatorlar o'qilgandan keyin (400); FileTypeValidator magic bytes'ni file-type bilan tekshiradi.
  • FileTypeValidator xabari chalkash, diskStorage bilan ishlamaydi, fallbackToMimetype soxta faylni o'tkazadi.
  • O'lchov: 100 MB fayl — Multer ~+200 MB RSS, oqim ~+50 MB; kanon oqimni (@Req() → busboy → S3) saqladi.
  • Ko'chish topilmalari: POST sukuti 201 @Res() bilan ham (@HttpCode(200) — tuzatildi), sarlavhasiz 404 — "Not Found" (tuzatilmagan).

Keyingi dars: Navbatlar: @nestjs/bullmq — @Processor va WorkerHost bilan Nest ishchisini yozamiz va kanon nega ishchini Nest'siz qoldirganini o'lchov bilan ko'ramiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Fayl yuklash NestJS'da: FileInterceptor, ParseFilePipe va oqimli yuklash — IlmHamroh