Refactor StorageService to include MIME type and size checks
Reorganized StorageService to incorporate MIME type validation and adjustable file size limits, enhancing file validation. Introduced new methods for checksum calculation and file information generation, and updated the StorageModule to import the DbModule for database interactions.
This commit is contained in:
parent
d42aaeda05
commit
f4393301a2
@ -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 {}
|
||||
|
@ -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<string>} 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<void>} - 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<void>} A Promise that resolves when the file is successfully saved.
|
||||
*/
|
||||
private async saveFile(fileName: string, file: Buffer): Promise<void> {
|
||||
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<string>} machineIds - The IDs of the associated machines.
|
||||
* @param {Buffer} file - The file to check.
|
||||
* @return {Promise<boolean>} - A Promise that resolves to true if the conditions are met, false otherwise.
|
||||
*/
|
||||
private async checkConditions(file: Buffer): Promise<string> {
|
||||
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<string>,file: Buffer): Promise<boolean> {
|
||||
|
||||
/**
|
||||
* Checks if the current MIME type is allowed based on the given set of allowed MIME types.
|
||||
* @param {Set<string>} 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<string>, 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<string> = []
|
||||
|
||||
// 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<Buffer>} 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<Buffer>} A Promise that resolves to the file contents as a Buffer.
|
||||
*/
|
||||
private async getFile(fileName: string): Promise<Buffer> {
|
||||
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<string>} 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<string> {
|
||||
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<IFileInformation>} - A Promise that resolves to the generated file information.
|
||||
*/
|
||||
public async generateInformation(file: Buffer, fileDisplayName: string, isDocumentation?: boolean): Promise<IFileInformation> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
17
apps/backend/src/app/storage/storage.types.ts
Normal file
17
apps/backend/src/app/storage/storage.types.ts
Normal file
@ -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<AdditionalData> {
|
||||
buffer: Buffer;
|
||||
info: IFileInformation;
|
||||
additionalData?: AdditionalData
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user