Mundarija (32)
- Bu darsda
- 1. Nega bu kerak?
- 2. Asosiy tushunchalar
- 2.1 Bucket, kalit, obyekt
- 2.2 "Papka" — illyuziya
- 2.3 Bucket nomi qoidalari
- 3. MinIO: lokal S3
- 3.1 Docker'da ishga tushirish
- 3.2 MinIO holati: 2026-10
- 4. Node'dan ulanish: AWS SDK v3
- 4.1 O'rnatish va klient
- 4.2 Buyruq naqshi
- 5. Asosiy amallar: kanon tajribasi
- 5.1 Bucket yaratish
- 5.2 Yozish, ma'lumot, o'qish
- 5.3 Yo'q kalit: uch xil javob
- 6. Ro'yxat: prefiks va sahifalash
- 7. Katta obyektlar: multipart
- 8. Private va public bucket
- 8.1 Sukut — yopiq
- 8.2 Qaysi biri qachon
- 9. AWS S3, Cloudflare R2 va boshqalar
- 10. Hujumchi nigohi
- 11. Ko'p uchraydigan xatolar
- 12. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): obyektlarni "papka" bo'yicha sanash
- 3-mashq (qiyin): sahifalarni yuruvchi generator
- 13. Loyiha qadami: s3.ts — FaylOmbori klassi
- 14. Real ishda
- Xulosa
- Manbalar
Object storage: S3 va MinIO bilan fayllarni saqlash Node'da
Qisqacha: Object storage (obyekt ombori) — fayllarni server diskida emas, alohida xizmatda saqlash: bucket (savat) ichida har fayl bitta kalit (satr) ostida yotadi, papkalar yo'q. Amazon S3 ning API'si sohada standartga aylangan: Cloudflare R2, MinIO va boshqalar o'sha API'ni gapiradi, Node'dan esa
@aws-sdk/client-s3(v3) bilan ishlanadi. Bucket sukutda private; ochiq faqat kerakli prefiks (masalanavatar/*).
Bu darsda
- Object storage nima va nega fayllar server diskida saqlanmasligini tushuntirasiz.
- Bucket, kalit, obyekt va metama'lumotni farqlaysiz, "papka" illyuziyasini tushunasiz.
@aws-sdk/client-s3bilan yozish, o'qish, ro'yxat va o'chirish buyruqlarini berasiz.- ETag, xatolar (
NoSuchKey,NotFound) va multipart cheklovlarini kanon o'lchovi bilan bilasiz. - Private va public bucket farqini, MinIO, AWS S3 va Cloudflare R2 tanlovini bilasiz.
Oldin bilishingiz kerak: Fayl yuklash: multipart va Multer, Katta fayllarni stream bilan yuklash, Environment o'zgaruvchilar va .env, Async iteratorlar va Web Streams.
Oldingi darsda fayl oqimini FaylOmbori.yoz ga uzatdik va "ombor keyingi darsda" dedik. Mana u.
1. Nega bu kerak?
Birinchi darsda Multer fayllarni server diskiga yozdi. Kichik loyihada bu ishlaydi. Lekin vazifalar-api o'sganda uchta muammo chiqadi:
- Bir nechta server. Foydalanuvchi soni oshdi, API ikki nusxada ishlaydi. Malika faylni 1-serverga yukladi, keyingi so'rovi 2-serverga tushdi — u yerda fayl yo'q.
- Disk tugaydi va o'ladi. Server diskini kengaytirish qiyin, zaxira nusxasini o'zingiz qilasiz. Server o'chsa — fayllar ham.
- Trafik. Har yuklab olish API serveri orqali o'tadi, uni band qiladi.
Object storage bularning hammasini alohida xizmatga beradi. Server faqat "qaysi faylni kim ko'ra oladi" ni hal qiladi. Baytlarni saqlash, nusxalash va tarqatishni ombor qiladi.
O'xshatish: kutubxonadagi garderob. Siz paltongizni berasiz, raqamli jeton olasiz. Palto qaysi javonda turgani sizga qiziq emas — jeton bo'yicha qaytarib berishadi. Kalit — jeton, bucket — garderob.
Fayl yuklash va progress darsida brauzer tomondan "fayllar S3, R2 yoki MinIO ga boradi" degan edik. Endi server tomonini quramiz.
2. Asosiy tushunchalar
2.1 Bucket, kalit, obyekt
- Bucket (savat) — obyektlar idishi. Har bucket o'z nomi, sozlamalari va ruxsatlari bilan. Loyiha uchun odatda bitta yoki bir nechta bucket.
- Kalit (key) — obyektning bucket ichidagi to'liq nomi, oddiy satr:
vazifalar/7/1f3c…b2.png. - Obyekt — baytlar + metama'lumot (tur, hajm, o'zgargan vaqt, o'z yozuvlaringiz).
flowchart LR
A[vazifalar-api] -->|PutObject kalit| B[(Bucket: vazifalar)]
A -->|GetObject kalit| B
B --- O1[vazifalar/7/uuid.png]
B --- O2[vazifalar/8/uuid.pdf]
B --- O3[avatar/7.webp]
D[(Postgres)] -.->|kalit, egasi, nomi| ADiagrammaga qarang: API omborga kalit bo'yicha yozadi va o'qiydi. Qaysi kalit kimga tegishli — Postgres'da (Fayl metama'lumoti darsida). Ombor faylning egasini bilmaydi.
| Fayl tizimi (disk) | Object storage | |
|---|---|---|
| Tuzilma | papkalar daraxti | tekis: bucket → kalit |
| Tahrirlash | faylning o'rtasini o'zgartirsa bo'ladi | faqat butunlay qayta yozish |
| Kirish | fs |
HTTP API (S3) |
| Hajm | disk hajmicha | amalda cheksiz |
Ikkinchi qatorga e'tibor bering: obyektni qisman o'zgartirib bo'lmaydi. Rasmni kichraytirsangiz — yangi obyekt yozasiz.
2.2 "Papka" — illyuziya
Kalitda / bor, lekin papka yo'q. vazifalar/7/a.png — shunchaki 17 belgili satr. Konsol va SDK papkani ko'rsatadi, xolos: kalitlarni prefiks bo'yicha tanlab, / gacha guruhlaydi. Buni toza JavaScript bilan takrorlaymiz (kanon tajribasidagi kalitlar bilan):
const kalitlar = [
"avatar/7.webp",
"vazifalar/7/a.png",
"vazifalar/7/b.png",
"vazifalar/7/eslatma.txt",
"vazifalar/8/c.png",
];
function royxat(prefiks = "", ajratgich) {
const fayllar = [];
const papkalar = new Set();
for (const k of kalitlar.filter((k) => k.startsWith(prefiks))) {
const qolgan = k.slice(prefiks.length);
const i = ajratgich ? qolgan.indexOf(ajratgich) : -1;
if (i === -1) fayllar.push(k);
else papkalar.add(prefiks + qolgan.slice(0, i + 1));
}
return { papkalar: [...papkalar], fayllar };
}
console.log(royxat("vazifalar/7/").fayllar);
console.log(royxat("", "/"));
console.log(royxat("vazifalar/", "/").papkalar);Konsolda:
[ 'vazifalar/7/a.png', 'vazifalar/7/b.png', 'vazifalar/7/eslatma.txt' ]
{ papkalar: [ 'avatar/', 'vazifalar/' ], fayllar: [] }
[ 'vazifalar/7/', 'vazifalar/8/' ]S3 da xuddi shu ikki parametr bor: Prefix (shu bilan boshlanadiganlar) va Delimiter (shu belgigacha guruhlash → CommonPrefixes). Kanon MinIO'dan aynan shunday javob oldi — buni «Ro'yxat: prefiks va sahifalash» bo'limida ko'ramiz.
Amaliy natija: "papkani o'chirish" yoki "papkani qayta nomlash" degan amal yo'q. Prefiks ostidagi har obyekt alohida o'chiriladi yoki nusxalanadi.
2.3 Bucket nomi qoidalari
Bucket nomi DNS nomi sifatida ishlatilishi mumkin (vazifalar.s3.amazonaws.com). Shuning uchun qat'iy qoidalar bor (AWS hujjati). Nom — 3–63 belgi, faqat kichik lotin harflar, raqamlar, nuqta va chiziqcha, harf yoki raqam bilan boshlanib tugaydi, ikki nuqta yonma-yon emas, IP manzilga o'xshamaydi.
function bucketNomiTogrimi(nom) {
if (nom.length < 3 || nom.length > 63) return false;
if (!/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/.test(nom)) return false;
if (nom.includes("..")) return false;
if (/^\d+\.\d+\.\d+\.\d+$/.test(nom)) return false;
return true;
}
for (const nom of ["vazifalar", "t04-vazifalar", "Katta_Harf",
"ab", "192.168.1.10", "vazifa..fayl"]) {
console.log(nom, bucketNomiTogrimi(nom));
}Konsolda:
vazifalar true
t04-vazifalar true
Katta_Harf false
ab false
192.168.1.10 false
vazifa..fayl falseKanon MinIO'da Katta_Harf bucket'ini yaratmoqchi bo'ldi — server InvalidBucketName: The specified bucket is not valid. ("ko'rsatilgan bucket nomi yaroqsiz") bilan rad etdi. AWS'da oddiy bucket nomi barcha hisoblar orasida noyob bo'lishi kerak: vazifalar ni kimdir allaqachon olgan bo'lishi mumkin. Shuning uchun nomga loyiha va muhit qo'shiladi: ilova-vazifalar-prod.
Tekshirib ko'ring:
vazifalar/7/"papkasi"da 3 ta fayl bor. Ulardan hammasini o'chirsangiz,vazifalar/7/papkasi ro'yxatda qoladimi?
Javob
Yo'q. Papka alohida narsa emas — u kalitlardagi prefiksdan "yasaladi". Shu prefiksli kalit qolmasa, CommonPrefixes da ham ko'rinmaydi. (Ba'zi vositalar bo'sh "papka" ko'rsatish uchun / bilan tugaydigan bo'sh obyekt yaratadi — lekin bu ham oddiy obyekt.)
3. MinIO: lokal S3
3.1 Docker'da ishga tushirish
Haqiqiy AWS hisobisiz S3 ni o'rganish uchun kanon MinIO ni ishlatdi — S3 API'sini gapiradigan ochiq kodli server. Kanon buyrug'i (kalitlar .env dan; parolni kodga yozmang):
docker run -d --name minio \
-p 127.0.0.1:9000:9000 -p 127.0.0.1:9001:9001 \
-e MINIO_ROOT_USER=$S3_KALIT -e MINIO_ROOT_PASSWORD=$S3_SIR \
-v minio-data:/data \
minio/minio:latest server /data --console-address :90019000— S3 API porti, kod shunga ulanadi.9001— veb-konsol: brauzerda bucket va obyektlarni ko'rish.-v minio-data:/data— obyektlar konteyner o'chganda ham qoladi (Volume).
Kanon mashinasida bu buyruq RELEASE.2025-09-07T16-13-09Z image'ini ishlatdi — u mashinada oldindan yuklab olingan edi. Kanon 9100/9101 portlarini ochdi, shuning uchun uning natijalarida shu portlar uchraydi.
Diqqat: Bu buyruq yangi kompyuterda hozir ishlamasligi mumkin — sababi keyingi bo'limda.
3.2 MinIO holati: 2026-10
MinIO'ning tarqatish siyosati o'zgardi. Rasmiy GitHub repozitoriyasida (github.com/minio/minio, 2026-10-28 da o'qildi):
- "THIS REPOSITORY IS NO LONGER MAINTAINED" — repozitoriya endi qo'llab-quvvatlanmaydi;
- repozitoriya 2026-04-25 da arxivlangan (faqat o'qish uchun);
- community nashri endi faqat manba kod sifatida tarqatiladi, tayyor binar fayllar chiqarilmaydi; eski binarlar yangilanmaydi;
- kompaniya o'rniga AIStor (bepul va tijoriy nashrlar) ni taklif qiladi.
Amalda bu nimani anglatadi:
- Tayyor Docker image yo'q. Biz 2026-10-28 da Docker Hub API'sini so'radik:
minio/miniovaminio/mcrepozitoriylari javobi —404(topilmadi). IBM qo'llab-quvvatlash sahifasi (2026-09-25) ham Quay'dagi MinIO image'i o'chirilganini aytadi. Demakdocker run minio/minioimage'i kompyuterida yo'q odamda ishlamaydi. - Image oldindan yuklangan kompyuterda (kanondagidek) MinIO ishlaydi, lekin xavfsizlik yamoqlarisiz qoladi. Lokal o'rganish uchun bo'ladi; production'da yangi o'rnatish uchun tanlamang. Rasmiy README'ga ko'ra endi uni faqat manba koddan o'zingiz yig'asiz.
- Kursdagi kod MinIO'ga emas, S3 API'ga yozilgan — omborni almashtirsangiz, faqat
endpointva kalitlar o'zgaradi. Shuning uchun darsdagi natijalarni kanon chiqishidan o'qiysiz, toza JS bloklarni esa o'zingiz yurgizasiz.
Muqobillar (kanonda sinalmagan, hujjat bo'yicha): Garage va SeaweedFS — o'z serveringizda S3 mos ombor; LocalStack — testlar uchun AWS xizmatlari taqlidi. Internetda MinIO'ning boshqa odamlar qayta joylagan image nusxalari ham bor — ular rasmiy emas, ichida nima borligini kafolatlab bo'lmaydi, ishlatmang.
Diqqat: MinIO kalitlari (
MINIO_ROOT_USER,MINIO_ROOT_PASSWORD) — administrator huquqi. Portni127.0.0.1dan tashqariga ochmang va sukutminioadminparolini qoldirmang.
4. Node'dan ulanish: AWS SDK v3
4.1 O'rnatish va klient
npm i @aws-sdk/client-s3@3.1153.0AWS SDK v3 — modulli: har xizmat alohida paket. Eski aws-sdk (v2) ni eski kodda uchratasiz: u bitta katta paket, 2025-yil 8-sentyabrda qo'llab-quvvatlanishi tugagan. Yangi kodda faqat v3.
import { S3Client } from "@aws-sdk/client-s3";
const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT, // MinIO: http://127.0.0.1:9000
region: process.env.S3_REGION ?? "us-east-1",
forcePathStyle: true, // MinIO uchun
credentials: {
accessKeyId: process.env.S3_KALIT,
secretAccessKey: process.env.S3_SIR,
},
});endpoint— AWS S3 da bermaysiz (SDK o'zi biladi). MinIO va R2 da — majburiy.region— AWS'da bucket joylashgan hudud. MinIO uchun istalgan qiymat; kanonus-east-1.forcePathStyle: true— manzilhttp://host:9000/bucket/kalitko'rinishida. AWS sukutdabucket.s3.amazonaws.com/kalit(virtual-hosted) ishlatadi. Lokal MinIO'da bucket uchun DNS nomi yo'q — shuning uchun path-style.- Kalitlar faqat env'dan (Sirlarni boshqarish).
.env.example (namuna, haqiqiy kalit emas):
S3_ENDPOINT=http://127.0.0.1:9000
S3_REGION=us-east-1
S3_BUCKET=vazifalar
S3_KALIT=almashtiring
S3_SIR=almashtiring-uzun-tasodifiy4.2 Buyruq naqshi
SDK v3 da har amal — buyruq (command) obyekti, send bilan yuboriladi:
import { PutObjectCommand } from "@aws-sdk/client-s3";
await s3.send(new PutObjectCommand({
Bucket: "vazifalar",
Key: "vazifalar/7/eslatma.txt",
Body: "Oʻzbekcha matn: gʻalaba\n",
ContentType: "text/plain; charset=utf-8",
}));Hamma buyruq shu shaklda: new XCommand({ Bucket, Key, … }). Parametr nomlari katta harf bilan — S3 HTTP API'sidagi nomlar.
5. Asosiy amallar: kanon tajribasi
5.1 Bucket yaratish
Kanon t04-vazifalar ni yaratdi, keyin xuddi shu nomni qayta yaratib ko'rdi:
yaratildi: t04-vazifalar
qayta yaratish: BucketAlreadyOwnedByYou: Your previous request to create the named bucket succeeded and you already own it. 409
noto'g'ri nom: InvalidBucketName: The specified bucket is not valid.BucketAlreadyOwnedByYou — "bu bucket allaqachon sizniki", status 409 Conflict. Kanon modul shuning uchun avval HeadBucket ("bormi?") so'raydi, yo'q bo'lsa yaratadi. Production'da esa bucket kod ishga tushganda emas, infratuzilma bilan oldindan yaratiladi.
5.2 Yozish, ma'lumot, o'qish
Kanon matn obyektini metama'lumot bilan yozdi, keyin HeadObject (faqat ma'lumot, baytlarsiz) va GetObject (baytlar bilan) qildi:
ETag: "bd5b55efe3e9b38fc49e47c72b25fdab" | MD5: "bd5b55efe3e9b38fc49e47c72b25fdab"
Head: {
ContentLength: 26,
ContentType: 'text/plain; charset=utf-8',
Metadata: { 'asl-nom': 'eslatma%20(1).txt', egasi: '7' },
LastModified: 'object'
}
Get Body turi: ChecksumStream | matn: "Oʻzbekcha matn: gʻalaba\n"Uch narsani o'qiymiz.
ETag. Obyektning "barmoq izi". Bitta so'rov bilan yozilgan obyektda u — tananing MD5 hash'i (crypto: hash). Tekshirib ko'ramiz:
import { createHash } from "node:crypto";
const matn = "Oʻzbekcha matn: gʻalaba\n";
console.log(Buffer.byteLength(matn));
console.log(createHash("md5").update(matn).digest("hex"));Konsolda:
26
bd5b55efe3e9b38fc49e47c72b25fdabHajm 26 bayt — matnda 24 belgi, lekin ʻ UTF-8 da 2 bayt. MD5 kanon ETag'iga aynan teng. Lekin multipart yuklashda bunday emas — «Katta obyektlar: multipart» bo'limida ko'ramiz. ETag'ni HTTP keshi uchun ham ishlatsa bo'ladi (HTTP caching).
Metama'lumot (Metadata). O'zingizning kalit: qiymat yozuvlaringiz, HTTP'da x-amz-meta-* sarlavhalari bo'lib boradi. Sarlavhalar ASCII bo'lishi kerak, shuning uchun kanon asl nomni encodeURIComponent bilan yozdi:
const asl = "eslatma (1).txt";
const meta = encodeURIComponent(asl);
console.log(meta);
console.log(decodeURIComponent(meta) === asl);
console.log(encodeURIComponent("Oʻzbekcha hisobot.pdf"));Konsolda:
eslatma%20(1).txt
true
O%CA%BBzbekcha%20hisobot.pdfMetama'lumot bilan qidirib bo'lmaydi va hajmi cheklangan (AWS — 2 KB). Faylning egasi, asl nomi va holati asosan bazada saqlanadi; ombordagi metama'lumot — qo'shimcha.
Body. GetObject tanasi — oqim (kanon ko'rgan klass — ChecksumStream: SDK oqimni o'qish bilan birga tekshiruv yig'indisini ham hisoblaydi). Kichik obyektni await get.Body.transformToString() bilan satrga aylantirish mumkin. Katta faylni esa oqim sifatida mijozga uzatiladi (Fayl yuklab berish darsida).
5.3 Yo'q kalit: uch xil javob
yo'q kalit Get: NoSuchKey: The specified key does not exist. 404
yo'q kalit Head: NotFound: UnknownError 404
yo'q kalitni Delete: 204 (xato emas — idempotent)GetObject—NoSuchKey, aniq xabar bilan.HeadObject—NotFound: UnknownError. Nega "noma'lum"? HEAD javobida tana bo'lmaydi (HTTP metodlari) — xato tafsiloti yoziladigan joy yo'q, SDK faqat 404 ni biladi.DeleteObject— yo'q kalitni o'chirish ham 204. O'chirish idempotent: ikki marta o'chirish bir marta o'chirish bilan bir xil natija.
Shuning uchun "topilmadimi?" tekshiruvini xato nomiga emas, bir nechta belgiga qarab qiling. Kanon funksiyasi:
export function topilmadimi(e: unknown): boolean {
return e instanceof S3ServiceException && (
e.$metadata.httpStatusCode === 404 || e.name === "NoSuchKey" ||
e.name === "NotFound" || e.name === "NoSuchBucket"
);
}Tekshirib ko'ring: Foydalanuvchi faylini o'chirish marshrutida avval
HeadObjectbilan "bormi?" deb tekshirib, keyinDeleteObjectqilish kerakmi?
Javob
Ombor uchun shart emas: DeleteObject yo'q kalitda ham 204 qaytaradi. Ortiqcha so'rov — ortiqcha vaqt va poyga holati (tekshiruv va o'chirish orasida boshqa so'rov o'chirib qo'yishi mumkin). "Bu fayl bormi va u shu foydalanuvchinikimi?" degan savol esa bazada hal qilinadi.
6. Ro'yxat: prefiks va sahifalash
Kanon to'rtta kichik obyekt qo'shib, ListObjectsV2 ni uch xil chaqirdi:
Prefix vazifalar/7/: [ 'vazifalar/7/a.png', 'vazifalar/7/b.png', 'vazifalar/7/eslatma.txt' ]
Delimiter '/': [ 'avatar/', 'vazifalar/' ] + fayllar: []
MaxKeys 2: 2 IsTruncated true NextContinuationToken bor: trueBirinchi ikki qator — «"Papka" — illyuziya» bo'limidagi JS natijamizning aynan o'zi. Uchinchisi — sahifalash: bitta javobda ko'pi bilan 1 000 kalit (MaxKeys bilan kamroq). Davomi bo'lsa — IsTruncated: true va NextContinuationToken. Keyingi sahifa uchun shu tokenni ContinuationToken sifatida berasiz. Bu kursorli sahifalash ning o'zi.
Kanon modulda barcha kalitlarni yuruvchi async generator:
async *kalitlar(prefiks: string) {
let token: string | undefined;
do {
const l = await this.s3.send(new ListObjectsV2Command({
Bucket: this.bucket, Prefix: prefiks,
...(token ? { ContinuationToken: token } : {}),
}));
for (const o of l.Contents ?? []) {
if (o.Key) yield { kalit: o.Key, hajm: o.Size ?? 0 };
}
token = l.NextContinuationToken;
} while (token);
}Ishlatish: for await (const o of ombor.kalitlar("vaqtincha/")) { … } (Async iteratorlar). Chaqiruvchi sahifalar haqida o'ylamaydi — u shunchaki obyektlarni birin-ketin oladi. Kanon tajriba oxirida shu yo'l bilan bucket'ni tozaladi: o'chirildi: 207 ta obyekt.
7. Katta obyektlar: multipart
Oldingi darsda lib-storage Upload faylni 5 MB lik qismlarga bo'lishini ko'rdik. Kanon 20 MB obyektni shunday yukladi, keyin qismni 1 MB qilib ko'rdi:
ETag: "ad3dbd2a6bd4f8d42a9abc463ce9ca33-4" (oxirida -N: qismlar soni; MD5 EMAS) | progress hodisalari: 4
partSize 1 MB: Error: EntityTooSmall: Your proposed upload part size [1048576] is smaller than the minimum allowed size [5242880] (5MB)- ETag oxirida
-4— 4 ta qism. Bu butun faylning MD5 i emas (qismlar MD5 laridan hisoblanadi). Shuning uchun "ETag = MD5" ga tayanib butunlikni tekshirmang; butunlik uchun kanon o'zi sha256 hisoblaydi (Katta fayllarni stream bilan yuklash). EntityTooSmall— "taklif qilingan qism hajmi 1 048 576 bayt, ruxsat etilgan minimal 5 242 880 (5MB)". Oxirgi qism kichik bo'lishi mumkin, qolganlari — kamida 5 MB.
Nusxalash (CopyObject) — baytlar server orqali o'tmaydi, ombor ichida ko'chiriladi:
copy OK: 2097152020 MB obyekt doimiy/ prefiksiga nusxalandi. Vaqtinchalik yuklashni tasdiqlangandan keyin doimiy joyga ko'chirish naqshi — Fayl metama'lumoti darsida.
Tezlik (lokal Docker, 200 ta kichik obyekt):
200 ta kichik PutObject: mediana 15.18 ms, p95 31.76 ms
200 ta kichik GetObject: mediana 6.20 ms, p95 21.77 msHar amal — alohida HTTP so'rov. Bu lokal o'lchov; bulutda tarmoq har so'rovga o'nlab millisekund qo'shadi. Xulosa: sahifa uchun yuzlab obyektni ketma-ket o'qimang — ro'yxatni bazadan oling.
8. Private va public bucket
8.1 Sukut — yopiq
Kanon bucket'dagi avatar/7.webp ni kalitsiz oddiy fetch bilan ochmoqchi bo'ldi:
sukut: 403
avatar/* ga public-read siyosati: 200 | vazifalar/7/a.png: 403
siyosat o'chirildi: 403Sukutda bucket private: kalitsiz so'rov — 403 Forbidden. Kanon faqat avatar/ prefiksi uchun o'qishni ochdi (bucket policy — bucket siyosati):
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": ["*"] },
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::t04-vazifalar/avatar/*"]
}]
}Principal: *— hamma, kalitsiz ham.Action: s3:GetObject— faqat o'qish (yozish va ro'yxat emas).Resource— faqatavatar/ostidagi obyektlar."Version": "2012-10-17"— siyosat tilining versiyasi (sana emas, doimiy qiymat).
Natija: avatar ochildi (200), vazifa fayli baribir 403. Siyosat o'chirilgach — yana 403.
8.2 Qaysi biri qachon
| Fayl | Kim ko'radi | Bucket |
|---|---|---|
| Avatar, mahsulot rasmi | hamma | public prefiks + CDN |
| Vazifa ilovasi, chek, pasport | faqat egasi | private |
| Hisobotlar, eksport | egasi, muddatli | private + presigned URL |
Private faylni foydalanuvchiga qanday beramiz? Ikki yo'l: server o'zi o'qib uzatadi yoki muddatli presigned URL (oldindan imzolangan havola) beradi. Ikkinchisi — keyingi darsda.
CDN (Content Delivery Network — kontent yetkazish tarmog'i) — dunyo bo'ylab joylashgan kesh serverlari. Public obyekt bir marta CDN'ga tushadi, keyin foydalanuvchiga eng yaqin serverdan beriladi. Avatarlar uchun standart: AWS'da CloudFront, Cloudflare'da o'zining tarmog'i.
Diqqat: Butun bucket'ni public qilmang va
s3:ListBucketni ochmang. Ochiq ro'yxatda hujumchi hamma kalitlarni ko'radi — shaxsiy hujjatlar ham yuklab olinadi. Bunday sizishlar (leak) har yili yangilikka chiqadi.
9. AWS S3, Cloudflare R2 va boshqalar
Kod bir xil, sozlama farq qiladi:
| Ombor | endpoint |
region |
|---|---|---|
| MinIO (lokal) | http://127.0.0.1:9000 |
istalgan, us-east-1 |
| AWS S3 | bermaysiz | bucket hududi |
| Cloudflare R2 | https://<ACCOUNT_ID>.r2.cloudflarestorage.com |
auto |
- Cloudflare R2 hujjatiga ko'ra (2026-10-28 da o'qildi): region
auto; R2 dan tashqariga chiqadigan trafik (egress) uchun to'lov yo'q; bepul hajm — oyiga 10 GB. AWS S3 da esa chiquvchi trafik pullik — ko'p yuklab olinadigan fayllar uchun bu katta farq. - Boshqa S3 mos xizmatlar: Backblaze B2, DigitalOcean Spaces, Google Cloud Storage (XML API orqali).
Diqqat: Kanon faqat lokal MinIO bilan sinadi. AWS S3, Cloudflare R2 va CDN — sinalmagan (hisob yaratilmadi). Narx va limitlar o'zgaradi — tanlashdan oldin rasmiy narx sahifasini o'qing.
10. Hujumchi nigohi
Ombor bilan bog'liq eng ko'p uchraydigan teshiklar:
- Ochiq bucket. "Ishlamayapti" deb butun bucket public qilinadi. Himoya — sukut private, ochiq faqat kerakli prefiks va
GetObject. - Kalit kodda.
S3_SIRGitHub'ga tushsa, botlar uni daqiqalar ichida topadi. Himoya — env,.gitignore, sirlar menejeri. - Kalit brauzerda. Kalitni frontendga berish. Himoya — muddatli presigned URL (Presigned URL'lar).
- Taxmin qilinadigan kalit.
vazifalar/7/chek.pdf— hujumchi8,9ni sinaydi. Himoya — UUID kalit (Fayl yuklash xavfsizligi). - Ortiqcha huquq. Ilova kaliti butun hisobga to'liq ruxsat bilan. Himoya — bitta bucket va kerakli amallar (
PutObject,GetObject,DeleteObject,ListBucket).
11. Ko'p uchraydigan xatolar
| Xato | Sabab | Davosi |
|---|---|---|
| MinIO'ga ulanib bo'lmaydi (bucket nomi host oldiga qo'shiladi) | virtual-hosted manzil | forcePathStyle: true |
InvalidBucketName |
katta harf, _, juda qisqa nom |
nom qoidalari |
BucketAlreadyOwnedByYou (409) |
bucket allaqachon bor | avval HeadBucket |
NotFound: UnknownError |
HeadObject yo'q kalitda |
404 ni topilmadimi bilan ushlang |
EntityTooSmall |
multipart qismi < 5 MB | partSize ≥ 5 MB |
Metama'lumotda ? yoki xato |
ASCII bo'lmagan qiymat | encodeURIComponent |
| ETag MD5 ga teng emas | obyekt multipart bilan yuklangan | o'z sha256 ingiz |
403 kalitsiz fetch da |
bucket private (to'g'ri!) | presigned URL yoki server orqali |
12. Mashqlar
Mashqlar kurs/mashqlar/29/04-s3/ da.
1-mashq (oson): bo'shliqlarni to'ldiring
- S3 da fayllar idishi —
- Obyektning bucket ichidagi to'liq nomi —
- "Papka"larni ko'rsatish uchun
ListObjectsV2parametri — - Bitta so'rovda yuklangan obyektning ETag'i — tananing hash'i.
- Yo'q kalitni o'chirganda status —
Yechim
- bucket — savat, nomi qat'iy qoidalar bilan.
- kalit (key) — oddiy satr,
/bilan ham. - Delimiter —
/gacha guruhlabCommonPrefixesberadi. - MD5 — multipart'da esa
…-N. - 204 — o'chirish idempotent.
2-mashq (o'rta): obyektlarni "papka" bo'yicha sanash
hajmlarHisobi(obyektlar) funksiyasini yozing: { kalit, hajm } massivini olib, har birinchi darajali prefiks (birinchi / gacha, / bilan) uchun jami hajmni qaytarsin. / siz kalit — "(ildiz)" guruhiga. Natijani obyekt sifatida chiqaring. Ma'lumot: avatar/7.webp 120, avatar/8.webp 80, vazifalar/7/a.png 5000, vazifalar/8/c.pdf 3000, readme.txt 10. Ishora: indexOf("/"), oddiy obyekt va for…of yetarli (Obyekt bo'ylab yurish).
Yechim
const obyektlar = [
{ kalit: "avatar/7.webp", hajm: 120 },
{ kalit: "avatar/8.webp", hajm: 80 },
{ kalit: "vazifalar/7/a.png", hajm: 5000 },
{ kalit: "vazifalar/8/c.pdf", hajm: 3000 },
{ kalit: "readme.txt", hajm: 10 },
];
function hajmlarHisobi(royxat) {
const natija = {};
for (const { kalit, hajm } of royxat) {
const i = kalit.indexOf("/");
const guruh = i === -1 ? "(ildiz)" : kalit.slice(0, i + 1);
natija[guruh] = (natija[guruh] ?? 0) + hajm;
}
return natija;
}
console.log(hajmlarHisobi(obyektlar));Konsolda:
{ 'avatar/': 200, 'vazifalar/': 8000, '(ildiz)': 10 }Haqiqiy bucket'da obyektlar kalitlar("") generatoridan keladi va millionlab bo'lishi mumkin — bunday hisobni har safar qilish qimmat. Production'da hajm hisobini bazada yuritasiz (har fayl yozuvida hajm ustuni bor) yoki ombor hisobotlaridan olasiz.
3-mashq (qiyin): sahifalarni yuruvchi generator
Haqiqiy S3 o'rniga sahifaOl(token) soxta funksiyasi berilgan: u kalitlarni 2 tadan qaytaradi va { Contents, NextContinuationToken } beradi. Kanon kalitlar metodiga o'xshash barchaKalitlar(prefiks) async generatorini yozing: barcha sahifalarni yurib, faqat prefiks bilan boshlanadigan kalitlarni yield qilsin. Nechta sahifa so'ralganini ham sanang va chiqaring.
const BARCHASI = ["avatar/7.webp", "vazifalar/7/a.png",
"vazifalar/7/b.png", "vazifalar/8/c.png", "vaqtincha/1.png"];
let soralgan = 0;
async function sahifaOl(token = 0) {
soralgan++;
const bosh = Number(token);
const Contents = BARCHASI.slice(bosh, bosh + 2)
.map((Key) => ({ Key }));
const keyingi = bosh + 2 < BARCHASI.length ? bosh + 2 : undefined;
return {
Contents,
NextContinuationToken: keyingi === undefined
? undefined : String(keyingi),
};
}Yechim
const BARCHASI = ["avatar/7.webp", "vazifalar/7/a.png",
"vazifalar/7/b.png", "vazifalar/8/c.png", "vaqtincha/1.png"];
let soralgan = 0;
async function sahifaOl(token = 0) {
soralgan++;
const bosh = Number(token);
const Contents = BARCHASI.slice(bosh, bosh + 2)
.map((Key) => ({ Key }));
const keyingi = bosh + 2 < BARCHASI.length ? bosh + 2 : undefined;
return {
Contents,
NextContinuationToken: keyingi === undefined
? undefined : String(keyingi),
};
}
async function* barchaKalitlar(prefiks) {
let token;
do {
const sahifa = await sahifaOl(token);
for (const o of sahifa.Contents ?? []) {
if (o.Key.startsWith(prefiks)) yield o.Key;
}
token = sahifa.NextContinuationToken;
} while (token);
}
for await (const k of barchaKalitlar("vazifalar/")) console.log(k);
console.log("so'ralgan sahifalar:", soralgan);Konsolda:
vazifalar/7/a.png
vazifalar/7/b.png
vazifalar/8/c.png
so'ralgan sahifalar: 35 ta kalit, sahifada 2 tadan — 3 ta so'rov. Haqiqiy S3 da filtrni klientda qilmang: Prefix parametrini bering, ombor o'zi faqat keraklilarini qaytaradi va so'rovlar kamayadi. Bu yerda filtr klientda — soxta funksiya Prefix ni bilmaydi. token boshida undefined: birinchi so'rov tokensiz ketadi, xuddi S3 dagidek.
13. Loyiha qadami: s3.ts — FaylOmbori klassi
Kanon omborni bitta klassga o'radi: modullar/src/fayl/s3.ts. Ilovaning qolgan qismi SDK buyruqlarini emas, FaylOmbori metodlarini ko'radi — omborni almashtirish yoki testda soxtasini qo'yish oson. Versiyalar: @aws-sdk/client-s3, lib-storage, s3-request-presigner, s3-presigned-post — hammasi 3.1153.0.
Telefon uchun qatorlarni bo'ldik, ma'no kanondagidek. Avval metodlar jadvalini ko'zdan kechiring (koddan keyin), keyin kodni o'qing. Oxirgi ikki metod va contentDisposition — keyingi darslar mavzusi:
// s3.ts — S3 mos ombor (MinIO lokal, AWS S3 / Cloudflare R2 prodda)
// ustidagi yupqa qatlam. Bucket private: o'qish/yozish faqat server
// kaliti yoki muddatli imzolangan havola (presigned) bilan.
import { Readable } from "node:stream";
import {
S3Client, CreateBucketCommand, HeadBucketCommand, GetObjectCommand,
HeadObjectCommand, DeleteObjectCommand, CopyObjectCommand,
ListObjectsV2Command, S3ServiceException,
} from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import {
createPresignedPost, type PresignedPost,
} from "@aws-sdk/s3-presigned-post";
export type S3Sozlama = {
endpoint?: string; region: string; kalit: string; sir: string;
bucket: string; pathStyle?: boolean;
};
export type ObyektMa = {
hajm: number; tur: string | undefined; etag: string | undefined;
meta: Record<string, string>;
};
export class FaylOmbori {
readonly s3: S3Client;
readonly bucket: string;
constructor(s: S3Sozlama) {
this.bucket = s.bucket;
this.s3 = new S3Client({
region: s.region,
...(s.endpoint === undefined ? {} : { endpoint: s.endpoint }),
// MinIO: http://host:port/bucket/kalit
forcePathStyle: s.pathStyle ?? s.endpoint !== undefined,
credentials: { accessKeyId: s.kalit, secretAccessKey: s.sir },
});
}
/** Bucket bo'lmasa yaratadi (lokal/test uchun; prodda bucket
* infratuzilma kodi bilan yaratiladi). */
async tayyorla(): Promise<void> {
try {
await this.s3.send(
new HeadBucketCommand({ Bucket: this.bucket }),
);
} catch (e) {
if (!topilmadimi(e)) throw e;
await this.s3.send(
new CreateBucketCommand({ Bucket: this.bucket }),
);
}
}
/** Oqimni (yoki buferni) yuklaydi. Hajm noma'lum oqim uchun
* multipart (`lib-storage`), `signal` bilan bekor qilinadi. */
async yoz(
kalit: string,
tana: Readable | Uint8Array | string,
tur: string,
opts: {
meta?: Record<string, string>;
signal?: AbortSignal;
progress?: (yuklandi: number) => void;
} = {},
): Promise<{ etag: string | undefined }> {
const up = new Upload({
client: this.s3,
params: {
Bucket: this.bucket, Key: kalit, Body: tana, ContentType: tur,
...(opts.meta ? { Metadata: opts.meta } : {}),
},
partSize: 5 * 1024 * 1024,
queueSize: 2,
// xatoda AbortMultipartUpload — chala qismlar qolmaydi
leavePartsOnError: false,
...(opts.signal
? { abortController: abortControllerDan(opts.signal) }
: {}),
});
if (opts.progress) {
const p = opts.progress;
up.on("httpUploadProgress", (h) => { p(h.loaded ?? 0); });
}
const r = await up.done();
return { etag: r.ETag };
}
async oqi(kalit: string, oraliq?: string): Promise<{
oqim: Readable; hajm: number | undefined;
tur: string | undefined; oraliq: string | undefined;
}> {
const r = await this.s3.send(new GetObjectCommand({
Bucket: this.bucket, Key: kalit,
...(oraliq ? { Range: oraliq } : {}),
}));
if (!(r.Body instanceof Readable)) {
throw new Error("S3 javobi Node oqimi emas");
}
return {
oqim: r.Body, hajm: r.ContentLength, tur: r.ContentType,
oraliq: r.ContentRange,
};
}
/** Obyekt ma'lumoti yoki `undefined` (yo'q bo'lsa). */
async ma(kalit: string): Promise<ObyektMa | undefined> {
try {
const h = await this.s3.send(
new HeadObjectCommand({ Bucket: this.bucket, Key: kalit }),
);
return {
hajm: h.ContentLength ?? 0, tur: h.ContentType, etag: h.ETag,
meta: h.Metadata ?? {},
};
} catch (e) {
if (topilmadimi(e)) return undefined;
throw e;
}
}
/** S3 da DeleteObject idempotent — yo'q kalit ham 204. */
async ochir(kalit: string): Promise<void> {
await this.s3.send(
new DeleteObjectCommand({ Bucket: this.bucket, Key: kalit }),
);
}
async kochir(manba: string, nishon: string): Promise<void> {
await this.s3.send(new CopyObjectCommand({
Bucket: this.bucket,
CopySource: `${this.bucket}/${encodeURI(manba)}`,
Key: nishon,
}));
}
async *kalitlar(prefiks: string): AsyncGenerator<{
kalit: string; hajm: number; vaqt: Date | undefined;
}> {
let token: string | undefined;
do {
const l = await this.s3.send(new ListObjectsV2Command({
Bucket: this.bucket, Prefix: prefiks,
...(token ? { ContinuationToken: token } : {}),
}));
for (const o of l.Contents ?? []) {
if (o.Key) {
yield {
kalit: o.Key, hajm: o.Size ?? 0, vaqt: o.LastModified,
};
}
}
token = l.NextContinuationToken;
} while (token);
}
/** Brauzer to'g'ridan bucket'ga yuklashi uchun presigned POST
* (keyingi darsda). */
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 (keyingi darsda). */
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 },
);
}
}
/** `attachment; filename="ascii"; filename*=UTF-8''foiz-kodlangan` —
* eski brauzerlar ASCII zaxirasini oladi. */
export function contentDisposition(
nom: string, tur: "attachment" | "inline" = "attachment",
): string {
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}`;
}
export function topilmadimi(e: unknown): boolean {
return e instanceof S3ServiceException && (
e.$metadata.httpStatusCode === 404 || e.name === "NoSuchKey" ||
e.name === "NotFound" || e.name === "NoSuchBucket"
);
}
function abortControllerDan(signal: AbortSignal): AbortController {
const ac = new AbortController();
if (signal.aborted) ac.abort(signal.reason);
else {
signal.addEventListener(
"abort", () => { ac.abort(signal.reason); }, { once: true },
);
}
return ac;
}| Metod | S3 buyrug'i | Nima uchun |
|---|---|---|
tayyorla() |
HeadBucket → CreateBucket |
lokal/test bucket |
yoz(kalit, tana, tur) |
Upload (multipart) |
oqim yoki bufer |
oqi(kalit, oraliq?) |
GetObject |
yuklab berish, Range |
ma(kalit) |
HeadObject |
bormi, hajmi, turi |
ochir, kochir |
DeleteObject, CopyObject |
tozalash, ko'chirish |
kalitlar(prefiks) |
ListObjectsV2 + token |
yetimlarni topish |
Kanon modullar paketi npm run check dan o'tgan: tsc 0, eslint 0, testlar 82/82 (MinIO bilan). Biz Docker'ni yoqmadik: contentDisposition testi o'tdi, MinIO talab qiladigan to'plam S3_ENDPOINT yo'q sababi bilan o'tkazildi.
Commit xabari (modul uchun):
git commit -m "feat(fayl): FaylOmbori — S3 mos ombor (MinIO, R2, AWS)"vazifalar-api ga ulanishi 28-qism tugagach: ilova ishga tushganda bitta FaylOmbori yaratiladi, S3_* o'zgaruvchilari env validatsiyasiga (Env validatsiyasi) qo'shiladi.
14. Real ishda
- Deyarli har zamonaviy ilova rasmlar, hujjatlar, zaxira nusxalar va loglarni S3 yoki uning muqobilida saqlaydi. S3 API — sohaning umumiy tili.
- Narx tanlovi: saqlash, so'rovlar va chiquvchi trafik. Ko'p yuklab olinadigan ilovada trafik eng katta xarajat bo'lishi mumkin.
- Intervyuda: "Fayllarni qayerda saqlaysiz va nega serverda emas?", "Private faylni foydalanuvchiga qanday berasiz?", "ETag nima?"
Xulosa
- Object storage: bucket → kalit → obyekt (baytlar + metama'lumot); papkalar yo'q, faqat prefiks.
- S3 API — standart; Node'da
@aws-sdk/client-s3v3:new XCommand({...})+send. - Bitta so'rovli obyekt ETag'i = MD5, multipart'da
…-N; minimal qism 5 MB. - Yo'q kalit: Get —
NoSuchKey, Head —NotFound: UnknownError, Delete — 204 (idempotent). - Bucket sukutda private; ochiq faqat kerakli prefiks va
GetObject. - MinIO community repozitoriyasi 2026-04 da arxivlangan, Docker Hub image'i endi yo'q — kod S3 API'ga yozilgani uchun boshqa omborga o'tish — sozlama o'zgarishi.
Keyingi dars: Presigned URL'lar — brauzer faylni serverni chetlab to'g'ridan omborga yuklaydi va yuklab oladi, server esa faqat muddatli ruxsat beradi.
Manbalar
- Amazon S3 User Guide — bucket nomi qoidalari: https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html
- Amazon S3 — obyekt metama'lumoti (2 KB): https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingMetadata.html
- AWS SDK for JavaScript v3 — S3 klienti: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/
- AWS SDK v2 qo'llab-quvvatlash tugashi: https://aws.amazon.com/blogs/developer/announcing-end-of-support-for-aws-sdk-for-javascript-v2/
- MinIO repozitoriyasi (README: "no longer maintained", source only; 2026-04-25 da arxivlangan): https://github.com/minio/minio — 2026-10-28 da o'qildi
- Docker Hub API (
hub.docker.com/v2/namespaces/minio/repositories/minio/) — 2026-10-28 da 404; IBM Support, "MinIO Image Removal from Quay Registry" (2026-09-25): https://www.ibm.com/support/pages/node/7289585 - Cloudflare R2 — S3 API (endpoint, region
auto): https://developers.cloudflare.com/r2/api/s3/api/; narxlar (egress yo'q, 10 GB bepul): https://developers.cloudflare.com/r2/pricing/ — 2026-10-28 da o'qildi, sinalmagan - Kanon tajribasi (IlmHamroh, 2026-10-27): MinIO RELEASE.2025-09-07 (Docker, lokal), @aws-sdk 3.1153.0, Node 24.21.0. MD5 hisobi va mashq yechimlari — biz ishga tushirdik (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!