diff --git a/controllers/file_uploads.controller.js b/controllers/file_uploads.controller.js new file mode 100644 index 0000000..d6ce4c0 --- /dev/null +++ b/controllers/file_uploads.controller.js @@ -0,0 +1,151 @@ +const path = require("path"); +const fs = require("fs"); +const { setResponse } = require("../helpers/utils"); +const { + createFileUploadDb, + deleteFileUploadByPathDb, +} = require("../db/file_uploads.db"); +const { + createSolutionDb, +} = require("../db/brand.db"); + +const uploadFile = async (req, res) => { + try { + if (!req.file) { + const response = await setResponse([], "Tidak ada file yang diunggah", 400); + return res.status(400).json(response); + } + + const file = req.file; + const ext = path.extname(file.originalname).toLowerCase(); + const typeDoc = ext === ".pdf" ? "PDF" : "IMAGE"; + const folder = typeDoc === "PDF" ? "pdf" : "images"; + + const pathDocument = `${folder}/${file.filename}`; + + // Insert ke DB via DB layer + const fileData = { + file_upload_name: file.originalname, + path_document: pathDocument, + type_document: typeDoc, + createdBy: req.user?.user_id || null, + }; + + await createFileUploadDb(fileData); + + const response = await setResponse( + { name: file.originalname, path: pathDocument }, + "File berhasil diunggah" + ); + res.status(200).json(response); + } catch (error) { + const response = await setResponse([], error.message, 500); + res.status(500).json(response); + } +}; + +const getFile = (folder) => async (req, res) => { + try { + const { filename } = req.params; + const filePath = path.join(__dirname, "../uploads", folder, filename); + + if (!fs.existsSync(filePath)) { + const response = await setResponse([], "File tidak ditemukan", 404); + return res.status(404).json(response); + } + + res.sendFile(filePath); + } catch (error) { + const response = await setResponse([], error.message, 500); + res.status(500).json(response); + } +}; + +const deleteFile = (folder) => async (req, res) => { + try { + const { filename } = req.params; + const filePath = path.join(__dirname, "../uploads", folder, filename); + + if (!fs.existsSync(filePath)) { + const response = await setResponse([], "File tidak ditemukan", 404); + return res.status(404).json(response); + } + + // Delete physical file + fs.unlinkSync(filePath); + + const pathDocument = `${folder}/${filename}`; + const deletedBy = req.user?.user_id || null; + await deleteFileUploadByPathDb(pathDocument, deletedBy); + + const response = await setResponse([], "File berhasil dihapus"); + res.status(200).json(response); + } catch (error) { + const response = await setResponse([], error.message, 500); + res.status(500).json(response); + } +}; + +const uploadSolutionFile = async (req, res) => { + try { + if (!req.file) { + const response = await setResponse([], "Tidak ada file yang diunggah", 400); + return res.status(400).json(response); + } + + const { error_code_id, solution_name } = req.body; + + if (!error_code_id || !solution_name) { + const response = await setResponse([], "error_code_id dan solution_name harus diisi", 400); + return res.status(400).json(response); + } + + const file = req.file; + const ext = path.extname(file.originalname).toLowerCase(); + const typeDoc = ext === ".pdf" ? "PDF" : "IMAGE"; + const folder = typeDoc === "PDF" ? "pdf" : "images"; + const pathDocument = `${folder}/${file.filename}`; + + const fileData = { + file_upload_name: file.originalname, + path_document: pathDocument, + type_document: typeDoc, + createdBy: req.user?.user_id || null, + }; + + await createFileUploadDb(fileData); + + const solutionData = { + solution_name: solution_name, + type_solution: typeDoc.toLowerCase(), + path_solution: pathDocument, + is_active: true, + created_by: req.user?.user_id || null + }; + + const solutionId = await createSolutionDb(error_code_id, solutionData); + + const response = await setResponse( + { + solution_id: solutionId, + solution_name: solution_name, + error_code_id: error_code_id, + file_name: file.originalname, + file_path: pathDocument, + file_type: typeDoc.toLowerCase() + }, + "Solution file berhasil diunggah" + ); + res.status(200).json(response); + } catch (error) { + const response = await setResponse([], error.message, 500); + res.status(500).json(response); + } +}; + +module.exports = { + uploadFile, + uploadSolutionFile, + getFile, + deleteFile, +}; \ No newline at end of file diff --git a/db/file_uploads.db.js b/db/file_uploads.db.js new file mode 100644 index 0000000..d10fbaf --- /dev/null +++ b/db/file_uploads.db.js @@ -0,0 +1,92 @@ +const pool = require("../config"); + +// Get file upload by path +const getFileUploadByPathDb = async (path) => { + const queryText = ` + SELECT + file_upload_id, + file_upload_name, + type_document, + path_document, + created_by, + updated_by, + deleted_by, + created_at, + updated_at, + deleted_at + FROM file_upload + WHERE path_document = $1 AND deleted_at IS NULL + `; + const result = await pool.query(queryText, [path]); + return result.recordset[0]; +}; + +// Create file upload +const createFileUploadDb = async (data) => { + const store = { + file_upload_name: data.file_upload_name, + }; + + // Add path_document if exists + if (data.path_document) { + store.path_document = data.path_document; + } + + // Add type_document if exists + if (data.type_document) { + store.type_document = data.type_document; + } + + if (data.createdBy) { + store.created_by = data.createdBy; + } + + console.log('Data to insert:', store); + + const queryText = ` + INSERT INTO file_upload (file_upload_name, path_document, type_document, created_by, created_at, updated_at) + VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + SELECT SCOPE_IDENTITY() as inserted_id; + `; + const values = [ + store.file_upload_name, + store.path_document, + store.type_document, + store.created_by || null + ]; + + // console.log('Manual Query:', queryText); + // console.log('Manual Values:', values); + + const result = await pool.query(queryText, values); + return result.recordset[0]; +}; + +// Soft delete file upload by path +const deleteFileUploadByPathDb = async (path, deletedBy = null) => { + const store = { + deleted_at: new Date(), + }; + + if (deletedBy) { + store.deleted_by = deletedBy; + } + + const whereData = { + path_document: path, + }; + + const { query: queryText, values } = pool.buildDynamicUpdate( + "file_upload", + store, + whereData + ); + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return true; +}; + +module.exports = { + getFileUploadByPathDb, + createFileUploadDb, + deleteFileUploadByPathDb, +}; \ No newline at end of file diff --git a/middleware/uploads.js b/middleware/uploads.js new file mode 100644 index 0000000..9e683d2 --- /dev/null +++ b/middleware/uploads.js @@ -0,0 +1,69 @@ +const multer = require("multer"); +const path = require("path"); +const { randomUUID } = require("crypto"); +const fs = require("fs"); + +// Fungsi untuk tentuin folder berdasarkan file type +function getFolderByType(mimetype) { + if (mimetype === "application/pdf") { + return "pdf"; + } else if (mimetype.startsWith("image/")) { + return "images"; + } + return "file"; +} + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const folderName = getFolderByType(file.mimetype); + const folderPath = path.join(__dirname, "../uploads", folderName); + + fs.mkdirSync(folderPath, { recursive: true }); + + cb(null, folderPath); + }, + filename: (req, file, cb) => { + const ext = path.extname(file.originalname); + const timestamp = Date.now(); + + const date = new Date(timestamp); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + const hours = String(date.getHours()).padStart(2, "0"); + const minutes = String(date.getMinutes()).padStart(2, "0"); + const seconds = String(date.getSeconds()).padStart(2, "0"); + + const formattedDate = `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`; + + // Prefix berdasarkan tipe file + const prefix = file.mimetype === "application/pdf" ? "pdf" : "img"; + const uniqueId = randomUUID().slice(0, 8); + + cb(null, `${prefix}-${uniqueId}-${formattedDate}${ext}`); + }, +}); + +const upload = multer({ + storage, + fileFilter: (req, file, cb) => { + const allowedTypes = /jpeg|jpg|png|pdf/; + const allowedMimeTypes = /image\/(jpeg|jpg|png)|application\/pdf/; + + const extname = allowedTypes.test( + path.extname(file.originalname).toLowerCase() + ); + const mimetype = allowedMimeTypes.test(file.mimetype); + + if (extname && mimetype) { + return cb(null, true); + } else { + cb(new Error("File type not allowed. Only PDF and Images (JPEG, JPG, PNG) are accepted.")); + } + }, + limits: { + fileSize: 10 * 1024 * 1024, // 10MB max file size + }, +}); + +module.exports = upload; \ No newline at end of file diff --git a/routes/file_uploads.route.js b/routes/file_uploads.route.js new file mode 100644 index 0000000..53f4fbc --- /dev/null +++ b/routes/file_uploads.route.js @@ -0,0 +1,21 @@ +const router = require("express").Router(); +const upload = require("../middleware/uploads"); +const verifyToken = require("../middleware/verifyToken"); +const verifyAccess = require("../middleware/verifyAccess"); +const { + uploadFile, + getFile, + deleteFile, +} = require("../controllers/file_uploads.controller"); + +router.post("/", verifyToken.verifyAccessToken, verifyAccess(), upload.single("file"), uploadFile); + +router.route("/pdf/:filename") + .get(verifyToken.verifyAccessToken, getFile("pdf")) + .delete(verifyToken.verifyAccessToken, verifyAccess(), deleteFile("pdf")); + +router.route("/images/:filename") + .get(verifyToken.verifyAccessToken, getFile("images")) + .delete(verifyToken.verifyAccessToken, verifyAccess(), deleteFile("images")); + +module.exports = router; \ No newline at end of file