diff --git a/apps/backend/src/app/storage/storage.service.ts b/apps/backend/src/app/storage/storage.service.ts index 81f5680..1ab28d3 100644 --- a/apps/backend/src/app/storage/storage.service.ts +++ b/apps/backend/src/app/storage/storage.service.ts @@ -1,4 +1,95 @@ -import { Injectable } from "@nestjs/common"; +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 StorageService {} +export class StorageService { + + /** + * Saves the given file with a generated file name that includes a prefix, + * the checksum of the file, and the file type extension. + * + * @param {string} prefix - The prefix to include in the generated file name. + * @param {Buffer} file - The file to save. + * @returns {Promise} A promise that resolves with the generated file name after the file is saved. + */ + private async save(prefix: string, file: Buffer) { + const checksum = await this.checkConditions(file) + const fileType = await FileType.fileTypeFromBuffer(file) + const fileName = `${prefix.toLowerCase()}-${checksum}.${fileType.ext.toLowerCase()}`; + await this.saveFile(fileName, file) + return fileName; + //SEC TODO Catch case + } + + /** + * Calculates the checksum of a given file. + * + * @param {Buffer} file - The file for which the checksum is to be calculated. + * @return {string} - The hexadecimal representation of the checksum. + */ + private getChecksum(file: Buffer) { + return crypto.createHash('sha256').update(file).digest('hex').toLowerCase(); + } + + /** + * Saves a file to the specified location with the provided name. + * + * @param {string} fileName - The name of the file to be saved. + * @param {Buffer} file - The file content to be saved. + * + * @return {Promise} - A promise that resolves when the file is successfully saved. + * + * @throws {InternalServerErrorException} - If there is an error while saving the file. + */ + private async saveFile(fileName: string, file: Buffer): Promise { + try { + await writeFile(join(process.cwd(), 'files/', fileName), file, 'utf8'); + } catch (err) { + console.error(err); + throw new InternalServerErrorException("File save failed !") + } + } + + /** + * Checks the conditions for a given file. + * + * @param file - The file to check. + * @returns The checksum of the file. + * @throws BadRequestException - If the file type is invalid or the file size exceeds the limit. + */ + private async checkConditions(file: Buffer): Promise { + const checksum = this.getChecksum(file) + const fileType = await FileType.fileTypeFromBuffer(file) + //TODO make file type configurable by admin + 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; + //TODO make file size configurable by admin + if (fileSize > 2 * 1024 * 1024) { + throw new BadRequestException('File size exceeds the limit. Maximum file size allowed is 2MB.'); + } + return checksum; + } + + /** + * Retrieves the contents of a file as a Buffer. + * + * @param {string} fileName - The name of the file to retrieve. + * @return {Promise} A Promise that resolves with the contents of the file as a Buffer. + * @throws {NotFoundException} If the file could not be found. + */ + private async getFile(fileName: string): Promise { + try { + return await readFile(join(process.cwd(), 'files/', fileName)); + } catch (err) { + throw new NotFoundException("File not found") + } + } + +}