edit media
This commit is contained in:
155
routes/upload.js
155
routes/upload.js
@@ -6,40 +6,49 @@ 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 isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
const uploadsDir = getUploadsDir();
|
||||
const urlPrefix = (process.env.UPLOADS_URL_PREFIX || '/uploads').replace(/\/$/, '');
|
||||
// In dev: oldvine_cms/client/public
|
||||
const devPublicRoot = path.join(__dirname, '../../client/public');
|
||||
|
||||
// On server: nginx serves /uploads from /var/www/oldvine_uploads (recommended)
|
||||
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, '');
|
||||
|
||||
// Pick output format so extension matches actual bytes (important!)
|
||||
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 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 base = safeBaseName(file.originalname);
|
||||
const { ext } = pickOutput(file.mimetype);
|
||||
cb(null, `${base || 'upload'}-${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);
|
||||
};
|
||||
@@ -50,29 +59,6 @@ const upload = multer({
|
||||
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)
|
||||
@@ -82,38 +68,50 @@ router.post('/', adminAuth, upload.array('images', 10), async (req, res) => {
|
||||
return res.status(400).json({ success: false, message: 'No files uploaded' });
|
||||
}
|
||||
|
||||
const uploadedFiles = [];
|
||||
const uploadedFiles = await Promise.all(
|
||||
req.files.map(async (file) => {
|
||||
try {
|
||||
const tmpPath = `${file.path}.opt`;
|
||||
const { fmt } = pickOutput(file.mimetype);
|
||||
|
||||
for (const file of req.files) {
|
||||
try {
|
||||
const extLower = path.extname(file.filename).toLowerCase();
|
||||
await optimizeImageIfPossible(file.path, extLower);
|
||||
let img = sharp(file.path).resize(1920, 1080, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true,
|
||||
});
|
||||
|
||||
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',
|
||||
});
|
||||
}
|
||||
}
|
||||
if (fmt === 'png') img = img.png({ compressionLevel: 9 });
|
||||
else if (fmt === 'webp') img = img.webp({ quality: 85 });
|
||||
else img = img.jpeg({ quality: 85 });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Files uploaded successfully',
|
||||
data: { files: uploadedFiles },
|
||||
});
|
||||
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' });
|
||||
@@ -121,8 +119,6 @@ router.post('/', adminAuth, upload.array('images', 10), async (req, res) => {
|
||||
});
|
||||
|
||||
// @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);
|
||||
@@ -132,12 +128,7 @@ router.get('/list', adminAuth, async (req, res) => {
|
||||
.map((filename) => {
|
||||
const filePath = path.join(uploadsDir, filename);
|
||||
const stats = fs.statSync(filePath);
|
||||
return {
|
||||
filename,
|
||||
url: `${urlPrefix}/${filename}`,
|
||||
size: stats.size,
|
||||
uploadedAt: stats.mtime,
|
||||
};
|
||||
return { filename, url: `/uploads/${filename}`, size: stats.size, uploadedAt: stats.mtime };
|
||||
})
|
||||
.sort((a, b) => b.uploadedAt - a.uploadedAt);
|
||||
|
||||
@@ -149,22 +140,16 @@ router.get('/list', adminAuth, async (req, res) => {
|
||||
});
|
||||
|
||||
// @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' });
|
||||
}
|
||||
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' });
|
||||
|
||||
Reference in New Issue
Block a user