import { Response, NextFunction } from "express"; import { Request } from "express-jwt"; import { ReturnHelper } from "../../helpers/express/return"; import Joi from "joi"; import { ILogObj, Logger } from "tslog"; import { Language } from "../../langs/lang"; import fileUpload from "express-fileupload"; import { v4 as uuidv4 } from 'uuid'; import fs from "fs"; import path from "path"; const log: Logger = new Logger({ name: '[FileController]', type: 'pretty' }); const STORAGE_DIR = 'assets/saude/'; export class FileController { static async upload(req: Request, res: Response, next: NextFunction): Promise { /* #swagger.tags = ['Handle File'] #swagger.security = [{ "bearerAuth": [] }] #swagger.consumes = ['multipart/form-data'] #swagger.requestBody = { required: true, content: { "multipart/form-data": { schema: { type: "object", properties: { file: { type: "string", format: "binary", description: "Image file (max 1MB, jpg/png/webp)" }, }, required: ["file"] } } } } */ try { // Check if file exists if (!req.files || !req.files.file) { return ReturnHelper.errorResponse(res, 400, 404, Language.lang.failed_insert, "File not found"); } const file = req.files.file as fileUpload.UploadedFile; // Validate file size (max 1MB) const maxSize = 10 * 1024 * 1024; // 10MB if (file.size > maxSize) { return ReturnHelper.errorResponse( res, 400, 400, Language.lang.failed_insert, "File size exceeds 10MB limit" ); } // Validate file type const allowedMimes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/svg+xml', 'image/svg']; if (!allowedMimes.includes(file.mimetype)) { return ReturnHelper.errorResponse(res, 400, 400, Language.lang.failed_insert, "Invalid file type. Only JPG, PNG, WEBP, and SVG are allowed"); } const ext = file.name.split('.'); const name = uuidv4() + '.' + ext[ext.length - 1]; const result: { file: string } = { file: "", }; const folder_path = STORAGE_DIR; const file_path = path.join(folder_path, name); // Create directory if it doesn't exist if (!fs.existsSync(folder_path)) { fs.mkdirSync(folder_path, { recursive: true }); } // Move file to destination await file.mv(file_path); result.file = name; log.info(`File uploaded successfully: ${file_path}`); return ReturnHelper.successResponseAny(res, 200, Language.lang.success_insert, result); } catch (e: unknown) { // Proper error logging to avoid tslog serialization issues if (e instanceof Error) { log.error("File upload failed:", { message: e.message, stack: e.stack, name: e.name }); return ReturnHelper.errorResponse(res, 500, 500, Language.lang.failed_insert, e.message); } else { log.error("File upload failed with unknown error:", String(e)); return ReturnHelper.errorResponse(res, 500, 500, Language.lang.failed_insert, "Unknown error occurred"); } } } static async delete(req: Request, res: Response, next: NextFunction): Promise { /* #swagger.tags = ['Handle File'] #swagger.security = [{ "bearerAuth": [] }] #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", properties: { file_name: { type: "string", description: "File name to delete" } }, required: ["file_name"] } } } } */ try { const schema = Joi.object().keys({ file_name: Joi.string().required().label('File Name'), }); const param: { file_name: string } = await schema.validateAsync(req.body); if (param.file_name.includes('..') || param.file_name.includes('/') || param.file_name.includes('\\')) { return ReturnHelper.errorResponse(res, 400, 400, Language.lang.failed_delete, "Invalid file name"); } const file_path = path.join(STORAGE_DIR, param.file_name); // Check if file exists if (!fs.existsSync(file_path)) { return ReturnHelper.errorResponse(res, 404, 404, Language.lang.failed_not_found, "File not found"); } // Delete file fs.unlinkSync(file_path); return ReturnHelper.successResponseAny(res, 200, Language.lang.success_delete, {}); } catch (e: unknown) { log.error(e); const err = e as Error; return ReturnHelper.errorResponse(res, 500, 402, Language.lang.failed_delete, err.message); } } static async download(req: Request, res: Response, next: NextFunction): Promise { /* #swagger.tags = ['Handle File'] #swagger.security = [{ "bearerAuth": [] }] #swagger.parameters['name'] = { in: 'query', required: true, type: 'string', description: 'File name' } #swagger.parameters['token'] = { in: 'query', required: true, type: 'string', description: 'Token' } */ try { const schema = Joi.object({ name: Joi.string().required().label("File Name"), token: Joi.string().required().label("Token"), }); const param: { name: string } = await schema.validateAsync(req.query); if (param.name.includes('..') || param.name.includes('/') || param.name.includes('\\')) { return ReturnHelper.errorResponse(res, 400, 400, Language.lang.failed_not_found, "Invalid file name"); } const file_path = path.join(STORAGE_DIR, param.name); if (!fs.existsSync(file_path)) { return ReturnHelper.errorResponse(res, 404, 404, Language.lang.failed_not_found, "File not found"); } const buffer = fs.readFileSync(file_path); const fileName = param.name; const ext = fileName.split(".").pop()?.toLowerCase(); const mimeMap: Record = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", webp: "image/webp", gif: "image/gif", pdf: "application/pdf", svg: "image/svg+xml", }; const contentType = mimeMap[ext ?? ""] ?? "application/octet-stream"; res.setHeader("Content-Type", contentType); res.setHeader("Content-Length", buffer.length); const isInline = contentType.startsWith("image/") || contentType === "application/pdf"; if (isInline) { res.setHeader("Content-Disposition", `inline; filename="${encodeURIComponent(fileName)}"`); } else { res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(fileName)}"`); } return res.end(buffer); } catch (e: unknown) { log.error(e); const err = e as Error; return ReturnHelper.errorResponse(res, 400, 401, Language.lang.failed_not_found, err.message); } } }