Amazon S3 — fayl saqlash, upload, delete, signed URL

~25 daq · Express · Fayl va Media

Muammodan boshlaylik

Fayllarni serverda uploads/ papkasiga saqlayapsiz. Hozir ishlaydi. Lekin:

  • Server o'chirib yoqilsa — fayllar o'chadi (agar persistent storage yo'q bo'lsa)
  • Ikki server ishlatilsa (load balancing) — biri yuklagan fayl ikkinchisida yo'q
  • Disk to'lsa — yangi fayllar yuklanmaydi
  • Foydalanuvchi Toshkentda, server Germaniyadа — rasm yuklash sekin

Amazon S3 (Simple Storage Service) bu muammolarni hal qiladi: cheksiz joy, global tarqatish, 99.999999999% ishonchlilik.


S3 nima?

S3 — Amazon ning bulut fayl saqlash xizmati. Fayllar bucket (chelak) da saqlanadi. Har bir fayl — object (obyekt), uning key (nomi/yo'li) bor.

Hayotiy o'xshatma

Tasavvur qiling: ulkan pochta ombori. Omborning o'zi — S3. Har bir yachiyka — bucket. Yachiykaдаги har bir paket — object. Paketning manzili — key.

S3
├── bucket: mening-loyiham-rasmlar
│   ├── profil/user-123.webp       ← key
│   ├── profil/user-456.webp
│   └── mahsulot/iphone-front.webp
└── bucket: mening-loyiham-hujjatlar
    ├── shartnomalar/2024-001.pdf
    └── hisobotlar/yanvar.xlsx

O'rnatish va sozlash

npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
# .env
AWS_REGION=eu-central-1
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
AWS_S3_BUCKET=mening-loyiham-rasmlar
// config/s3.js
const { S3Client } = require('@aws-sdk/client-s3');

const s3 = new S3Client({
  region: process.env.AWS_REGION,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  },
});

module.exports = s3;

Fayl yuklash (Upload)

const { PutObjectCommand } = require('@aws-sdk/client-s3');
const s3 = require('./config/s3');
const multer = require('multer');
const sharp = require('sharp');
const { v4: uuidv4 } = require('uuid'); // npm install uuid

const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 10 * 1024 * 1024 },
  fileFilter: (req, file, cb) => {
    const allowed = ['image/jpeg', 'image/png', 'image/webp'];
    allowed.includes(file.mimetype) ? cb(null, true) : cb(new Error('Faqat rasm'));
  }
});

app.post('/profil/rasm', upload.single('rasm'), async (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'Rasm yuborilmadi' });
  }

  // Sharp bilan qayta ishlash
  const buffer = await sharp(req.file.buffer)
    .resize(300, 300, { fit: 'cover' })
    .webp({ quality: 85 })
    .toBuffer();                              // ← faylga emas, bufferga

  const key = `profil/${uuidv4()}.webp`;     // ← noyob yo'l

  // S3 ga yuklash
  await s3.send(new PutObjectCommand({
    Bucket: process.env.AWS_S3_BUCKET,        // ← bucket nomi
    Key: key,                                  // ← fayl yo'li
    Body: buffer,                              // ← fayl ma'lumoti
    ContentType: 'image/webp',                 // ← fayl turi
  }));

  const url = `https://${process.env.AWS_S3_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`;

  res.json({ message: 'Yuklandi', url, key });
});

key da papka tuzilishi nima uchun? profil/, mahsulot/ kabi prefix lar S3 da "papka" tashkil qiladi. Keyin faqat profil rasmlarini ko'rish, narx hisoblash yoki o'chirish oson bo'ladi.


Fayl o'chirish (Delete)

const { DeleteObjectCommand } = require('@aws-sdk/client-s3');

app.delete('/profil/rasm', async (req, res) => {
  const { key } = req.body; // ← masalan: "profil/abc-123.webp"

  await s3.send(new DeleteObjectCommand({
    Bucket: process.env.AWS_S3_BUCKET,
    Key: key,
  }));

  res.json({ message: 'O\'chirildi' });
});

Fayl o'qish (Get)

Agar bucket public bo'lsa — to'g'ridan-to'g'ri URL orqali kirish mumkin. Lekin ko'plab hollarda bucket private bo'lishi kerak — maxfiy hujjatlar, faqat o'z foydalanuvchisiga ko'rinadigan fayllar.

Private bucket — Signed URL

Signed URL — vaqtincha amal qiladigan maxsus havola. Foydalanuvchi bu havola orqali S3 ga to'g'ridan-to'g'RI kirishi mumkin — serveringizdan o'tmasdan:

const { GetObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');

app.get('/fayl/:key', async (req, res) => {
  const command = new GetObjectCommand({
    Bucket: process.env.AWS_S3_BUCKET,
    Key: req.params.key,
  });

  // 1 soat amal qiladigan URL
  const signedUrl = await getSignedUrl(s3, command, { expiresIn: 3600 });
  //                                                              ↑ soniyada

  res.json({ url: signedUrl });
});

Signed URL qanday ko'rinadi:

https://bucket.s3.eu-central-1.amazonaws.com/hujjat/shartnoma.pdf
  ?X-Amz-Algorithm=AWS4-HMAC-SHA256
  &X-Amz-Credential=AKIAIOSFODNN7...
  &X-Amz-Date=20240115T120000Z
  &X-Amz-Expires=3600
  &X-Amz-Signature=abc123...

Nima uchun Signed URL? Har bir fayl so'rovini serveringiz orqali o'tkazsangiz — server banda bo'ladi, katta fayllar uchun internet chiqimi to'laysiz. Signed URL bilan foydalanuvchi S3 dan to'g'ridan-to'g'RI yuklab oladi — server zimmasidan tushadi.


Upload uchun Signed URL (frontend dan to'g'ridan-to'g'RI S3 ga)

Ba'zan frontend S3 ga to'g'ridan-to'g'RI yuklashi kerak — server orqali o'tmasdan. Masalan, 100 MB video fayllar:

const { PutObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');

// Frontend bu URL ni so'raydi, keyin to'g'ridan-to'g'RI S3 ga yuklaydi
app.get('/upload-url', async (req, res) => {
  const { contentType } = req.query; // ← masalan: "image/webp"
  const key = `uploads/${uuidv4()}`;

  const command = new PutObjectCommand({
    Bucket: process.env.AWS_S3_BUCKET,
    Key: key,
    ContentType: contentType,
  });

  const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 300 }); // ← 5 daqiqa

  res.json({ uploadUrl, key });
});

Frontend da:

// 1. Server dan upload URL olish
const { uploadUrl, key } = await fetch('/upload-url?contentType=image/webp')
  .then(r => r.json());

// 2. To'g'ridan-to'g'RI S3 ga yuklash (server orqali o'tmaydi)
await fetch(uploadUrl, {
  method: 'PUT',
  body: file,                          // ← File obyekti
  headers: { 'Content-Type': 'image/webp' },
});

// 3. Serverga faqat key ni xabar berish
await fetch('/profil/rasm-saqlash', {
  method: 'POST',
  body: JSON.stringify({ key }),
  headers: { 'Content-Type': 'application/json' },
});

To'liq yordamchi modul

Kod takrorlanishini kamaytirish uchun alohida modul:

// utils/s3-helpers.js
const { PutObjectCommand, DeleteObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const s3 = require('../config/s3');

const BUCKET = process.env.AWS_S3_BUCKET;
const REGION = process.env.AWS_REGION;

const uploadToS3 = async (buffer, key, contentType) => {
  await s3.send(new PutObjectCommand({
    Bucket: BUCKET,
    Key: key,
    Body: buffer,
    ContentType: contentType,
  }));
  return `https://${BUCKET}.s3.${REGION}.amazonaws.com/${key}`;
};

const deleteFromS3 = async (key) => {
  await s3.send(new DeleteObjectCommand({ Bucket: BUCKET, Key: key }));
};

const getSignedDownloadUrl = async (key, expiresIn = 3600) => {
  const command = new GetObjectCommand({ Bucket: BUCKET, Key: key });
  return getSignedUrl(s3, command, { expiresIn });
};

module.exports = { uploadToS3, deleteFromS3, getSignedDownloadUrl };

Ishlatish:

const { uploadToS3, deleteFromS3 } = require('./utils/s3-helpers');

app.post('/rasm', upload.single('rasm'), async (req, res) => {
  const buffer = await sharp(req.file.buffer).resize(800).webp({ quality: 85 }).toBuffer();
  const key = `rasmlar/${uuidv4()}.webp`;
  const url = await uploadToS3(buffer, key, 'image/webp');
  res.json({ url, key });
});

app.delete('/rasm/:key', async (req, res) => {
  await deleteFromS3(req.params.key);
  res.json({ message: 'O\'chirildi' });
});

Tez-tez uchraydigan xatolar

1. AWS kalitlarini kodga yozish

// ❌
const s3 = new S3Client({
  credentials: {
    accessKeyId: 'AKIAIOSFODNN7EXAMPLE',           // ← GitHub da ochiq!
    secretAccessKey: 'wJalrXUtnFEMI/K7MDENG...',
  }
});

Sababi: GitHub da yuzlab bot repository larni skanerlaydi — kalitlar topilsa darhol AWS hisobingizni ishlatib, ming dollarlik to'lov chiqishi mumkin. Har doim .env dan oling.

2. Bucket ni to'liq public qilish

// ❌ — hamma fayllar internetga ochiq
// Bucket Policy da: "Effect": "Allow", "Principal": "*"

Sababi: maxfiy hujjatlar, foydalanuvchi ma'lumotlari — hammasi ochiq bo'lib qoladi. Private bucket + Signed URL ishlatish kerak.

3. Key da bo'shliq yoki maxsus belgilar

// ❌
const key = `profil/${req.body.username}.jpg`; // ← "Ali Valiyev.jpg"

Sababi: bo'shliq va ba'zi belgilar URL da muammo chiqaradi. uuid yoki Date.now() + random son ishlatgan ma'qul.

4. S3 xatolarini ushlash

// ❌ — xato ushlashsiz
await s3.send(new PutObjectCommand({ ... }));
// ✅
try {
  await s3.send(new PutObjectCommand({ ... }));
} catch (err) {
  if (err.name === 'NoSuchBucket') {
    return res.status(500).json({ error: 'Bucket topilmadi' });
  }
  throw err;
}

Sababi: tarmoq uzilishi, noto'g'ri bucket nomi, ruxsat yo'qligi — bular alohida xato turlari. Ushlanmasa, foydalanuvchi serverda "500 Internal Error" ko'radi, sabab noma'lum.


Xulosa

  • S3 — cheksiz, ishonchli, global bulut fayl saqlash
  • PutObjectCommand — yuklash, DeleteObjectCommand — o'chirish
  • Signed URL — private fayllar uchun vaqtincha havola
  • Frontend dan to'g'ridan-to'g'RI S3 ga yuklash — server yukini kamaytiradi
  • Kalitlarni hech qachon kodga yozmang — .env dan oling

Keyingi dars: 35-dars: Streaming — katta fayllarni stream qilish