const express = require('express'); const router = express.Router(); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const sharp = require('sharp'); const adminAuth = require('../middleware/adminAuth'); // --- Helpers --- const getUploadsDir = () => { const envDir = (process.env.UPLOADS_DIR || '').trim(); if (envDir) return path.isAbsolute(envDir) ? envDir : path.resolve(process.cwd(), envDir); return path.resolve(__dirname, '..', 'uploads'); }; const uploadsDir = getUploadsDir(); const urlPrefix = (process.env.UPLOADS_URL_PREFIX || '/uploads').replace(/\/$/, ''); // Ensure uploads directory exists if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } // Configure multer for file upload const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, uploadsDir), filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); const ext = path.extname(file.originalname).toLowerCase(); const name = path .basename(file.originalname, ext) .replace(/[^a-z0-9]/gi, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, '') .toLowerCase(); cb(null, `${name || 'file'}-${uniqueSuffix}${ext || ''}`); }, }); const fileFilter = (req, file, cb) => { // Accept images only if (file.mimetype && file.mimetype.startsWith('image/')) return cb(null, true); cb(new Error('Only image files are allowed'), false); }; const upload = multer({ storage, fileFilter, limits: { fileSize: 10 * 1024 * 1024 }, // 10MB }); async function optimizeImageIfPossible(filePath, extLower) { if (extLower === '.gif') return; const tmpPath = `${filePath}.tmp`; let pipeline = sharp(filePath).resize(1920, 1080, { fit: 'inside', withoutEnlargement: true, }); if (extLower === '.png') { pipeline = pipeline.png({ quality: 85, compressionLevel: 9 }); } else if (extLower === '.webp') { pipeline = pipeline.webp({ quality: 85 }); } else { pipeline = pipeline.jpeg({ quality: 85 }); } await pipeline.toFile(tmpPath); fs.unlinkSync(filePath); fs.renameSync(tmpPath, filePath); } // @route POST /api/upload // @desc Upload single or multiple images // @access Private (Admin) router.post('/', adminAuth, upload.array('images', 10), async (req, res) => { try { if (!req.files || req.files.length === 0) { return res.status(400).json({ success: false, message: 'No files uploaded' }); } const uploadedFiles = []; for (const file of req.files) { try { const extLower = path.extname(file.filename).toLowerCase(); await optimizeImageIfPossible(file.path, extLower); uploadedFiles.push({ filename: file.filename, originalName: file.originalname, url: `${urlPrefix}/${file.filename}`, mimeType: file.mimetype, uploadedAt: new Date(), }); } catch (e) { console.error('Error processing image:', e); uploadedFiles.push({ filename: file.filename, originalName: file.originalname, url: `${urlPrefix}/${file.filename}`, mimeType: file.mimetype, uploadedAt: new Date(), error: 'Optimization failed', }); } } res.json({ success: true, message: 'Files uploaded successfully', data: { files: uploadedFiles }, }); } catch (error) { console.error('Upload error:', error); res.status(500).json({ success: false, message: error.message || 'Error uploading files' }); } }); // @route GET /api/upload/list // @desc Get list of uploaded files // @access Private (Admin) router.get('/list', adminAuth, async (req, res) => { try { const files = fs.readdirSync(uploadsDir); const fileList = files .filter((f) => !f.startsWith('.')) .map((filename) => { const filePath = path.join(uploadsDir, filename); const stats = fs.statSync(filePath); return { filename, url: `${urlPrefix}/${filename}`, size: stats.size, uploadedAt: stats.mtime, }; }) .sort((a, b) => b.uploadedAt - a.uploadedAt); res.json({ success: true, data: { files: fileList, total: fileList.length } }); } catch (error) { console.error('List files error:', error); res.status(500).json({ success: false, message: 'Error listing files' }); } }); // @route DELETE /api/upload/:filename // @desc Delete an uploaded file // @access Private (Admin) router.delete('/:filename', adminAuth, async (req, res) => { try { const { filename } = req.params; // Security check: prevent path traversal if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) { return res.status(400).json({ success: false, message: 'Invalid filename' }); } const filePath = path.join(uploadsDir, filename); if (!fs.existsSync(filePath)) { return res.status(404).json({ success: false, message: 'File not found' }); } fs.unlinkSync(filePath); res.json({ success: true, message: 'File deleted successfully' }); } catch (error) { console.error('Delete file error:', error); res.status(500).json({ success: false, message: 'Error deleting file' }); } }); module.exports = router;