const path = require('path'); const fs = require('fs'); const sharp = require('sharp'); const jsonHelper = require('../utils/jsonHelper'); const imageResizePresets = { floatingContactBrandImage: { width: 104, height: 104, quality: 88 }, floatingContactTriggerIcon: { width: 96, height: 96, quality: 88 }, floatingContactActionIcon: { width: 84, height: 84, quality: 88 }, }; const isSvgFile = (filePath) => path.extname(filePath).toLowerCase() === '.svg'; function scheduleTemporaryFileCleanup(filePath, attemptsLeft = 5, delayMs = 250) { if (!filePath || attemptsLeft <= 0) { return; } setTimeout(() => { try { if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } } catch (cleanupError) { scheduleTemporaryFileCleanup(filePath, attemptsLeft - 1, delayMs * 2); } }, delayMs); } function getFinalUploadTarget(file, req, forceWebp = false) { const parsedPath = path.parse(file.path); const requestedFileName = req.uploadFinalFileName || file.filename.replace('.__upload__', ''); const parsedRequestedFileName = path.parse(requestedFileName); const finalFileName = forceWebp ? `${parsedRequestedFileName.name}.webp` : requestedFileName; const finalDirectory = req.uploadFinalDirectory || parsedPath.dir; return { finalFileName, finalPath: path.join(finalDirectory, finalFileName), }; } async function finalizeUploadedImage(file, req, resizePreset) { const preset = imageResizePresets[resizePreset]; if (!file) { return file; } if (!preset || isSvgFile(file.path)) { const { finalFileName, finalPath } = getFinalUploadTarget(file, req); if (path.resolve(file.path) !== path.resolve(finalPath)) { fs.renameSync(file.path, finalPath); } return { ...file, filename: finalFileName, path: finalPath, }; } const { finalFileName, finalPath } = getFinalUploadTarget(file, req, true); await sharp(file.path) .resize(preset.width, preset.height, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 }, withoutEnlargement: true, }) .webp({ quality: preset.quality }) .toFile(finalPath); if (fs.existsSync(file.path)) { try { fs.unlinkSync(file.path); } catch (cleanupError) { console.warn('Unable to remove original uploaded image after optimization:', cleanupError.message); scheduleTemporaryFileCleanup(file.path); } } return { ...file, filename: finalFileName, path: finalPath, mimetype: 'image/webp', }; } // Controller xử lý upload ảnh const uploadController = { // Upload ảnh và trả về đường dẫn uploadImage: async (req, res) => { try { if (!req.file) { return res.status(400).json({ success: false, error: 'No file was uploaded' }); } // Lấy loại ảnh từ query params const imageType = req.query.imageType || 'general'; const resizePreset = req.query.resizePreset || ''; req.file = await finalizeUploadedImage(req.file, req, resizePreset); // Tạo đường dẫn tương đối để lưu vào database const relativePath = `/uploads/${imageType}/${req.file.filename}`; const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`; const fullUrl = `${baseUrl}${relativePath}`; // Nếu client yêu cầu cập nhật trực tiếp file JSON (ví dụ activities.json), // thì đồng bộ camps.image và camps.camp-detail.hero.bgImage try { const jsonFile = req.body && req.body.jsonFile; const campLink = req.body && req.body.campLink; if (jsonFile && campLink) { // Đọc JSON và cập nhật camp có link khớp const jsonFilePath = require('path').join(__dirname, '../data', jsonFile); const jsonData = jsonHelper.readJsonFile(jsonFilePath); if (jsonData && Array.isArray(jsonData.camps)) { // campLink có thể được gửi không có dấu / đầu, chuẩn hóa const normalizedLink = campLink.startsWith('/') ? campLink : `/${campLink}`; const camp = jsonData.camps.find(c => (c.link || '').toString() === normalizedLink || (c.link || '').toString() === campLink); if (camp) { // Đồng bộ camps.image và camps.camp-detail.hero.bgImage - 2 trường này luôn giống nhau camp.image = relativePath; // Đảm bảo camp-detail.hero tồn tại và sync bgImage if (!camp['camp-detail']) camp['camp-detail'] = {}; if (!camp['camp-detail'].hero) camp['camp-detail'].hero = {}; camp['camp-detail'].hero.bgImage = relativePath; // Lưu thay đổi jsonHelper.writeJsonFile(jsonFilePath, jsonData); } } } } catch (e) { console.warn('Failed to update JSON file after upload:', e && e.message ? e.message : e); } return res.status(200).json({ success: true, path: relativePath, url: fullUrl, resizePreset: resizePreset || null, reused: false, message: 'File uploaded successfully' }); } catch (error) { console.error('Error uploading image:', error); return res.status(500).json({ success: false, error: 'Server error while uploading image' }); } }, // Cập nhật đường dẫn ảnh trong file JSON updateImagePath: async (req, res) => { try { const { jsonFile, jsonPath, newImagePath } = req.body; if (!jsonFile || !jsonPath || !newImagePath) { return res.status(400).json({ success: false, message: 'Missing required information (jsonFile, jsonPath, newImagePath)' }); } // Đọc file JSON const jsonFilePath = path.join(__dirname, '../data', jsonFile); const jsonData = jsonHelper.readJsonFile(jsonFilePath); // Cập nhật đường dẫn ảnh theo jsonPath // jsonPath có định dạng như "banner.image" hoặc "partners[0].logo" const pathParts = jsonPath.split('.'); let current = jsonData; // Duyệt qua các phần của path trừ phần cuối for (let i = 0; i < pathParts.length - 1; i++) { const part = pathParts[i]; // Kiểm tra nếu là mảng (ví dụ: partners[0]) if (part.includes('[') && part.includes(']')) { const arrName = part.substring(0, part.indexOf('[')); const index = parseInt(part.substring(part.indexOf('[') + 1, part.indexOf(']'))); if (!current[arrName] || !Array.isArray(current[arrName])) { return res.status(400).json({ success: false, message: `Array ${arrName} not found in data` }); } current = current[arrName][index]; } else { if (!current[part]) { return res.status(400).json({ success: false, message: `Property ${part} not found in data` }); } current = current[part]; } } // Cập nhật giá trị const lastPart = pathParts[pathParts.length - 1]; current[lastPart] = newImagePath; // Lưu lại file JSON jsonHelper.writeJsonFile(jsonFilePath, jsonData); return res.status(200).json({ success: true, message: 'Image path updated successfully', data: { jsonPath, newImagePath } }); } catch (error) { console.error('Error updating image path:', error); return res.status(500).json({ success: false, message: 'Server error while updating image path' }); } }, // Xóa ảnh deleteImage: async (req, res) => { try { const { imagePath } = req.body; if (!imagePath) { return res.status(400).json({ success: false, message: 'Missing image path to delete' }); } // Chuyển đổi đường dẫn tương đối thành đường dẫn tuyệt đối const fullPath = path.join(__dirname, '../public', imagePath); // Kiểm tra xem file có tồn tại không if (!fs.existsSync(fullPath)) { return res.status(404).json({ success: false, message: 'Image file not found' }); } // Xóa file fs.unlinkSync(fullPath); return res.status(200).json({ success: true, message: 'Image deleted successfully', data: { imagePath } }); } catch (error) { console.error('Error deleting image:', error); return res.status(500).json({ success: false, message: 'Server error while deleting image' }); } }, // List images in a folder listImages: async (req, res) => { try { const imageType = req.query.imageType || 'general'; const dirPath = path.join(__dirname, '../public/uploads', imageType); if (!fs.existsSync(dirPath)) { return res.status(200).json({ success: true, images: [] }); } const files = fs.readdirSync(dirPath).filter(f => !f.startsWith('.')); const images = files.map(name => ({ name, path: `/uploads/${imageType}/${name}`, url: (process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`) + `/uploads/${imageType}/${name}` })); return res.status(200).json({ success: true, images }); } catch (error) { console.error('Error listing images:', error); return res.status(500).json({ success: false, error: 'Server error while listing images' }); } }, // Upload video uploadVideo: async (req, res) => { try { if (!req.file) { return res.status(400).json({ success: false, error: 'No file was uploaded' }); } // Kiểm tra loại file const fileType = req.file.mimetype; if (!fileType.startsWith('video/')) { // Xóa file nếu không phải video fs.unlinkSync(req.file.path); return res.status(400).json({ success: false, error: 'Uploaded file is not a video' }); } // Tạo đường dẫn tương đối để lưu vào database const relativePath = `/uploads/videos/${req.file.filename}`; const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`; const fullUrl = `${baseUrl}${relativePath}`; return res.status(200).json({ success: true, path: relativePath, url: fullUrl }); } catch (error) { console.error('Error uploading video:', error); return res.status(500).json({ success: false, error: 'Server error while uploading video' }); } } }; module.exports = uploadController;