diff --git a/apps/backend/src/app/storage/storage.module.ts b/apps/backend/src/app/storage/storage.module.ts index 794bf27..128f770 100644 --- a/apps/backend/src/app/storage/storage.module.ts +++ b/apps/backend/src/app/storage/storage.module.ts @@ -1,7 +1,9 @@ import { Module } from "@nestjs/common"; import { StorageService } from "apps/backend/src/app/storage/storage.service"; +import { DbModule } from 'apps/backend/src/app/db/db.module'; @Module({ + imports: [DbModule], providers: [StorageService], }) export class StorageModule {} diff --git a/apps/backend/src/app/storage/storage.service.ts b/apps/backend/src/app/storage/storage.service.ts index c24b452..c4f23b6 100644 --- a/apps/backend/src/app/storage/storage.service.ts +++ b/apps/backend/src/app/storage/storage.service.ts @@ -9,45 +9,25 @@ import { NotFoundException, } from "@nestjs/common"; import FileType from "file-type"; +import { DbService } from 'apps/backend/src/app/db/db.service'; +import { IFileInformation } from 'apps/backend/src/app/storage/storage.types'; +import { FilesTypeForMachine, FilesTypesTable } from 'apps/backend/src/app/db/schema'; +import { eq } from 'drizzle-orm'; @Injectable() 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 - } + //TODO make file size configurable by admin + private maxFileSize = 256; // MiB unit + constructor(private readonly dbService: DbService) {} + /** - * Calculates the checksum of a given file. + * Save a file to the specified directory. * - * @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): string { - 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. + * @param {string} fileName - The name of the file. + * @param {Buffer} file - The file content in Buffer format. + * @throws {InternalServerErrorException} If the file save fails. + * @return {Promise} A Promise that resolves when the file is successfully saved. */ private async saveFile(fileName: string, file: Buffer): Promise { try { @@ -59,38 +39,81 @@ export class StorageService { } /** - * 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. + * Checks if the current MIME type and file size meet the specified conditions. + * @param {Array} machineIds - The IDs of the associated machines. + * @param {Buffer} file - The file to check. + * @return {Promise} - A Promise that resolves to true if the conditions are met, false otherwise. */ - 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.", - ); + private async checkConditions(machineIds: Array,file: Buffer): Promise { + + /** + * Checks if the current MIME type is allowed based on the given set of allowed MIME types. + * @param {Set} allowedMime - The set of allowed MIME types. + * @param {string} currentMime - The current MIME type to check. + * @return {boolean} - True if the current MIME type is allowed, false otherwise. + */ + function checkMime(allowedMime: Set, currentMime: string): boolean { + for (const mime of allowedMime) if (mime === currentMime) return true; + return false; } + + const fileType = await FileType.fileTypeFromBuffer(file); + + // Array of MIMEs with possible duplicate field + const _mimes: Array = [] + + // Fetching MIMEs for the associated machines + for (const machineId of machineIds) { + console.debug(`Fetching mimeTypes for machine : ${machineId}`) + // Get mimes associated to a machine + const allowedMimeId = this.dbService.use() + .select() + .from(FilesTypeForMachine) + .where(eq(FilesTypeForMachine.machineId, machineId)).as("allowedMimeId"); + const _allowedMime = await this.dbService.use() + .select({ + slug: FilesTypesTable.mime, + name: FilesTypesTable.typeName + }) + .from(FilesTypesTable) + .leftJoin(allowedMimeId, eq(FilesTypesTable.id, allowedMimeId.fileTypeId)) + console.debug(`Total : ${_allowedMime.length}`) + // Append each mime of a machine + for (const allowedMimeElement of _allowedMime) { + _mimes.push(allowedMimeElement.slug) + } + } + //Store the MIMEs without duplicate + const mimeSet = new Set(_mimes) + console.debug(`Indexed ${mimeSet.size} unique mimeTypes`) + //check file size is less than 2mb const fileSize = file.byteLength; - //TODO make file size configurable by admin - if (fileSize > 2 * 1024 * 1024) { + if (fileSize > this.maxFileSize * (1024 * 1024)) { throw new BadRequestException( - "File size exceeds the limit. Maximum file size allowed is 2MB.", + "File size to high.", + { + cause: "File size", + description: `File size exceeds the limit. Maximum file size allowed is ${this.maxFileSize}MiB.` + } ); } - return checksum; + + if (!checkMime(mimeSet, fileType.mime)) throw new BadRequestException( + { + cause: "MIME type", + description: `Invalid MIME type. Allowed MIME types are: ${[...mimeSet].join(", ")}.` + } + ) + + return true } /** * 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. + * @throws {NotFoundException} If the file is not found. + * @returns {Promise} A Promise that resolves to the file contents as a Buffer. */ private async getFile(fileName: string): Promise { try { @@ -99,4 +122,61 @@ export class StorageService { throw new NotFoundException("File not found"); } } + + /** + * Calculates the SHA256 checksum for a given file. + * + * @param {Buffer} file - The file to calculate the checksum for. + * @return {Promise} A Promise resolving to the calculated checksum as a lowercase hexadecimal string. + * @throws {InternalServerErrorException} If an error occurs during the checksum calculation. + */ + public async getChecksum(file: Buffer): Promise { + return new Promise((resolve) => { + try { + const checksum = crypto.createHash("sha256").update(file).digest("hex").toLowerCase(); + resolve(checksum) + } catch (err) { + throw new InternalServerErrorException(err) + } + }) + } + + /** + * Generate file information based on the provided file and display name. + * + * @param {Buffer} file - The file in Buffer format. + * @param {string} fileDisplayName - The display name of the file. + * @param {boolean} [isDocumentation] - Optional flag to indicate if the file is a documentation file. + * @returns {Promise} - A Promise that resolves to the generated file information. + */ + public async generateInformation(file: Buffer, fileDisplayName: string, isDocumentation?: boolean): Promise { + const fileType = await FileType.fileTypeFromBuffer(file); + const checksum = await this.getChecksum(file) + const fileName = `${isDocumentation ? "doc" : "file"}-${checksum}.${fileType.ext.toLowerCase()}`; + return { + fileName: fileName, + fileDisplayName: fileDisplayName, + fileSize: file.byteLength, + fileChecksum: checksum, + fileType: fileType, + isDocumentation: isDocumentation || false + } + } + + public async new(fileDisplayName: string, file: Buffer, isDocumentation?: boolean) { + try { + const info = await this.generateInformation(file, fileDisplayName, isDocumentation); + console.log(`Trying to append a new file : "${info.fileDisplayName}"...\n > Checksum SHA-256 : ${info.fileChecksum}\n > Size : ${info.fileSize / (1024*1024)}Mio\n > File format : ${info.fileType.mime}\n`) + const condition = await this.checkConditions([], file) + if (!condition) { + console.warn(`File "${info.fileDisplayName}" did not pass the files requirement.\n${info.fileChecksum}`) + } + + //TODO Append in DB and save to storage + + + } catch (err) { + throw new BadRequestException(err) + } + } } diff --git a/apps/backend/src/app/storage/storage.types.ts b/apps/backend/src/app/storage/storage.types.ts new file mode 100644 index 0000000..071572b --- /dev/null +++ b/apps/backend/src/app/storage/storage.types.ts @@ -0,0 +1,17 @@ +import FileType from 'file-type'; + + +export interface IFileInformation { + fileDisplayName: string; + fileName: string; + fileChecksum: string; + isDocumentation: boolean; + fileType: FileType.FileTypeResult; + fileSize: number; +} + +export interface IFileWithInformation { + buffer: Buffer; + info: IFileInformation; + additionalData?: AdditionalData +} \ No newline at end of file