diff --git a/controllers/contentControllers/level.js b/controllers/contentControllers/level.js index 62d8097..0385fd4 100644 --- a/controllers/contentControllers/level.js +++ b/controllers/contentControllers/level.js @@ -716,6 +716,54 @@ export const deleteLevelFileById = async (req, res) => { } }; +export const deleteLevelFilesById = async (req, res) => { + const { id } = req.params; + const { fileName } = req.body; + + const filePattern = + /^(AUDIO|IMAGE)-([a-f0-9-]{36})-[a-f0-9]{32}\.(mp3|jpg|jpeg|png)$/; + const match = fileName.match(filePattern); + + if (!match) { + return response(400, null, "Invalid file name format", res); + } + + const fileType = match[1]; + const levelIdFromFile = match[2]; + + if (levelIdFromFile !== id) { + return response(400, null, "Level ID in file name does not match", res); + } + + try { + const level = await models.Level.findByPk(id); + + if (!level) { + return response(404, null, "Level not found", res); + } + + let filePath; + + if (fileType === "AUDIO") { + filePath = path.join("public/uploads/level/audio", fileName); + } else if (fileType === "IMAGE") { + filePath = path.join("public/uploads/level/image", fileName); + } else { + return response(400, null, "Invalid file type", res); + } + + if (filePath && fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + return response(200, null, `${fileType} file deleted successfully`, res); + } else { + return response(404, null, "File not found on the server", res); + } + } catch (error) { + console.log(error); + response(500, null, "Internal Server Error", res); + } +}; + export const getPreviousLevel = async (req, res) => { try { const { next_learning } = req.params; @@ -874,3 +922,96 @@ export const getPreviousLevel = async (req, res) => { res.status(500).json({ message: "Internal Server Error" }); } }; + +export const uploadLevelFile = async (req, res) => { + const { levelId } = req.params; + + try { + const level = await models.Level.findByPk(levelId); + + if (!level) { + return response(404, null, "Level not found", res); + } + + const filesToSave = req.filesToSave; + + if (!filesToSave || Object.keys(filesToSave).length === 0) { + return response(400, null, "No files to upload", res); + } + + const { ID_TOPIC, ID_SECTION } = level; + const savedFiles = {}; + + Object.keys(filesToSave).forEach((key) => { + let filename; + if (key.startsWith("AUDIO")) { + const audioFile = filesToSave[key]; + filename = saveFileToDisk( + audioFile, + "AUDIO", + ID_TOPIC, + ID_SECTION, + levelId + ); + } else if (key.startsWith("IMAGE")) { + const imageFile = filesToSave[key]; + filename = saveFileToDisk( + imageFile, + "IMAGE", + ID_TOPIC, + ID_SECTION, + levelId + ); + } + + if (filename) { + savedFiles[key] = filename; + } + }); + + if (Object.keys(savedFiles).length === 0) { + return response(400, null, "Failed to save files", res); + } + + return response(200, savedFiles, "Files uploaded successfully", res); + } catch (error) { + console.error(error); + return response(500, null, "Internal server error", res); + } +}; + +export const getLevelFiles = async (req, res) => { + const { levelId } = req.params; + + try { + const audioFolderPath = path.join("public/uploads/level/audio"); + const imageFolderPath = path.join("public/uploads/level/image"); + + const getFilesByLevelId = (folderPath, fileType) => { + if (fs.existsSync(folderPath)) { + const files = fs.readdirSync(folderPath); + return files.filter((file) => + file.startsWith(`${fileType}-${levelId}`) + ); + } + return []; + }; + + const audioFiles = getFilesByLevelId(audioFolderPath, "AUDIO"); + const imageFiles = getFilesByLevelId(imageFolderPath, "IMAGE"); + + if (audioFiles.length === 0 && imageFiles.length === 0) { + return response(404, null, "No files found for this level", res); + } + + const levelFiles = { + audioFiles, + imageFiles, + }; + + return response(200, levelFiles, "Files retrieved successfully", res); + } catch (error) { + console.error(error); + return response(500, null, "Internal server error", res); + } +}; diff --git a/middlewares/Level/uploadLevelFile.js b/middlewares/Level/uploadLevelFile.js new file mode 100644 index 0000000..1537075 --- /dev/null +++ b/middlewares/Level/uploadLevelFile.js @@ -0,0 +1,137 @@ +import multer from "multer"; +import crypto from "crypto"; +import path from "path"; +import fs from "fs"; +import response from "../../response.js"; + +const memoryStorage = multer.memoryStorage(); + +const fileFilter = (req, file, cb) => { + const ext = path.extname(file.originalname).toLowerCase(); + + if (file.fieldname.startsWith("AUDIO")) { + if (ext === ".mp3") { + cb(null, true); + } else { + cb( + new Error("Invalid file type, only .mp3 files are allowed for audio!"), + false + ); + } + } else if (file.fieldname.startsWith("IMAGE")) { + if (ext === ".jpg" || ext === ".jpeg" || ext === ".png") { + cb(null, true); + } else { + cb( + new Error( + "Invalid file type, only .jpg, .jpeg, and .png files are allowed for image!" + ), + false + ); + } + } else { + cb(new Error("Invalid file type!"), false); + } +}; + +const upload = multer({ + storage: memoryStorage, + fileFilter, + limits: { + fileSize: 100 * 1024 * 1024, + }, +}).any(); + +const handleUploadFile = (req, res, next) => { + upload(req, res, (err) => { + if (err) { + return response(400, null, err.message, res); + } + + const files = req.files || []; + req.filesToSave = {}; + + let validFiles = true; + let errorMessages = []; + + files.forEach((file) => { + if (file.fieldname.startsWith("AUDIO")) { + if (file.size > 10 * 1024 * 1024) { + validFiles = false; + errorMessages.push(`Audio file exceeds the size limit of 10MB`); + } else { + req.filesToSave[file.fieldname] = file; + } + } else if (file.fieldname.startsWith("IMAGE")) { + if (file.size > 5 * 1024 * 1024) { + validFiles = false; + errorMessages.push(`Image file exceeds the size limit of 5MB`); + } else { + req.filesToSave[file.fieldname] = file; + } + } + }); + + if (validFiles) { + next(); + } else { + clearFileBuffers(req.filesToSave); + return response(400, null, errorMessages.join("; "), res); + } + }); +}; + +export const clearFileBuffers = (files) => { + for (const file of Object.values(files)) { + if (file && file.buffer) { + file.buffer = null; + } + } +}; + +export const generateHash = ( + levelId, + sectionId, + topicId, + filename, + bufferLength +) => { + return crypto + .createHash("md5") + .update(levelId + sectionId + topicId + filename + bufferLength) + .digest("hex"); +}; + +export const saveFileToDisk = (file, type, topicId, sectionId, levelId) => { + const ext = path.extname(file.originalname); + const hash = generateHash( + levelId, + sectionId, + topicId, + file.originalname, + file.buffer.length + ); + const filename = `${type}-${levelId}-${hash}${ext}`; + + let folderPath; + switch (type) { + case "AUDIO": + folderPath = path.join("public/uploads/level/audio"); + break; + case "IMAGE": + folderPath = path.join("public/uploads/level/image"); + break; + default: + folderPath = path.join("public/uploads/level"); + } + + if (!fs.existsSync(folderPath)) { + fs.mkdirSync(folderPath, { recursive: true }); + } + + const filepath = path.join(folderPath, filename); + fs.writeFileSync(filepath, file.buffer); + return filename; +}; + +export default handleUploadFile; diff --git a/public/uploads/level/audio/AUDIO-09fde9ab-bc8c-4d76-9e6c-f4f27a0f2878-6174deec821539477b29c0b1aa6ff375.mp3 b/public/uploads/level/audio/AUDIO-09fde9ab-bc8c-4d76-9e6c-f4f27a0f2878-6174deec821539477b29c0b1aa6ff375.mp3 new file mode 100644 index 0000000..03b707c Binary files /dev/null and b/public/uploads/level/audio/AUDIO-09fde9ab-bc8c-4d76-9e6c-f4f27a0f2878-6174deec821539477b29c0b1aa6ff375.mp3 differ diff --git a/public/uploads/level/image/IMAGE-09fde9ab-bc8c-4d76-9e6c-f4f27a0f2878-9b7abd31eb795a753c654509ff2357ea.jpeg b/public/uploads/level/image/IMAGE-09fde9ab-bc8c-4d76-9e6c-f4f27a0f2878-9b7abd31eb795a753c654509ff2357ea.jpeg new file mode 100644 index 0000000..39859e4 Binary files /dev/null and b/public/uploads/level/image/IMAGE-09fde9ab-bc8c-4d76-9e6c-f4f27a0f2878-9b7abd31eb795a753c654509ff2357ea.jpeg differ diff --git a/public/uploads/level/image/IMAGE-09fde9ab-bc8c-4d76-9e6c-f4f27a0f2878-ef42b968ff137775484cd8d9ce1397ef.jpeg b/public/uploads/level/image/IMAGE-09fde9ab-bc8c-4d76-9e6c-f4f27a0f2878-ef42b968ff137775484cd8d9ce1397ef.jpeg new file mode 100644 index 0000000..953f28d Binary files /dev/null and b/public/uploads/level/image/IMAGE-09fde9ab-bc8c-4d76-9e6c-f4f27a0f2878-ef42b968ff137775484cd8d9ce1397ef.jpeg differ diff --git a/routes/contents/level.js b/routes/contents/level.js index d7ca706..4a3796a 100644 --- a/routes/contents/level.js +++ b/routes/contents/level.js @@ -1,7 +1,8 @@ import express from "express"; -import { getLevels, getLevelById, getLevelForAdmin, getLevelsByTopicId, createLevel, updateLevelById, deleteLevelById, deleteLevelFileById, getPreviousLevel } from "../../controllers/contentControllers/level.js"; +import { getLevels, getLevelById, getLevelForAdmin, getLevelsByTopicId, createLevel, updateLevelById, deleteLevelById, deleteLevelFilesById, getPreviousLevel, uploadLevelFile, getLevelFiles } from "../../controllers/contentControllers/level.js"; import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js"; import handleUpload from '../../middlewares/Level/uploadLevel.js'; +import handleUploadFile from "../../middlewares/Level/uploadLevelFile.js"; import {checkLevelsPerTopic, autoCalculateRoutes, getSectionAndTopicByLevelId } from '../../middlewares/Level/checkLevel.js'; @@ -17,12 +18,18 @@ router.get("/level/:id", verifyLoginUser, getLevelById); router.get("/previous/level/:next_learning", verifyLoginUser, getPreviousLevel); +router.get("/level/file/:levelId", verifyLoginUser, getLevelFiles); + router.post("/level", verifyLoginUser, adminOnly, handleUpload, checkLevelsPerTopic, autoCalculateRoutes, createLevel); +router.post("/level/file/:levelId", verifyLoginUser, adminOnly, handleUploadFile, uploadLevelFile); + router.put("/level/:id", verifyLoginUser, adminOnly, handleUpload, getSectionAndTopicByLevelId, autoCalculateRoutes, updateLevelById); router.delete("/level/:id", verifyLoginUser, adminOnly, getSectionAndTopicByLevelId, deleteLevelById); -router.delete("/level/file/:id", verifyLoginUser, adminOnly, deleteLevelFileById); +// router.delete("/level/file/:id", verifyLoginUser, adminOnly, deleteLevelFileById); + +router.delete("/level/file/:id", verifyLoginUser, adminOnly, deleteLevelFilesById); export default router \ No newline at end of file