Files
oldvine_cms/routes/upload.js

161 lines
5.2 KiB
JavaScript

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');
const isProd = process.env.NODE_ENV === 'production';
// In dev: oldvine_cms/client/public
const devPublicRoot = path.join(__dirname, '../../client/public');
// On server: nginx serves /uploads from /var/www/oldvine/uploads (matches current nginx config)
const uploadsDir =
process.env.UPLOADS_DIR ||
(isProd ? '/var/www/oldvine/uploads' : path.join(devPublicRoot, 'uploads'));
// Ensure uploads directory exists
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
const safeBaseName = (name) =>
String(name || 'file')
.toLowerCase()
.replace(/\.[^/.]+$/, '') // remove extension
.replace(/[^a-z0-9]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
const pickOutput = (mimetype) => {
if (mimetype === 'image/png') return { ext: '.png', fmt: 'png' };
if (mimetype === 'image/webp') return { ext: '.webp', fmt: 'webp' };
// default jpeg for jpg/jpeg/gif/others
return { ext: '.jpg', fmt: 'jpeg' };
};
// 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 base = safeBaseName(file.originalname);
const { ext } = pickOutput(file.mimetype);
cb(null, `${base || 'upload'}-${uniqueSuffix}${ext}`);
},
});
const fileFilter = (req, file, cb) => {
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
});
// @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 = await Promise.all(
req.files.map(async (file) => {
try {
const tmpPath = `${file.path}.opt`;
const { fmt } = pickOutput(file.mimetype);
let img = sharp(file.path).resize(1920, 1080, {
fit: 'inside',
withoutEnlargement: true,
});
if (fmt === 'png') img = img.png({ compressionLevel: 9 });
else if (fmt === 'webp') img = img.webp({ quality: 85 });
else img = img.jpeg({ quality: 85 });
await img.toFile(tmpPath);
fs.unlinkSync(file.path);
fs.renameSync(tmpPath, file.path);
return {
filename: file.filename,
originalName: file.originalname,
url: `/uploads/${file.filename}`,
size: fs.statSync(file.path).size,
mimeType: file.mimetype,
uploadedAt: new Date(),
};
} catch (err) {
console.error('Error processing image:', err);
return {
filename: file.filename,
originalName: file.originalname,
url: `/uploads/${file.filename}`,
size: file.size,
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
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: `/uploads/${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
router.delete('/:filename', adminAuth, async (req, res) => {
try {
const { filename } = req.params;
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;