File Upload va Static Files

~20 daq · NestJS · Fayllar va Xavfsizlik

Muammodan boshlaylik

Foydalanuvchi profil rasmi yukladi. Fayl qayerga saqlanadi? Qanday formatlar ruxsat? Hajmi chegaralanganmi? Xaker .php yoki .exe fayl yuklasa nima bo'ladi?

Multer — bu savollarning barchasiga NestJS ichida javob beradi.


Loyiha tuzilishi

src/
├── app.module.ts
├── main.ts
│
├── upload/
│   ├── upload.module.ts
│   ├── upload.controller.ts
│   ├── upload.service.ts
│   └── multer.config.ts      ← Multer sozlamalari
│
└── users/
    ├── users.controller.ts   ← avatar yuklash endpoint
    └── ...

uploads/                      ← yuklangan fayllar (gitignore da)
  avatars/
  documents/
  temp/

O'rnatish

npm install @nestjs/platform-express multer
npm install -D @types/multer

@nestjs/platform-express — NestJS ning Express adapteri, Multer ichida keladi.


Multer konfiguratsiya

// src/upload/multer.config.ts
import { diskStorage } from 'multer';
import { extname } from 'path';
import { BadRequestException } from '@nestjs/common';
import { v4 as uuid } from 'uuid'; // npm install uuid @types/uuid

// Ruxsat etilgan rasm formatlari
const IMAGE_TYPES = ['.jpg', '.jpeg', '.png', '.webp', '.gif'];
const DOC_TYPES   = ['.pdf', '.doc', '.docx', '.xls', '.xlsx'];

// Umumiy diskka saqlash factory
export function diskStorageConfig(destination: string) {
  return diskStorage({
    destination: `./uploads/${destination}`, // papka
    filename: (req, file, cb) => {
      // Asl nom o'rniga uuid — noyob, xavfsiz
      const ext = extname(file.originalname).toLowerCase();
      cb(null, `${uuid()}${ext}`);
    },
  });
}

// Fayl turini tekshirish factory
export function fileFilter(allowedTypes: string[]) {
  return (req: any, file: Express.Multer.File, cb: any) => {
    const ext = extname(file.originalname).toLowerCase();
    if (!allowedTypes.includes(ext)) {
      return cb(
        new BadRequestException(
          `Faqat ${allowedTypes.join(', ')} formatlar ruxsat`
        ),
        false,
      );
    }
    cb(null, true);
  };
}

// Tayyor konfiguratsiyalar
export const avatarUploadConfig = {
  storage: diskStorageConfig('avatars'),
  fileFilter: fileFilter(IMAGE_TYPES),
  limits: { fileSize: 2 * 1024 * 1024 }, // 2MB
};

export const documentUploadConfig = {
  storage: diskStorageConfig('documents'),
  fileFilter: fileFilter(DOC_TYPES),
  limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
};

Upload Module

// src/upload/upload.module.ts
import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { UploadController } from './upload.controller';
import { UploadService } from './upload.service';

@Module({
  controllers: [UploadController],
  providers: [UploadService],
  exports: [UploadService],
})
export class UploadModule {}

Upload Service — fayl boshqarish

// src/upload/upload.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { unlink, access } from 'fs/promises';
import { join } from 'path';

@Injectable()
export class UploadService {
  // Fayl URL sini tuzish
  getFileUrl(filename: string, folder: string): string {
    return `/uploads/${folder}/${filename}`;
  }

  // Eski faylni o'chirish (avatar yangilananda)
  async deleteFile(filepath: string): Promise<void> {
    const fullPath = join(process.cwd(), filepath);
    try {
      await access(fullPath); // fayl bormi?
      await unlink(fullPath); // o'chirish
    } catch {
      // Fayl yo'q — muammo emas, davom etish
    }
  }

  // Fayl ma'lumotlarini olish
  getFileInfo(file: Express.Multer.File) {
    return {
      filename:     file.filename,
      originalName: file.originalname,
      size:         file.size,
      mimetype:     file.mimetype,
      url:          this.getFileUrl(file.filename, this.getFolderByMime(file.mimetype)),
    };
  }

  private getFolderByMime(mimetype: string): string {
    if (mimetype.startsWith('image/')) return 'avatars';
    return 'documents';
  }
}

Upload Controller — endpointlar

// src/upload/upload.controller.ts
import {
  Controller, Post, UseInterceptors,
  UploadedFile, UploadedFiles, UseGuards,
  BadRequestException,
} from '@nestjs/common';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { UploadService } from './upload.service';
import { avatarUploadConfig, documentUploadConfig } from './multer.config';

@Controller('upload')
@UseGuards(JwtAuthGuard)
export class UploadController {
  constructor(private uploadService: UploadService) {}

  // Bitta rasm yuklash
  // POST /upload/avatar
  @Post('avatar')
  @UseInterceptors(FileInterceptor('file', avatarUploadConfig))
  uploadAvatar(@UploadedFile() file: Express.Multer.File) {
    if (!file) throw new BadRequestException('Fayl yuklanmadi');
    return this.uploadService.getFileInfo(file);
  }

  // Ko'p fayl yuklash (maksimal 5 ta)
  // POST /upload/documents
  @Post('documents')
  @UseInterceptors(FilesInterceptor('files', 5, documentUploadConfig))
  uploadDocuments(@UploadedFiles() files: Express.Multer.File[]) {
    if (!files?.length) throw new BadRequestException('Fayllar yuklanmadi');
    return files.map(f => this.uploadService.getFileInfo(f));
  }
}

FileInterceptor('file', config) — bitta fayl. 'file' — form-data dagi maydon nomi. FilesInterceptor('files', 5, config) — ko'p fayl, maksimal 5 ta.


Avatar yuklash — Users controller bilan birlashtirish

// src/users/users.controller.ts
import { Controller, Post, UseInterceptors, UploadedFile, Req, UseGuards } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { avatarUploadConfig } from '../upload/multer.config';
import { UploadService } from '../upload/upload.service';
import { UsersService } from './users.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';

@Controller('users')
@UseGuards(JwtAuthGuard)
export class UsersController {
  constructor(
    private usersService: UsersService,
    private uploadService: UploadService,
  ) {}

  // POST /users/avatar — profil rasmi yangilash
  @Post('avatar')
  @UseInterceptors(FileInterceptor('file', avatarUploadConfig))
  async updateAvatar(
    @Req() req,
    @UploadedFile() file: Express.Multer.File,
  ) {
    if (!file) throw new BadRequestException('Fayl yuklanmadi');

    // Eski avatar URL ni olib, o'chiramiz
    const currentUser = await this.usersService.findById(req.user.id);
    if (currentUser.photo) {
      await this.uploadService.deleteFile(currentUser.photo);
    }

    // Yangi URL ni saqlaymiz
    const url = this.uploadService.getFileUrl(file.filename, 'avatars');
    await this.usersService.update(req.user.id, { photo: url });

    return { photo: url };
  }
}

Static fayllarni ochish

Yuklangan fayllarni brauzerdan ko'rish uchun uploads/ papkasini public qilish:

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  // /uploads/avatars/uuid.jpg → ./uploads/avatars/uuid.jpg
  app.useStaticAssets(join(__dirname, '..', 'uploads'), {
    prefix: '/uploads',
  });

  await app.listen(3000);
}

Endi http://localhost:3000/uploads/avatars/uuid.jpg orqali ko'rinadi.

Nima uchun NestExpressApplication? useStaticAssets — Express spesifik metod. Generic INestApplication da mavjud emas.


.gitignore va uploads/ papkasi

# .gitignore
uploads/

# Lekin papka tuzilishi git da bo'lsin
# uploads/avatars/.gitkeep
# uploads/documents/.gitkeep
# Papkalarni yarating
mkdir -p uploads/avatars uploads/documents uploads/temp
touch uploads/avatars/.gitkeep uploads/documents/.gitkeep

Tez-tez uchraydigan xatolar

uploads/ papkasi yo'q — Multer xato beradi

// ❌ Papka yo'q bo'lsa MulterError
destination: './uploads/avatars'

// ✅ Papkani oldin yarating yoki kod bilan tekshiring
import { mkdirSync, existsSync } from 'fs';

const dest = './uploads/avatars';
if (!existsSync(dest)) mkdirSync(dest, { recursive: true });

Fayl turi MIME bilan emas, kengaytma bilan tekshiriladi

// ❌ MIME soxtalashtirish mumkin — .jpg faylni image/png deb yuborsa
if (file.mimetype !== 'image/jpeg') ...

// ✅ Kengaytmani ham tekshirish
const ext = extname(file.originalname).toLowerCase();
if (!['.jpg', '.jpeg', '.png'].includes(ext)) ...

Asl fayl nomi saqlansa — path traversal xavfi

// ❌ Xavfli — "../../etc/passwd" kabi nom yuborilishi mumkin
filename: (req, file, cb) => cb(null, file.originalname)

// ✅ UUID bilan noyob va xavfsiz nom
filename: (req, file, cb) => cb(null, `${uuid()}${extname(file.originalname)}`)

Xulosa

File Upload — Multer + NestJS interceptor. Konfiguratsiyani alohida faylda saqlash, service da fayl boshqarish, controller da faqat endpoint — toza arxitektura.

Eslab qoling:

  • diskStorageConfig(folder) — papka va nom factory
  • fileFilter(types) — ruxsat etilgan formatlar factory
  • limits.fileSize — hajm chegarasi (baytda)
  • uuid() — xavfsiz fayl nomi
  • app.useStaticAssets(path, { prefix }) — statik fayllar
  • uploads/ papkasi git da emas, .gitkeep bilan tuzilish saqlash

Keyingi dars

31-dars: Amazon S3 / Cloud File Storage