This commit is contained in:
Shibly Teknologi Solusi
2026-05-18 15:25:01 +07:00
parent a6a76ec706
commit 03022576bd
3 changed files with 186 additions and 23 deletions

View File

@ -0,0 +1,182 @@
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 config from "config";
import fs from "fs";
import path from "path";
const log: Logger<ILogObj> = new Logger({ name: '[FileController]', type: 'pretty' });
export class FileController {
static async upload(req: Request, res: Response, next: NextFunction): Promise<Response> {
/* #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)"
},
path: {
type: "string",
format: "text",
description: "Path File"
},
},
required: ["file", "path"]
}
}
}
}
*/
try {
const schema = Joi.object().keys({
path: Joi.string().required().label('File Path'),
});
const param: { path: string } = await schema.validateAsync(req.body);
if (param.path.includes('..')) {
return ReturnHelper.errorResponse(res, 400, 400, Language.lang.failed_insert, "Invalid path");
}
// 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'];
if (!allowedMimes.includes(file.mimetype)) {
return ReturnHelper.errorResponse(res, 400, 400, Language.lang.failed_insert, "Invalid file type. Only JPG, PNG, and WEBP are allowed");
}
param.path = param.path[param.path.length - 1] !== '/' ? param.path + '/' : param.path;
if (!param.path.startsWith('uploads/')) {
param.path = 'uploads/' + param.path.replace(/^\/+/, '');
}
const ext = file.name.split('.');
const name = uuidv4() + '.' + ext[ext.length - 1];
const result: { file: string } = {
file: "",
};
const folder_path = param.path;
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<Response> {
/*
#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" },
path: { type: "string", description: "Folder path (e.g. patient-photos or uploads/patient-photos/)" }
},
required: ["file_name", "path"]
}
}
}
}
*/
try {
const schema = Joi.object().keys({
file_name: Joi.string().required().label('File Name'),
path: Joi.string().required().label('File Path'),
});
const param: { file_name: string; path: string } = await schema.validateAsync(req.body);
if (param.path.includes('..')) {
return ReturnHelper.errorResponse(res, 400, 400, Language.lang.failed_delete, "Invalid path");
}
param.path = param.path[param.path.length - 1] !== '/' ? param.path + '/' : param.path;
if (!param.path.startsWith('uploads/')) {
param.path = 'uploads/' + param.path.replace(/^\/+/, '');
}
const file_path = path.join(param.path, 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);
}
}
}