const express = require('express'); const router = express.Router(); const multer = require('multer'); const sharp = require('sharp'); const path = require('path'); const fs = require('fs').promises; // Authentication middleware const requireAuth = (req, res, next) => { if (!req.session.user) { return res.status(401).json({ success: false, message: 'Authentication required' }); } next(); }; // Configure multer for file uploads const storage = multer.diskStorage({ destination: async (req, file, cb) => { const uploadPath = path.join(__dirname, '../public/uploads'); try { await fs.mkdir(uploadPath, { recursive: true }); cb(null, uploadPath); } catch (error) { cb(error); } }, filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); const ext = path.extname(file.originalname); cb(null, file.fieldname + '-' + uniqueSuffix + ext); } }); const upload = multer({ storage: storage, limits: { fileSize: parseInt(process.env.MAX_FILE_SIZE) || 10 * 1024 * 1024, // 10MB files: 10 }, fileFilter: (req, file, cb) => { // Allow images only if (file.mimetype.startsWith('image/')) { cb(null, true); } else { cb(new Error('Only image files are allowed'), false); } } }); // Upload single image router.post('/upload', requireAuth, upload.single('image'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ success: false, message: 'No file uploaded' }); } const originalPath = req.file.path; const filename = req.file.filename; const nameWithoutExt = path.parse(filename).name; // Create optimized versions const sizes = { thumbnail: { width: 300, height: 200 }, medium: { width: 800, height: 600 }, large: { width: 1200, height: 900 } }; const optimizedImages = {}; for (const [sizeName, dimensions] of Object.entries(sizes)) { const outputPath = path.join( path.dirname(originalPath), `${nameWithoutExt}-${sizeName}.webp` ); await sharp(originalPath) .resize(dimensions.width, dimensions.height, { fit: 'inside', withoutEnlargement: true }) .webp({ quality: 85 }) .toFile(outputPath); optimizedImages[sizeName] = `/uploads/${path.basename(outputPath)}`; } // Keep original as well (converted to webp for better compression) const originalWebpPath = path.join( path.dirname(originalPath), `${nameWithoutExt}-original.webp` ); await sharp(originalPath) .webp({ quality: 90 }) .toFile(originalWebpPath); optimizedImages.original = `/uploads/${path.basename(originalWebpPath)}`; // Remove the original uploaded file await fs.unlink(originalPath); res.json({ success: true, message: 'Image uploaded and optimized successfully', images: optimizedImages, metadata: { originalName: req.file.originalname, mimeType: req.file.mimetype, size: req.file.size } }); } catch (error) { console.error('Image upload error:', error); // Clean up files on error if (req.file && req.file.path) { try { await fs.unlink(req.file.path); } catch (unlinkError) { console.error('Error removing file:', unlinkError); } } res.status(500).json({ success: false, message: 'Error uploading image' }); } }); // Upload multiple images router.post('/upload-multiple', requireAuth, 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 uploadedImages = []; for (const file of req.files) { try { const originalPath = file.path; const filename = file.filename; const nameWithoutExt = path.parse(filename).name; // Create optimized versions const sizes = { thumbnail: { width: 300, height: 200 }, medium: { width: 800, height: 600 }, large: { width: 1200, height: 900 } }; const optimizedImages = {}; for (const [sizeName, dimensions] of Object.entries(sizes)) { const outputPath = path.join( path.dirname(originalPath), `${nameWithoutExt}-${sizeName}.webp` ); await sharp(originalPath) .resize(dimensions.width, dimensions.height, { fit: 'inside', withoutEnlargement: true }) .webp({ quality: 85 }) .toFile(outputPath); optimizedImages[sizeName] = `/uploads/${path.basename(outputPath)}`; } // Original as webp const originalWebpPath = path.join( path.dirname(originalPath), `${nameWithoutExt}-original.webp` ); await sharp(originalPath) .webp({ quality: 90 }) .toFile(originalWebpPath); optimizedImages.original = `/uploads/${path.basename(originalWebpPath)}`; uploadedImages.push({ originalName: file.originalname, images: optimizedImages, metadata: { mimeType: file.mimetype, size: file.size } }); // Remove original file await fs.unlink(originalPath); } catch (fileError) { console.error(`Error processing file ${file.originalname}:`, fileError); // Clean up this file and continue with others try { await fs.unlink(file.path); } catch (unlinkError) { console.error('Error removing file:', unlinkError); } } } res.json({ success: true, message: `${uploadedImages.length} images uploaded and optimized successfully`, images: uploadedImages }); } catch (error) { console.error('Multiple images upload error:', error); // Clean up files on error if (req.files) { for (const file of req.files) { try { await fs.unlink(file.path); } catch (unlinkError) { console.error('Error removing file:', unlinkError); } } } res.status(500).json({ success: false, message: 'Error uploading images' }); } }); // Delete image router.delete('/:filename', requireAuth, async (req, res) => { try { const filename = req.params.filename; const uploadPath = path.join(__dirname, '../public/uploads'); // Security check - ensure filename doesn't contain path traversal if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) { return res.status(400).json({ success: false, message: 'Invalid filename' }); } const filePath = path.join(uploadPath, filename); try { await fs.access(filePath); await fs.unlink(filePath); res.json({ success: true, message: 'Image deleted successfully' }); } catch (error) { if (error.code === 'ENOENT') { return res.status(404).json({ success: false, message: 'Image not found' }); } throw error; } } catch (error) { console.error('Image deletion error:', error); res.status(500).json({ success: false, message: 'Error deleting image' }); } }); // List uploaded images router.get('/list', requireAuth, async (req, res) => { try { const uploadPath = path.join(__dirname, '../public/uploads'); const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 20; const files = await fs.readdir(uploadPath); const imageFiles = files.filter(file => /\.(jpg|jpeg|png|gif|webp)$/i.test(file) ); const total = imageFiles.length; const totalPages = Math.ceil(total / limit); const start = (page - 1) * limit; const end = start + limit; const paginatedFiles = imageFiles.slice(start, end); const imagesWithStats = await Promise.all( paginatedFiles.map(async (file) => { try { const filePath = path.join(uploadPath, file); const stats = await fs.stat(filePath); return { filename: file, url: `/uploads/${file}`, size: stats.size, modified: stats.mtime, isImage: true }; } catch (error) { console.error(`Error getting stats for ${file}:`, error); return null; } }) ); const validImages = imagesWithStats.filter(img => img !== null); res.json({ success: true, images: validImages, pagination: { current: page, total: totalPages, limit, totalItems: total, hasNext: page < totalPages, hasPrev: page > 1 } }); } catch (error) { console.error('List images error:', error); res.status(500).json({ success: false, message: 'Error listing images' }); } }); module.exports = router;