Amazon S3 / Cloud File Storage
Muammodan boshlaylik
Oldingi darsda fayllarni serverga saqladik. Produksiyada bu muammo:
- Server qayta ishga tushsa —
uploads/papkasi o'chishi mumkin - Ikki server (load balancer) bo'lsa — fayl birinchisida, so'rov ikkinchisiga tushsa — topilmaydi
- Server diskin kattalashtirish qimmat va qiyin
- CDN orqali tez yetkazib berish yo'q
S3 — Amazon ning bulut fayl xizmati. Fayllar serverdan tashqarida, alohida tizimda saqlanadi. Cheksiz hajm, yuqori ishonchlilik, CDN bilan integratsiya.
Loyiha tuzilishi
src/
└── upload/
├── upload.module.ts
├── upload.controller.ts
├── upload.service.ts
├── multer.config.ts ← lokal saqlash (dev uchun)
└── s3.service.ts ← S3 bilan ishlash (prod uchun)
S3 ni alohida service da ushlab, upload.service.ts unga murojaat qiladi. Agar S3 o'rniga boshqa xizmat (Cloudinary, Supabase Storage) ishlatilsa — faqat s3.service.ts almashtiriladi.
AWS sozlash
IAM foydalanuvchi va bucket yaratish
- aws.amazon.com → S3 → Create bucket
- Bucket nomi:
myapp-uploads(global unikal bo'lishi kerak) - Region:
ap-southeast-1(Singapore — O'zbekistonga yaqin) - Block all public access — hozircha yoqiq qolsin (Signed URL ishlatamiz)
IAM foydalanuvchi:
- IAM → Users → Create user
- Policy:
AmazonS3FullAccess(yoki faqat kerakli bucket uchun maxsus policy) - Access Keys → Create →
ACCESS_KEY_IDvaSECRET_ACCESS_KEYni saqlang
# .env
AWS_REGION=ap-southeast-1
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI...
AWS_S3_BUCKET=myapp-uploads
O'rnatish
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
npm install multer @types/multer
@aws-sdk/client-s3 — AWS SDK v3 (modular, yengil).
@aws-sdk/s3-request-presigner — Signed URL yaratish uchun.
S3 Service
// src/upload/s3.service.ts
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
S3Client,
PutObjectCommand,
DeleteObjectCommand,
GetObjectCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { v4 as uuid } from 'uuid';
import { extname } from 'path';
@Injectable()
export class S3Service {
private s3: S3Client;
private bucket: string;
constructor(private config: ConfigService) {
this.s3 = new S3Client({
region: this.config.get('AWS_REGION'),
credentials: {
accessKeyId: this.config.get('AWS_ACCESS_KEY_ID'),
secretAccessKey: this.config.get('AWS_SECRET_ACCESS_KEY'),
},
});
this.bucket = this.config.get('AWS_S3_BUCKET');
}
// Fayl yuklash
async upload(
file: Express.Multer.File,
folder: string = 'general',
): Promise<{ key: string; url: string }> {
const ext = extname(file.originalname).toLowerCase();
const key = `${folder}/${uuid()}${ext}`; // ← S3 dagi "yo'l"
try {
await this.s3.send(
new PutObjectCommand({
Bucket: this.bucket,
Key: key,
Body: file.buffer, // ← fayl buffer (memoryStorage kerak)
ContentType: file.mimetype,
// Ommaviy bo'lmasin — signed URL orqali ko'rsatamiz
}),
);
} catch (err) {
throw new InternalServerErrorException('Fayl yuklashda xato: ' + err.message);
}
return {
key,
url: `https://${this.bucket}.s3.${this.config.get('AWS_REGION')}.amazonaws.com/${key}`,
};
}
// Faylni o'chirish
async delete(key: string): Promise<void> {
try {
await this.s3.send(
new DeleteObjectCommand({ Bucket: this.bucket, Key: key }),
);
} catch (err) {
// O'chirmasa ham davom — log qilib ketish
console.error('S3 delete xato:', err.message);
}
}
// Vaqtinchalik URL yaratish (private fayllar uchun)
async getSignedUrl(key: string, expiresIn = 3600): Promise<string> {
const command = new GetObjectCommand({ Bucket: this.bucket, Key: key });
return getSignedUrl(this.s3, command, { expiresIn }); // ← 1 soat
}
}
file.buffer nima uchun? S3 ga yuklashda fayl diskga yozilmaydi — buffer (xotira) dan to'g'ridan-to'g'ri S3 ga yuboriladi. Buning uchun Multer memoryStorage ishlatishi kerak.
Upload Service — S3 bilan
// src/upload/upload.service.ts
import { Injectable, BadRequestException } from '@nestjs/common';
import { S3Service } from './s3.service';
import { extname } from 'path';
const IMAGE_TYPES = ['.jpg', '.jpeg', '.png', '.webp'];
const DOC_TYPES = ['.pdf', '.doc', '.docx'];
@Injectable()
export class UploadService {
constructor(private s3Service: S3Service) {}
async uploadAvatar(file: Express.Multer.File) {
this.validateFile(file, IMAGE_TYPES, 2); // 2MB limit
return this.s3Service.upload(file, 'avatars');
}
async uploadDocument(file: Express.Multer.File) {
this.validateFile(file, DOC_TYPES, 10); // 10MB limit
return this.s3Service.upload(file, 'documents');
}
async deleteFile(key: string) {
return this.s3Service.delete(key);
}
async getSignedUrl(key: string) {
return this.s3Service.getSignedUrl(key, 3600); // 1 soat
}
private validateFile(
file: Express.Multer.File,
allowedTypes: string[],
maxMb: number,
) {
if (!file) throw new BadRequestException('Fayl yuklanmadi');
const ext = extname(file.originalname).toLowerCase();
if (!allowedTypes.includes(ext)) {
throw new BadRequestException(
`Faqat ${allowedTypes.join(', ')} ruxsat`
);
}
const maxBytes = maxMb * 1024 * 1024;
if (file.size > maxBytes) {
throw new BadRequestException(`Maksimal hajm: ${maxMb}MB`);
}
}
}
Multer — memoryStorage (S3 uchun)
// src/upload/multer.config.ts
import { memoryStorage } from 'multer';
// S3 uchun — diskka yozmasdan xotirada ushlab turadi
export const s3UploadConfig = {
storage: memoryStorage(), // ← disk emas, xotira
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB (eng katta limit)
// Kichikroq limit service da tekshiriladi
};
Controller
// src/upload/upload.controller.ts
import {
Controller, Post, Delete, Get,
Param, UseInterceptors, UploadedFile,
UseGuards,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { UploadService } from './upload.service';
import { s3UploadConfig } from './multer.config';
@Controller('upload')
@UseGuards(JwtAuthGuard)
export class UploadController {
constructor(private uploadService: UploadService) {}
// POST /upload/avatar
@Post('avatar')
@UseInterceptors(FileInterceptor('file', s3UploadConfig))
uploadAvatar(@UploadedFile() file: Express.Multer.File) {
return this.uploadService.uploadAvatar(file);
// → { key: "avatars/uuid.jpg", url: "https://..." }
}
// POST /upload/document
@Post('document')
@UseInterceptors(FileInterceptor('file', s3UploadConfig))
uploadDocument(@UploadedFile() file: Express.Multer.File) {
return this.uploadService.uploadDocument(file);
}
// DELETE /upload/:key — fayl o'chirish
@Delete(':key')
deleteFile(@Param('key') key: string) {
return this.uploadService.deleteFile(decodeURIComponent(key));
}
// GET /upload/signed/:key — vaqtinchalik URL
@Get('signed/:key')
getSignedUrl(@Param('key') key: string) {
return this.uploadService.getSignedUrl(decodeURIComponent(key));
}
}
Upload Module
// src/upload/upload.module.ts
import { Module } from '@nestjs/common';
import { UploadController } from './upload.controller';
import { UploadService } from './upload.service';
import { S3Service } from './s3.service';
@Module({
controllers: [UploadController],
providers: [UploadService, S3Service],
exports: [UploadService, S3Service], // boshqa modullar uchun
})
export class UploadModule {}
Public vs Private fayllar
Public fayl (avatar, banner):
→ bucket da public access yoqiladi
→ to'g'ridan-to'g'ri URL ishlaydi
→ https://bucket.s3.region.amazonaws.com/avatars/uuid.jpg
Private fayl (hujjat, shartnoma):
→ bucket private qoladi
→ Signed URL — vaqtinchalik, tokenli URL
→ https://bucket.s3.region.amazonaws.com/docs/uuid.pdf?X-Amz-Signature=...&X-Amz-Expires=3600
→ 1 soatdan keyin ishlamaydi
// Public avatar — to'g'ridan-to'g'ri URL
const avatarUrl = result.url; // https://...s3.../avatars/uuid.jpg
// Private hujjat — har ko'rishda signed URL
const signedUrl = await this.s3Service.getSignedUrl(doc.key, 300); // 5 daqiqa
User da S3 key saqlash
// user.schema.ts
@Schema({ timestamps: true })
export class User {
@Prop()
name: string;
// S3 key — to'liq URL emas, faqat kalit
// Chunki bucket nomi o'zgarishi mumkin
@Prop({ default: null })
avatarKey: string | null; // "avatars/uuid.jpg"
@Prop({ default: null })
avatarUrl: string | null; // "https://...amazonaws.com/avatars/uuid.jpg"
}
Nima uchun key alohida? Bucket nomi yoki region o'zgarsa — faqat URL qayta hisoblanadi, key bazada o'zgarmaydi.
Tez-tez uchraydigan xatolar
memoryStorage bilan xotira to'lib ketishi
// ❌ Katta fayllar xotirani to'ldiradi
storage: memoryStorage(),
limits: { fileSize: 100 * 1024 * 1024 } // 100MB — xavfli
// ✅ S3 uchun ham kichik limit
limits: { fileSize: 10 * 1024 * 1024 } // 10MB maksimum
// Katta fayllar uchun → Multipart Upload (alohida mavzu)
AWS credentials .env da emas, kodda
// ❌ Hech qachon
const s3 = new S3Client({
credentials: {
accessKeyId: 'AKIA...', // ← kodda qolsa git da ko'rinadi
secretAccessKey: 'wJalr...',
},
});
// ✅ ConfigService orqali
accessKeyId: this.config.get('AWS_ACCESS_KEY_ID'),
Eski faylni o'chirmaslik
// ❌ Avatar yangilananda eski fayl S3 da qolaveradi — pul ketadi
await this.s3Service.upload(newFile, 'avatars');
// ✅ Avval eski faylni o'chirish
if (user.avatarKey) {
await this.s3Service.delete(user.avatarKey);
}
const result = await this.s3Service.upload(newFile, 'avatars');
Xulosa
S3 — serverdan mustaqil, cheksiz hajmli fayl saqlash. Lokal disk o'rniga fayllar AWS da saqlanadi — server qayta ishga tushsa ham fayllar saqlanadi.
Eslab qoling:
memoryStorage— S3 uchun (disk emas)key— S3 dagi fayl yo'li (avatars/uuid.jpg)url— to'liq manzil (https://bucket.s3.region.amazonaws.com/key)- Public fayl → to'g'ridan-to'g'ri URL
- Private fayl →
getSignedUrl(vaqtinchalik tokenli URL) - Bazada
keysaqlash — URL emas (bucket o'zgarishi mumkin) - Avatar yangilananda eski faylni S3 dan o'chirish
Keyingi dars
32-dars: Rate Limiting va Throttling