From 314625bd9cc3c9fbd88e377927e20104997c95de Mon Sep 17 00:00:00 2001 From: Mathis Date: Wed, 24 Jul 2024 20:26:04 +0200 Subject: [PATCH] feat(uploads): add file upload and retrieval capabilities Introduced a new uploads module with controllers and service methods for handling file uploads and retrievals. Includes route guards, file validations, and error handling for both avatar and product image uploads. --- src/uploads/uploads.controller.ts | 33 +++++++++++++++ src/uploads/uploads.module.ts | 15 +++++++ src/uploads/uploads.service.ts | 67 +++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 src/uploads/uploads.controller.ts create mode 100644 src/uploads/uploads.module.ts create mode 100644 src/uploads/uploads.service.ts diff --git a/src/uploads/uploads.controller.ts b/src/uploads/uploads.controller.ts new file mode 100644 index 0000000..42cbe5b --- /dev/null +++ b/src/uploads/uploads.controller.ts @@ -0,0 +1,33 @@ +import { Controller, Get, Param, Post, Res, UploadedFile, UseGuards, UseInterceptors } from "@nestjs/common"; +import { FileInterceptor } from "@nestjs/platform-express"; +import { join } from 'node:path'; +import { AdminGuard, UserGuard } from "src/auth/auth.guard"; + +@Controller('uploads') +export class UploadsController { + @Post("p/new") + @UseGuards(AdminGuard) + @UseInterceptors(FileInterceptor('file')) + uploadProductImage(@UploadedFile() file) { + + } + + @Post("avatar") + @UseGuards(UserGuard) + @UseInterceptors(FileInterceptor('file')) + uploadAvatar(@UploadedFile() file) { + console.log(file); + } + + @Get('avatar/:userId') + seeUploadedAvatar(@Param('filepath') file: string, @Res() res: Response) { + const filePath = join(__dirname, '..', 'avatars', file); + res.sendFile(filePath); + } + + @Get('p/:filepath') + seeUploadedFile(@Param('filepath') file: string, @Res() res: Response) { + const filePath = join(__dirname, '..', 'files', file); + res.sendFile(filePath); + } +} diff --git a/src/uploads/uploads.module.ts b/src/uploads/uploads.module.ts new file mode 100644 index 0000000..2a35306 --- /dev/null +++ b/src/uploads/uploads.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { UploadsService } from './uploads.service'; +import { UploadsController } from './uploads.controller'; +import { MulterModule } from "@nestjs/platform-express"; + +@Module({ + imports: [ + MulterModule.register({ + dest: '/files', + }), + ], + providers: [UploadsService], + controllers: [UploadsController] +}) +export class UploadsModule {} diff --git a/src/uploads/uploads.service.ts b/src/uploads/uploads.service.ts new file mode 100644 index 0000000..a1327be --- /dev/null +++ b/src/uploads/uploads.service.ts @@ -0,0 +1,67 @@ +import { BadRequestException, Injectable, InternalServerErrorException, NotFoundException } from "@nestjs/common"; +import * as crypto from "node:crypto"; +import { readFile, writeFile } from 'node:fs/promises'; +import { join } from "node:path"; +import * as console from "node:console"; +import FileType from 'file-type'; + +@Injectable() +export class UploadsService { + + //TODO save avatar under avatar-{userId}-{checksum} + private async saveAvatar(userId: string, file: Buffer) { + const checksum = await this.checkConditions(file) + const fileType = await FileType.fileTypeFromBuffer(file) + const fileName = `avatar-${userId}.${fileType.ext}`; + await this.saveFile(fileName, file) + } + //TODO get + private async getAvatar(userId: string) { + + } + + //TODO save product image under product-{productId}-{checksum} + async saveProductImage(productId: string, imageBuffer: Buffer): Promise { + + } + //TODO get + async getProductImage(imageId: string): Promise { + + } + + private getChecksum(file: Buffer) { + return crypto.createHash('sha256').update(file).digest('hex').toLowerCase(); + } + + private async saveFile(fileName: string, file: Buffer) { + try { + await writeFile(join(process.cwd(), 'files/', fileName), file, 'utf8'); + } catch (err) { + console.error(err); + throw new InternalServerErrorException("File save failed !") + } + } + + private async checkConditions(file: Buffer) { + const checksum = this.getChecksum(file) + const fileType = await FileType.fileTypeFromBuffer(file) + if (!fileType || !fileType.mime.startsWith('image/')) { + throw new BadRequestException('Invalid file type. Only images are allowed.'); + } + //check file size is less than 2mb + const fileSize = file.byteLength; + if (fileSize > 2 * 1024 * 1024) { + throw new BadRequestException('File size exceeds the limit. Maximum file size allowed is 2MB.'); + } + return checksum; + } + + private async getFile(fileName: string){ + try { + return await readFile(join(process.cwd(), 'files/', fileName)); + } catch (err) { + throw new NotFoundException("File not found") + } + } + +}