forked from UKSOURCE/cms.hailearning.edu.vn
324 lines
11 KiB
JavaScript
324 lines
11 KiB
JavaScript
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);
|
|
const finalPathMatchesInput = path.resolve(file.path) === path.resolve(finalPath);
|
|
|
|
const sourceBuffer = fs.readFileSync(file.path);
|
|
const optimizedBuffer = await sharp(sourceBuffer)
|
|
.resize(preset.width, preset.height, {
|
|
fit: 'contain',
|
|
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
withoutEnlargement: true,
|
|
})
|
|
.webp({ quality: preset.quality })
|
|
.toBuffer();
|
|
|
|
fs.writeFileSync(finalPath, optimizedBuffer);
|
|
|
|
if (!finalPathMatchesInput && 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;
|