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

Presigned URL'lar: brauzer faylni to'g'ridan S3'ga yuklaydi

Qisqacha: Presigned URL (oldindan imzolangan havola) — server o'z maxfiy kaliti bilan imzolab bergan, muddati cheklangan havola. Brauzer shu havola bilan faylni to'g'ridan omborga yuklaydi yoki yuklab oladi, baytlar API serveridan o'tmaydi. Sir havolada yo'q — faqat HMAC imzo. Lekin kanon o'lchovi ko'rsatdi: presigned PUT hajmni cheklamaydi va sukutda Content-Type ni imzolamaydi. Hajm va tur shartlari kerak bo'lsa — presigned POST, va har holda yuklangandan keyin server faylni o'zi tekshiradi.

Bu darsda

  • Presigned URL ichida nima borligini va imzo qanday hisoblanishini (SigV4) o'z qo'lingiz bilan ko'rasiz.
  • Presigned PUT va POST farqini, ularning shartlari va tuzoqlarini kanon o'lchovi bilan bilasiz.
  • Havola muddatini to'g'ri tanlaysiz va 7 kunlik chegarani bilasiz.
  • Katta faylni brauzer qismlab yuklaydigan multipart presigned oqimini tushunasiz.
  • Yuklangandan keyin server faylni qanday tasdiqlashini (hajm, sehrli baytlar) o'rganasiz.

Oldin bilishingiz kerak: Object storage: S3 va MinIO, Fayl yuklash xavfsizligi, crypto 1: tasodif, hash va HMAC, CORS chuqur.

Oldingi darsda FaylOmbori klassini yozdik va oxirgi ikki metod — yuklashFormasi va olishHavolasi — "keyingi darsda" dedik. Mana shu dars.

1. Nega bu kerak?

Hozirgacha fayl shunday yo'l bosdi: brauzer → vazifalar-api → ombor. Server har baytni o'zi qabul qilib, omborga uzatdi. Bu ishlaydi, lekin uchta narxi bor:

  1. Trafik ikki marta. 100 MB video avval serverga, keyin serverdan omborga ketadi. Server tarmog'i va CPU band.
  2. Serverless cheklovi. AWS Lambda kabi muhitlarda so'rov tanasi hajmi cheklangan — katta fayl serverga sig'maydi (Serverless va edge funksiyalar darsida).
  3. Yuklab olishda ham xuddi shunday. Har yuklab olish server orqali o'tadi.

Presigned URL yo'lni qisqartiradi: server faqat ruxsat beradi, baytlar brauzer bilan ombor orasida yuradi.

sequenceDiagram
  participant B as Brauzer
  participant A as vazifalar-api
  participant S as Ombor (S3)
  B->>A: Yuklashga ruxsat (nom, tur, hajm)
  A->>A: Huquq va limitni tekshirish
  A-->>B: Presigned URL (5 daqiqa)
  B->>S: PUT/POST fayl (havola bilan)
  S-->>B: 200 yoki 204
  B->>A: Tayyor (kalit)
  A->>S: HEAD + Range 0-4099
  A->>A: Hajm va sehrli baytlar
  A-->>B: Fayl tasdiqlandi

Diagrammaga qarang: server ikki marta ishtirok etadi — boshida (ruxsat) va oxirida (tasdiq). O'rtadagi og'ir qism — omborda.

Kanon lokal o'lchovi (5 MB obyekt, 20 marta, mediana):

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

Lokal mashinada farq deyarli yo'q — hamma narsa bitta kompyuterda. Presigned'ning foydasi tezlikda emas, server yukida: production'da server 5 MB ni o'zi tortib, o'zi uzatmaydi. Bu o'lchovni Fayl yuklab berish va arxivlash darsida batafsil ko'ramiz.

Darsdagi MinIO javoblari — kanon kompyuteridagi lokal MinIO'dan. MinIO image'i endi Docker Hub'da yo'qligini oldingi darsda ko'rdik. Shuning uchun tarmoqsiz ishlaydigan qismlarni (imzo hisobi, havola yasash) biz alohida yurgizib tasdiqladik.

2. Havola ichida nima bor

2.1 Kanon havolasi

Kanon PutObject uchun 300 soniyalik havola yasadi va uning so'rov parametrlarini qatorma-qator chiqardi (imzo qisqartirilgan, kalit ID yashirilgan):

text
yo'l: /t05-presigned/put/rasm.png
  X-Amz-Algorithm = AWS4-HMAC-SHA256
  X-Amz-Content-Sha256 = UNSIGNED-PAYLOAD
  X-Amz-Credential = <kalit>/20261027/us-east-1/s3/aws4_request
  X-Amz-Date = 20261027T135227Z
  X-Amz-Expires = 300
  X-Amz-Signature = 96807a756bde…
  X-Amz-SignedHeaders = host
  x-amz-checksum-crc32 = AAAAAA==
  x-amz-sdk-checksum-algorithm = CRC32
  x-id = PutObject
Eslatma: sir (secret key) URL'da YO'Q — faqat kalit ID va HMAC imzo.
Parametr Ma'nosi
X-Amz-Algorithm imzo algoritmi — SigV4 (Signature Version 4)
X-Amz-Credential kalit ID + sana + hudud + xizmat ("doira", scope)
X-Amz-Date imzolangan vaqt (UTC)
X-Amz-Expires necha soniya amal qiladi
X-Amz-SignedHeaders imzoga kirgan HTTP sarlavhalar
X-Amz-Signature HMAC-SHA256 imzo (64 hex belgi)
X-Amz-Content-Sha256 UNSIGNED-PAYLOAD — tana imzolanmagan

Oxirgi qator muhim: tana imzoga kirmaydi. Havola "shu kalitga shu vaqtgacha yozish mumkin" deydi, "aynan shu baytlarni" emas.

2.2 Imzoni o'zimiz hisoblaymiz

Imzo qanday yasalishini ko'rish uchun SigV4 ni Web Crypto bilan qo'lda takrorlaymiz. AWS hujjatidagi namuna qiymatlarini olamiz (examplebucket, test.txt, haqiqiy bo'lmagan namuna kalit):

js
const enc = new TextEncoder();
const hex = (b) => [...new Uint8Array(b)]
  .map((x) => x.toString(16).padStart(2, "0")).join("");

async function hmac(kalit, matn) {
  const k = await crypto.subtle.importKey("raw", kalit,
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
  return crypto.subtle.sign("HMAC", k, enc.encode(matn));
}
const sha256 = async (s) =>
  hex(await crypto.subtle.digest("SHA-256", enc.encode(s)));

// AWS hujjatidagi namuna (haqiqiy kalit emas)
const KALIT_ID = "AKIAIOSFODNN7EXAMPLE";
const SIR = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
const kun = "20130524";
const doira = `${kun}/us-east-1/s3/aws4_request`;

async function imzola(muddat) {
  const sorov = [
    "X-Amz-Algorithm=AWS4-HMAC-SHA256",
    "X-Amz-Credential=" + encodeURIComponent(`${KALIT_ID}/${doira}`),
    `X-Amz-Date=${kun}T000000Z`,
    `X-Amz-Expires=${muddat}`,
    "X-Amz-SignedHeaders=host",
  ].join("&");
  const kanonik = ["GET", "/test.txt", sorov,
    "host:examplebucket.s3.amazonaws.com", "", "host",
    "UNSIGNED-PAYLOAD"].join("\n");
  const imzolanadigan = ["AWS4-HMAC-SHA256", `${kun}T000000Z`,
    doira, await sha256(kanonik)].join("\n");
  let k = await hmac(enc.encode("AWS4" + SIR), kun);
  for (const q of ["us-east-1", "s3", "aws4_request"]) {
    k = await hmac(k, q);
  }
  return hex(await hmac(k, imzolanadigan));
}

console.log(await imzola(86400));
console.log(await imzola(604800));

Konsolda:

text
aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404
ac6b6752478f6402474c09fa025d8c8ac0012e84d29a679d5600972fc7a664a4

Nimaga qarash kerak: birinchi qatorni AWS hujjatidagi namuna havolaning X-Amz-Signature qiymati bilan solishtiring (biz 2026-10-28 da hujjatni ochib tekshirdik) — ular aynan bir xil. Demak, algoritmni to'g'ri takrorladik. Ikkinchi qator — faqat muddati boshqa havola. Imzo uch bosqichda yasaladi:

  1. Kanonik so'rov (canonical request) — metod, yo'l, so'rov parametrlari, imzolanadigan sarlavhalar, tana hash'i (bu yerda UNSIGNED-PAYLOAD). Har biri yangi qatordan.
  2. Imzolanadigan satr — algoritm, vaqt, doira va kanonik so'rovning SHA-256 hash'i.
  3. Imzolash kaliti — sirdan to'rt marta HMAC: sana → hudud → xizmat → aws4_request. Sirning o'zi hech qayerga yuborilmaydi.

Ikkinchi qator nima uchun kerak? Hujumchi havoladagi X-Amz-Expires=86400 ni 604800 ga o'zgartirdi deylik. Ombor imzoni qayta hisoblaydi — boshqa natija chiqadi (ikkinchi qator) va so'rov 403 SignatureDoesNotMatch bilan rad etiladi. Havoladagi har parametr imzo bilan himoyalangan; sirsiz yangi imzo yasab bo'lmaydi (HMAC).

Maslahat: Imzoni hech qachon o'zingiz yozmang — SDK buni qiladi. Bu blok faqat ichini ko'rish uchun. Lekin endi bilasiz: havola — "sir egasining muddatli va aniq ruxsati", xolos.

Tekshirib ko'ring: Presigned URL sizib chiqdi (masalan, logga yozildi). Hujumchi u bilan nima qila oladi va nima qila olmaydi?

Javob

Muddat tugaguncha aynan shu amalni (masalan, shu kalitga PUT yoki shu obyektni GET) bajara oladi. Boshqa kalitga yozish, muddatni uzaytirish, boshqa amal qilish — imzo buziladi. Sir ham sizmaydi. Shuning uchun muddat qisqa (daqiqalar) bo'ladi va havolalar logga to'liq yozilmaydi.

3. Presigned PUT

3.1 Yasash

ts
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const havola = await getSignedUrl(
  s3,
  new PutObjectCommand({
    Bucket: "vazifalar",
    Key: "vaqtincha/7/1f3c….png",
    ContentType: "image/png",
  }),
  { expiresIn: 300 },
);

getSignedUrl tarmoqqa chiqmaydi — u faqat hisoblaydi. Brauzer keyin fetch(havola, { method: "PUT", body: fayl, headers: { "Content-Type": "image/png" } }) qiladi.

3.2 Kanon topgan uchta tuzoq

Kanon PUT havolasini MinIO'ga turli tanalar bilan yubordi:

text
30 MB nol 'image/png' sifatida: 200 → bucket'da 31457280 bayt
boshqa Content-Type bilan: 200 
ContentLength 100 imzolangan, 101 bayt yuborildi: 403 <?xml version="1.0" encoding="UTF-8"?> <Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you pr
aynan 100 bayt: 200

Birinchi tuzoq — hajm cheklanmaydi. 30 MB nol "rasm" sifatida qabul qilindi. PUT havolasida hajm sharti yo'q. Kim havolani olsa, istalgan hajmdagi faylni yozadi. ContentLength: 100 ni imzoga kiritsa bo'ladi (uchinchi va to'rtinchi qator: 101 bayt — 403, 100 bayt — 200). Lekin bu aniq hajm — "1 MB gacha" degan oraliq emas. Brauzer faylning aniq hajmini oldindan bilganda ishlaydi.

Ikkinchi tuzoq — Content-Type imzolanmagan. ContentType: "image/png" berdik, lekin text/html bilan ham 200. SDK 3.1153 sukutida faqat host imzolanadi (SignedHeaders=host). Kanon buni majburlab ko'rdi:

text
SignedHeaders = content-type;host | x-amz-checksum-crc32 bormi: false
text/html bilan: 403 <?xml version="1.0" encoding="UTF-8"?> <Error><Code>SignatureDoesNotMatch</Code><Message>T
image/png bilan: 200 — tur sarlavhasi tekshirildi, MAZMUN emas (6-bo'lim)

signableHeaders: new Set(["content-type"]) bilan endi boshqa tur 403 oldi. Lekin oxirgi qatorga qarang: image/png sarlavhasi bilan png bo'lmagan matn ham o'tdi. Ombor sarlavhani tekshiradi, mazmunni emas.

Uchinchi tuzoq — bo'sh tananing CRC32. Sukut havolada x-amz-checksum-crc32=AAAAAA== bor — bu bo'sh tananing tekshiruv yig'indisi. MinIO uni tekshirmadi va 30 MB ni qabul qildi. AWS S3 bu parametrni tekshiradimi — kanon sinamadi (AWS hisobi yo'q). requestChecksumCalculation: "WHEN_REQUIRED" bilan parametr yo'qoladi.

Kanonning ikki natijasini biz MinIO'siz ham tasdiqladik: havola yasash tarmoqsiz ishlaydi, shuning uchun uni namuna kalit bilan yurgizdik (biz ishga tushirdik, @aws-sdk 3.1153.0, Node 24.21.0):

js
const put = new PutObjectCommand({
  Bucket: "vazifalar", Key: "vaqtincha/7/a.png",
  ContentType: "image/png",
});
const p1 = new URL(await getSignedUrl(s3, put, { expiresIn: 300 }));
console.log("sukut:", p1.searchParams.get("X-Amz-SignedHeaders"),
  p1.searchParams.get("x-amz-checksum-crc32"));

const s3b = new S3Client({
  ...sozlama, requestChecksumCalculation: "WHEN_REQUIRED",
});
const p2 = new URL(await getSignedUrl(s3b, put, {
  expiresIn: 300, signableHeaders: new Set(["content-type"]),
}));
console.log("qat'iy:", p2.searchParams.get("X-Amz-SignedHeaders"),
  p2.searchParams.get("x-amz-checksum-crc32"));

Konsolda:

text
sukut: host AAAAAA==
qat'iy: content-type;host null

Diqqat: Presigned PUT bilan "faqat rasm, 5 MB gacha" degan qoidani ombor tomonida qo'yib bo'lmaydi. Ikki yo'l bor: aniq hajm va turni imzolash yoki presigned POST. Ikkala holda ham yuklangandan keyin server tekshiruvi majburiy («Yuklangandan keyin tasdiqlash» bo'limi).

4. Presigned POST: shartlar bilan

4.1 Policy

POST — HTML formasi bilan yuklash usuli. Server bergan policy (siyosat) — shartlar ro'yxati, base64 qilinib imzolanadi. Ombor faylni qabul qilishdan oldin har shartni tekshiradi. Kanon shunday forma yasadi:

ts
import { createPresignedPost } from "@aws-sdk/s3-presigned-post";

const post = await createPresignedPost(s3, {
  Bucket: "t05-presigned",
  Key: "post/${filename}",
  Conditions: [
    ["content-length-range", 1, 1024 * 1024],
    ["starts-with", "$Content-Type", "image/"],
  ],
  Fields: { "Content-Type": "image/png" },
  Expires: 120,
});

Key dagi ${filename} — JS shablon satri emas (oddiy qo'shtirnoq ichida), S3 ning o'z belgisi: ombor uni yuklangan fayl nomi bilan almashtiradi. Natija va policy:

text
url: http://127.0.0.1:9100/t05-presigned | maydonlar: Content-Type, bucket, X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, key, Policy, X-Amz-Signature
Policy: {"expiration":"2026-10-27T13:54:28Z","conditions":[["content-length-range",1,1048576],["starts-with","$Content-Type","image/"],{"Content-Type":"image/png"},{"bucket":"t05-presigned"},{"X-Amz-Algorithm":"AWS4-HMAC-SHA256"},{"X-Amz-Credential":"<kalit>/20261027/us-east-1/s3/aws4_request"},{"X-Amz-Date":"20261027T135228Z"},["starts-with","$key","post/"]]}

Ikki turdagi shart bor: ["starts-with", "$maydon", "prefiks"] va { "maydon": "aniq qiymat" }. content-length-range — PUT da yo'q narsa: hajm oralig'i. SDK ${filename} ni ko'rib, kalit uchun starts-with $key post/ shartini o'zi qo'shdi. Biz aniq kalit (vaqtincha/7/b.png) bilan yasaganimizda shart {"key":"vaqtincha/7/b.png"} bo'ldi (biz ishga tushirdik).

4.2 Ombor qanday tekshiradi

Ombor ichidagi mantiqni toza JS bilan taqlid qilamiz (o'quv modeli — haqiqiy S3 kodi emas):

js
const policy = {
  expiration: "2026-10-27T13:54:28Z",
  conditions: [
    ["content-length-range", 1, 1048576],
    ["starts-with", "$Content-Type", "image/"],
    ["starts-with", "$key", "post/"],
  ],
};

function shartlar(p, forma, hajm, hozir) {
  if (hozir > Date.parse(p.expiration)) return "403 muddat o'tgan";
  for (const [tur, a, b] of p.conditions) {
    if (tur === "content-length-range" && (hajm < a || hajm > b)) {
      return "400 hajm sharti";
    }
    if (tur === "starts-with") {
      const qiymat = String(forma[a.slice(1)] ?? "");
      if (!qiymat.startsWith(b)) return `403 shart: ${a}`;
    }
  }
  return "204 qabul qilindi";
}

const vaqt = Date.parse("2026-10-27T13:53:00Z");
const png = { key: "post/a.png", "Content-Type": "image/png" };
console.log(shartlar(policy, png, 500 * 1024, vaqt));
console.log(shartlar(policy, png, 2 * 1024 * 1024, vaqt));
console.log(shartlar(policy,
  { ...png, "Content-Type": "text/html" }, 10, vaqt));
console.log(shartlar(policy,
  { ...png, key: "fayllar/9/a.png" }, 10, vaqt));
console.log(shartlar(policy, png, 10, vaqt + 5 * 60_000));

Konsolda:

text
204 qabul qilindi
400 hajm sharti
403 shart: $Content-Type
403 shart: $key
403 muddat o'tgan

Endi haqiqiy MinIO javoblari (kanon):

text
500 KB image/png: 204 
2 MB: 400 <?xml version="1.0" encoding="UTF-8"?> <Error><Code>EntityTooLarge</Code><Message>Your proposed upload exceeds the maximum allowed object si
text/html: 403 <?xml version="1.0" encoding="UTF-8"?> <Error><Code>AccessDenied</Code><Message>Access Denied. (Invalid according to Policy: Policy Conditio
${filename} o'rniga: [ 'post/kichik.png' ]

Modelimiz bilan bir xil manzara: 500 KB — 204 No Content (muvaffaqiyat, tana yo'q), 2 MB — 400 EntityTooLarge ("taklif qilingan yuklash ruxsat etilgan maksimal hajmdan oshdi"), text/html — 403 AccessDenied … Policy Condition failed. ${filename} o'rniga asl nom (kichik.png) qo'yildi.

Diqqat: ${filename} qulay, lekin kalitga foydalanuvchi nomi tushadi — Fayl yuklash xavfsizligi darsidagi barcha muammolar (takror nom, maxsus belgilar) qaytadi. Kanon modulining yuklashFormasi kalitni server o'zi yasaydi (vaqtincha/<egasi>/<uuid>.png) va aniq turni talab qiladi (eq $Content-Type).

4.3 Brauzer tomoni

js
const javob = await fetch("/api/fayllar/forma", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ tur: fayl.type, hajm: fayl.size }),
});
const { url, fields } = await javob.json();

const fd = new FormData();
for (const [k, v] of Object.entries(fields)) fd.append(k, v);
fd.append("file", fayl); // fayl — OXIRIDA
const r = await fetch(url, { method: "POST", body: fd });
console.log(r.status); // 204

Ikki qoida:

  • Server bergan hamma maydon formaga o'zgarishsiz qo'shiladi — har biri policy'da imzolangan.
  • file maydoni — oxirgi. AWS hujjatiga ko'ra fayldan keyingi maydonlar e'tiborga olinmaydi.

Brauzer boshqa domenga (api.example.uz → bucket.s3...) so'rov yuboradi, shuning uchun bucket'da CORS qoidasi kerak: qaysi origin, qaysi metod (POST, PUT). Kanon CORS'ni haqiqiy brauzerda sinamadi — tamoyil CORS chuqur darsidagidek.

Presigned PUT Presigned POST
Hajm oralig'i yo'q (faqat aniq hajm) content-length-range
Tur prefiksi yo'q (faqat aniq tur, signableHeaders bilan) starts-with $Content-Type
Brauzerda fetch(url, { method: "PUT" }) FormData + maydonlar
Katta fayl (qismlab) ha, multipart bilan yo'q (AWS'da POST 5 GB gacha)

5. Muddat

text
darhol: 200
2.1 s dan keyin: 403 <?xml version="1.0" encoding="UTF-8"?> <Error><Code>AccessDenied</Code><Message>Request has expired</Message><Key>put/an
8 kun: Error: Signature version 4 presigned URLs must have an expiration date less than one week in the future
  • 1 soniyalik havola darhol ishladi, 2.1 soniyadan keyin Request has expired ("so'rov muddati o'tgan") — 403.
  • 8 kunlik havolani SDK yasamadi: SigV4 havolasi ko'pi bilan 7 kun (604 800 soniya). Biz ham MinIO'siz yurgizib aynan shu xabarni oldik.

Muddat qanday tanlanadi:

Amal Tavsiya Nega
Yuklash (PUT/POST) 5–15 daqiqa foydalanuvchi faylni tanlab, darhol yuklaydi
Rasmni sahifada ko'rsatish 5–60 daqiqa sahifa yangilansa yangi havola
Hisobot yuklab olish 1–24 soat xatdagi havola

Hujjatga ko'ra (sinalmagan): havola vaqtinchalik kalit (IAM rol, STS token) bilan imzolansa, kalitning o'zi tugaganda havola ham ishlamay qoladi — expiresIn katta bo'lsa ham. Kanon MinIO'da doimiy kalit ishlatdi.

Tekshirib ko'ring: Server soati 10 daqiqa orqada. Siz expiresIn: 300 bilan havola berdingiz. Nima bo'ladi?

Javob

X-Amz-Date server soatidan olinadi. Ombor uchun bu vaqt 10 daqiqa oldin bo'lgan, 5 daqiqalik havola esa allaqachon tugagan — foydalanuvchi darhol Request has expired oladi. Server soati NTP bilan sinxron bo'lishi kerak (Backend'da sana va vaqt).

6. Presigned GET: yuklab olish

Yuklab olish uchun ham xuddi shu naqsh — GetObjectCommand imzolanadi. Faylning yuklab olinadigan nomi ResponseContentDisposition bilan beriladi: ombor javobga shu sarlavhani qo'yadi (sarlavha qiymati ham imzoga kiradi, hujumchi o'zgartira olmaydi):

ts
const havola = await getSignedUrl(s3, new GetObjectCommand({
  Bucket: "vazifalar",
  Key: "fayllar/7/1f3c….pdf",
  ResponseContentDisposition:
    contentDisposition("Hisobot oʻzbekcha.pdf"),
}), { expiresIn: 300 });

contentDisposition — oldingi darsda ko'rgan funksiya, UTF-8 nom bilan. Kanon testi (MinIO bilan) tekshiradi: muddat ichida — 200, sarlavha aynan contentDisposition(...) natijasi, tana to'g'ri; 2.1 soniyadan keyin — 403 Request has expired. Sarlavhaning o'zi — Fayl yuklab berish va arxivlash darsida.

7. Katta fayl: multipart presigned

5 GB dan katta faylni bitta PUT bilan yuklab bo'lmaydi, 100 MB faylni ham bitta so'rov bilan yuklash xavfli — uzilsa, boshidan. Yechim — oldingi darsdagi multipart, lekin endi qismlarni brauzer yuklaydi:

  1. Server: CreateMultipartUpload → UploadId.
  2. Server: har qism uchun UploadPartCommand ni imzolaydi (har qismning o'z havolasi).
  3. Brauzer: qismlarni PUT qiladi, har javobdan ETag ni oladi.
  4. Server: CompleteMultipartUpload — qism raqamlari va ETag'lar ro'yxati bilan.

Kanon 5 + 5 + 1 MB qism bilan sinadi:

text
qismlar ETag: [
  '"08b46181d7094b5ece88bb389c7499af"',
  '"d76f89e449720d99d4b92c79040f4882"',
  '"3bc3be0b850eacf461ec036374616058"'
]
Complete ETag: "9874e3d5b93e67a3c90859515d6035a1-3" | hajm: 11534336
Eslatma: brauzer ETag sarlavhasini o'qishi uchun bucket CORS'ida ExposeHeaders: ETag kerak (MinIO/S3 sozlamasi; brauzerda sinalmagan).

Har qismning ETag'i — o'sha qism MD5 i, yakuniy ETag oxirida -3 (oldingi darsdagidek, butun faylning MD5 i emas). ETag'lar fayl tarkibidan hisoblanadi — sizning faylingizda boshqa qiymatlar chiqadi.

Eslatmaga e'tibor bering. Brauzer javob sarlavhalarining faqat "xavfsiz" qismini o'qiy oladi; ETag ni o'qish uchun bucket CORS sozlamasida ExposeHeaders: ["ETag"] bo'lishi kerak. Busiz r.headers.get("etag") null qaytaradi va yuklashni yakunlab bo'lmaydi. Kanon buni haqiqiy brauzerda sinamadi.

Chala qolgan multipart (brauzer yopildi) omborda joy egallaydi. Uni server AbortMultipartUpload bilan yoki bucket lifecycle qoidasi bilan tozalaydi (Katta fayllarni stream bilan yuklash darsidagi MinIO topilmasini eslang).

8. Yuklangandan keyin tasdiqlash

Presigned yo'lda server baytlarni ko'rmadi. Brauzer "yukladim" desa — bu ham mijoz so'zi. Kanon tasdiqlash tartibi:

  1. HeadObject — obyekt bormi, hajmi limitda va policy'dagi bilan mosmi.
  2. GetObject + Range: bytes=0-4099 — faqat boshidagi 4 100 bayt (butun fayl emas).
  3. Sehrli baytlar bilan haqiqiy tur (Fayl yuklash xavfsizligi).
  4. Mos — bazada tayyor, mos emas — obyekt o'chiriladi.
text
post/haqiqiy.png: e'lon image/png, 231 bayt, Range javobi bytes 0-230/231, haqiqiy image/png → TASDIQLANDI
post/yolgon.png: e'lon image/png, 302 bayt, Range javobi bytes 0-301/302, haqiqiy application/x-msdownload → RAD — o'chiriladi

Ikkinchi fayl — MZ bilan boshlanadigan exe, "image/png" deb yuklangan. Ombor uni qabul qildi (tur sarlavhasi to'g'ri edi), server esa sehrli baytlardan haqiqatni topdi va o'chirdi. Fayl kichik bo'lgani uchun Range javobi butun faylni qamradi (bytes 0-301/302).

stateDiagram-v2
  [*] --> kutilmoqda: forma berildi
  kutilmoqda --> tayyor: HEAD va magic bytes mos
  kutilmoqda --> ochirildi: tur yoki hajm mos emas
  kutilmoqda --> ochirildi: 24 soatda tasdiq yo'q
  tayyor --> [*]
  ochirildi --> [*]

Holatlar bazada saqlanadi va "24 soatda tasdiqlanmagan" yozuvlar tozalanadi — buni Fayl metama'lumotini DB'da boshqarish darsida quramiz. Rasmlar uchun yana bir qatlam bor: qayta kodlash (Rasmlarni qayta ishlash: sharp) — polyglot fayl ham yangi, toza rasmga aylanadi.

9. Hujumchi nigohi

  1. Hajm cheklovsiz PUT. Hujumchi bitta havola bilan 5 GB yozadi — omborga pul to'laysiz. Himoya: POST + content-length-range yoki aniq ContentLength ni imzolash; tasdiqda hajm tekshiruvi.
  2. Tur almashtirish. HTML yoki SVG image/png deb yuklanadi va keyin shu domenda ochiladi. Himoya: sehrli bayt tasdig'i, nosniff, attachment (Fayl yuklash xavfsizligi).
  3. Kalitni mijoz tanlaydi. Key ni so'rov tanasidan olsangiz, hujumchi fayllar/8/… ga — boshqa odamning fayli ustiga yozadi. Himoya: kalitni faqat server yasaydi, egasi ID'si bilan.
  4. Uzoq muddat. 7 kunlik havola chat yoki logga tushdi. Himoya: daqiqalar; yuklab olish havolasini har safar yangidan berish.
  5. Tasdiqsiz ishlatish. "Yuklandi" degan javobga ishonib, faylni darhol vazifaga biriktirish. Himoya: holat kutilmoqda, faqat server tasdig'idan keyin tayyor.

10. Ko'p uchraydigan xatolar

Xato Sabab Davosi
SignatureDoesNotMatch (403) yuborilgan sarlavha imzolangandan farq qiladi (Content-Type, hajm) aynan imzolangan qiymatlarni yuboring
Request has expired (403) muddat o'tdi yoki server soati noto'g'ri qisqa, lekin yetarli muddat; NTP
SDK: "less than one week" expiresIn > 604 800 ko'pi bilan 7 kun
EntityTooLarge (400) POST hajm sharti foydalanuvchiga limitni ayting
Policy Condition failed (403) tur, kalit yoki boshqa shart formaga hamma maydonni o'zgarishsiz qo'shing
Brauzerda CORS xatosi bucket CORS qoidasi yo'q origin va metodni ruxsat eting
etag null brauzerda ExposeHeaders: ETag yo'q CORS sozlamasi
file dan keyingi maydonlar yo'qoladi file oxirida emas file — oxirgi

11. Mashqlar

Mashqlar kurs/mashqlar/29/05-presigned/ da.

1-mashq (oson): bo'shliqlarni to'ldiring

  • SigV4 havolasi ko'pi bilan kun amal qiladi.
  • Presigned PUT sukutida faqat sarlavhasi imzolanadi.
  • POST policy'da hajm oralig'i sharti —
  • Muvaffaqiyatli presigned POST javobi statusi —
  • Faylni POST formasida qo'shish joyi —
Yechim
  • 7 kun — 604 800 soniya, undan ko'pini SDK yasamaydi.
  • host — Content-Type uchun signableHeaders kerak.
  • content-length-range — [shart, min, max].
  • 204 — No Content.
  • oxirida — fayldan keyingi maydonlar e'tiborga olinmaydi.

2-mashq (o'rta): havola qachon tugaydi

muddat(url, hozir) funksiyasini yozing: presigned URL dan X-Amz-Date (20261027T135227Z shaklida) va X-Amz-Expires ni olib, { tugash, qolgan } qaytarsin: tugash — ISO satr, qolgan — butun soniyalar (manfiy emas). Kanon havolasi parametrlari bilan ikki vaqtda sinang: 13:55:00 va 14:00:00 (UTC). Ishora: new URL(url).searchParams, slice bilan ISO satr yig'ing.

Yechim
js
function muddat(url, hozir) {
  const p = new URL(url).searchParams;
  const d = p.get("X-Amz-Date");
  const iso = `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}T` +
    `${d.slice(9, 11)}:${d.slice(11, 13)}:${d.slice(13, 15)}Z`;
  const soniya = Number(p.get("X-Amz-Expires"));
  const tugash = Date.parse(iso) + soniya * 1000;
  return {
    tugash: new Date(tugash).toISOString(),
    qolgan: Math.max(0, Math.round((tugash - hozir) / 1000)),
  };
}

const url = "http://127.0.0.1:9100/t05-presigned/put/rasm.png" +
  "?X-Amz-Date=20261027T135227Z&X-Amz-Expires=300";
console.log(muddat(url, Date.parse("2026-10-27T13:55:00Z")));
console.log(muddat(url, Date.parse("2026-10-27T14:00:00Z")));

Konsolda:

text
{ tugash: '2026-10-27T13:57:27.000Z', qolgan: 147 }
{ tugash: '2026-10-27T13:57:27.000Z', qolgan: 0 }

Frontend shu hisob bilan havola eskirganini oldindan biladi va serverdan yangisini so'raydi. Lekin oxirgi so'z omborniki — mijoz soati ham noto'g'ri bo'lishi mumkin.

3-mashq (qiyin): multipart qismlar rejasi

qismlarRejasi(hajm, qism = 5 MB) funksiyasini yozing: fayl hajmidan { PartNumber, bosh, hajm } massivini qaytarsin (PartNumber 1 dan). Qoidalar (S3): qism kamida 5 MB (oxirgisidan tashqari), qismlar soni 10 000 dan oshmaydi — buzilsa Error tashlang. Kanondagi 11 534 336 baytli fayl bilan, so'ng 20 MB, 1 MB va 60 000 MB, 5 MB bilan sinang.

Yechim
js
const MB = 1024 * 1024;
function qismlarRejasi(hajm, qism = 5 * MB) {
  if (qism < 5 * MB) throw new Error("qism kamida 5 MB");
  const soni = Math.ceil(hajm / qism);
  if (soni > 10000) throw new Error(`qismlar juda ko'p: ${soni}`);
  return Array.from({ length: soni }, (_x, i) => ({
    PartNumber: i + 1,
    bosh: i * qism,
    hajm: Math.min(qism, hajm - i * qism),
  }));
}
console.log(qismlarRejasi(11534336));
for (const [h, q] of [[20 * MB, MB], [60000 * MB, 5 * MB]]) {
  try { qismlarRejasi(h, q); } catch (e) { console.log(e.message); }
}

Konsolda:

text
[
  { PartNumber: 1, bosh: 0, hajm: 5242880 },
  { PartNumber: 2, bosh: 5242880, hajm: 5242880 },
  { PartNumber: 3, bosh: 10485760, hajm: 1048576 }
]
qism kamida 5 MB
qismlar juda ko'p: 12000

Natija kanondagi 5 + 5 + 1 MB bilan bir xil. Brauzerda har qism fayl.slice(bosh, bosh + hajm) bilan kesiladi va o'z havolasiga yuboriladi. 60 000 MB fayl uchun qismni kattalashtiring: Math.ceil(hajm / 10000) dan kichik bo'lmasin.

12. Loyiha qadami: yuklashFormasi va olishHavolasi

Kanon modulida presigned uchun ikki metod bor — s3.ts dagi FaylOmbori ichida (to'liq fayl oldingi darsda). Telefon uchun bo'lingan, mazmuni kanondagidek:

ts
/** Brauzer to'g'ridan bucket'ga yuklashi uchun presigned POST:
 * hajm (1…maksHajm) va aniq Content-Type sharti S3 tomonida
 * tekshiriladi (PUT da hajm sharti yo'q — kanon tajribasi). */
async yuklashFormasi(
  kalit: string, tur: string, maksHajm: number, muddatSoniya = 300,
): Promise<PresignedPost> {
  return createPresignedPost(this.s3, {
    Bucket: this.bucket,
    Key: kalit,
    Conditions: [
      ["content-length-range", 1, maksHajm],
      ["eq", "$Content-Type", tur],
    ],
    Fields: { "Content-Type": tur },
    Expires: muddatSoniya,
  });
}

/** Yuklab olish uchun muddatli havola; faylNomi bo'lsa
 * Content-Disposition (UTF-8) javobga qo'yiladi. */
async olishHavolasi(
  kalit: string, muddatSoniya = 300, faylNomi?: string,
): Promise<string> {
  return getSignedUrl(
    this.s3,
    new GetObjectCommand({
      Bucket: this.bucket, Key: kalit,
      ...(faylNomi ? {
        ResponseContentDisposition: contentDisposition(faylNomi),
      } : {}),
    }),
    { expiresIn: muddatSoniya },
  );
}

Uch qaror: kalitni server beradi (chaqiruvchi saqlashKaliti(egasi, "png", "vaqtincha") bilan yasaydi), tur eq — aniq (prefiks emas), hajm — oraliq. Kanon PUT'ni ataylab tanlamadi — hajmni cheklab bo'lmaydi.

Kanon testlari (test/fayl-s3.test.ts, MinIO bilan) presigned uchun ikki holatni tekshiradi:

  • POST: 500 bayt image/png — 204; 1 001 bayt (limit 1 000) — 400 EntityTooLarge; text/html — 403 Policy Condition failed|AccessDenied.
  • GET: muddat ichida 200 + Content-Disposition; 2.1 soniyadan keyin 403 Request has expired.

Kanonda modullar paketi to'liq o'tdi (82/82). Bizda Docker yoqilmagan — MinIO'siz ishga tushirdik (biz ishga tushirdik, Node 24.21.0):

text
✔ contentDisposition: UTF-8 nom va ASCII zaxira (2.3296ms)
﹣ S3 (MinIO) bilan (0.2792ms) # S3_ENDPOINT yo'q
ℹ tests 1
ℹ suites 1
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 563.0319

﹣ belgisi — MinIO talab qiladigan to'plam o'tkazildi (skipped 0 ga aldanmang). Presigned URL yasash qismini yuqorida MinIO'siz alohida tasdiqladik.

Commit xabari (modul uchun):

bash
git commit -m "feat(fayl): presigned POST (hajm, tur) va GET havolasi"

vazifalar-api ga ulash 28-qism (auth) tugagach: forma berish marshruti, tasdiq marshruti va yuklab olish havolasi — har biri egasini tekshirib.

13. Real ishda

  • Ko'p fayl xizmatlari yuklab olish uchun muddatli havola beradi. Masalan, Telegram Bot API hujjatiga ko'ra getFile bergan havola kamida 1 soat amal qiladi, keyin yangisini so'raysiz (Telegram Bot API asoslari darsida).
  • Mobil ilovalar katta videoni multipart presigned bilan, qism-qism, internet uzilsa davom ettirib yuklaydi.
  • Uppy kabi frontend kutubxonalari aynan shu oqimni (forma olish → to'g'ridan S3 → tasdiq) tayyor beradi; server tomoni — siz yozgan ikki marshrut.
  • Intervyuda: "Presigned URL qanday ishlaydi va sir qayerda?", "PUT va POST farqi?", "Foydalanuvchi yuklagan faylga qachon ishonasiz?"

Xulosa

  • Presigned URL — server sirida HMAC bilan imzolangan, muddatli ruxsat; sir havolada yo'q, har parametr imzoda.
  • PUT hajmni cheklamaydi va sukutda Content-Type ni imzolamaydi (SignedHeaders=host); bo'sh tananing CRC32 qo'shiladi.
  • POST policy: content-length-range, starts-with, eq; file — formaning oxirida; bucket CORS kerak.
  • Muddat — daqiqalar; SigV4 maksimumi 7 kun.
  • Multipart presigned: har qism o'z havolasi, ETag'larni o'qish uchun ExposeHeaders: ETag.
  • Yuklangandan keyin server tasdiqlaydi: HEAD + Range: bytes=0-4099 + sehrli baytlar; mos emas — o'chiriladi.

Keyingi dars: Fayl metama'lumotini DB'da boshqarish — fayllar jadvali, egasi bo'yicha dublikat, biriktirish va yetim fayllarni tozalash.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

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