edit upload & media file
This commit is contained in:
168
routes/media.js
168
routes/media.js
@@ -2,10 +2,22 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const fsNative = require('fs');
|
||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
|
|
||||||
const Media = require('../models/Media');
|
const Media = require('../models/Media');
|
||||||
const adminAuth = require('../middleware/adminAuth');
|
const adminAuth = require('../middleware/adminAuth');
|
||||||
|
|
||||||
|
// =======================
|
||||||
|
// Config (LOCAL + PROD)
|
||||||
|
// =======================
|
||||||
|
const UPLOAD_DIR = process.env.UPLOAD_DIR
|
||||||
|
? path.resolve(process.env.UPLOAD_DIR)
|
||||||
|
: path.resolve(__dirname, '..', 'uploads'); // local fallback
|
||||||
|
|
||||||
|
const PUBLIC_BASE = process.env.UPLOAD_PUBLIC_URL || '/uploads';
|
||||||
|
const MEDIA_ROOT = process.env.MEDIA_SUBDIR || 'images';
|
||||||
|
|
||||||
// Ensure upload directories exist
|
// Ensure upload directories exist
|
||||||
const ensureUploadDir = async (dir) => {
|
const ensureUploadDir = async (dir) => {
|
||||||
try {
|
try {
|
||||||
@@ -15,24 +27,39 @@ const ensureUploadDir = async (dir) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sanitizeFolder = (folder) => {
|
||||||
|
let f = String(folder || 'general').trim();
|
||||||
|
// allow a-z0-9 _ - and slashes for nested folders
|
||||||
|
f = f.replace(/[^a-z0-9/_-]/gi, '');
|
||||||
|
// prevent traversal
|
||||||
|
while (f.includes('..')) f = f.replace(/\.\./g, '');
|
||||||
|
f = f.replace(/^\/+|\/+$/g, '');
|
||||||
|
return f || 'general';
|
||||||
|
};
|
||||||
|
|
||||||
// Configure multer storage
|
// Configure multer storage
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: async (req, file, cb) => {
|
destination: async (req, file, cb) => {
|
||||||
const folder = req.body.folder || 'general';
|
try {
|
||||||
const uploadPath = path.join(__dirname, '../../client/public/images', folder);
|
const folderSafe = sanitizeFolder(req.body.folder);
|
||||||
await ensureUploadDir(uploadPath);
|
const uploadPath = path.join(UPLOAD_DIR, MEDIA_ROOT, folderSafe);
|
||||||
cb(null, uploadPath);
|
await ensureUploadDir(uploadPath);
|
||||||
|
cb(null, uploadPath);
|
||||||
|
} catch (e) {
|
||||||
|
cb(e);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
filename: (req, file, cb) => {
|
filename: (req, file, cb) => {
|
||||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||||
const ext = path.extname(file.originalname);
|
const ext = path.extname(file.originalname);
|
||||||
const basename = path.basename(file.originalname, ext)
|
const basename = path
|
||||||
|
.basename(file.originalname, ext)
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9]/g, '-')
|
.replace(/[^a-z0-9]/g, '-')
|
||||||
.replace(/-+/g, '-')
|
.replace(/-+/g, '-')
|
||||||
.replace(/^-|-$/g, '');
|
.replace(/^-|-$/g, '');
|
||||||
cb(null, basename + '-' + uniqueSuffix + ext);
|
cb(null, (basename || 'file') + '-' + uniqueSuffix + ext.toLowerCase());
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// File filter
|
// File filter
|
||||||
@@ -46,7 +73,7 @@ const fileFilter = (req, file, cb) => {
|
|||||||
'image/svg+xml',
|
'image/svg+xml',
|
||||||
'video/mp4',
|
'video/mp4',
|
||||||
'video/quicktime',
|
'video/quicktime',
|
||||||
'application/pdf'
|
'application/pdf',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (allowedMimeTypes.includes(file.mimetype)) {
|
if (allowedMimeTypes.includes(file.mimetype)) {
|
||||||
@@ -60,8 +87,8 @@ const upload = multer({
|
|||||||
storage,
|
storage,
|
||||||
fileFilter,
|
fileFilter,
|
||||||
limits: {
|
limits: {
|
||||||
fileSize: 10 * 1024 * 1024 // 10MB limit
|
fileSize: 10 * 1024 * 1024, // 10MB limit
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// @route POST /api/media/upload
|
// @route POST /api/media/upload
|
||||||
@@ -72,11 +99,12 @@ router.post('/upload', adminAuth, upload.single('file'), async (req, res) => {
|
|||||||
if (!req.file) {
|
if (!req.file) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'No file uploaded'
|
message: 'No file uploaded',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { folder = 'general', alt, caption, description, tags } = req.body;
|
const folderSafe = sanitizeFolder(req.body.folder);
|
||||||
|
const { alt, caption, description, tags } = req.body;
|
||||||
|
|
||||||
// Determine media type
|
// Determine media type
|
||||||
let mediaType = 'other';
|
let mediaType = 'other';
|
||||||
@@ -88,20 +116,22 @@ router.post('/upload', adminAuth, upload.single('file'), async (req, res) => {
|
|||||||
mediaType = 'document';
|
mediaType = 'document';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const url = `${PUBLIC_BASE}/${MEDIA_ROOT}/${folderSafe}/${req.file.filename}`;
|
||||||
|
|
||||||
// Create media record
|
// Create media record
|
||||||
const media = new Media({
|
const media = new Media({
|
||||||
filename: req.file.filename,
|
filename: req.file.filename,
|
||||||
originalName: req.file.originalname,
|
originalName: req.file.originalname,
|
||||||
url: `/images/${folder}/${req.file.filename}`,
|
url,
|
||||||
mimeType: req.file.mimetype,
|
mimeType: req.file.mimetype,
|
||||||
size: req.file.size,
|
size: req.file.size,
|
||||||
type: mediaType,
|
type: mediaType,
|
||||||
folder,
|
folder: folderSafe,
|
||||||
alt,
|
alt,
|
||||||
caption,
|
caption,
|
||||||
description,
|
description,
|
||||||
tags: tags ? tags.split(',').map(t => t.trim()) : [],
|
tags: tags ? tags.split(',').map((t) => t.trim()) : [],
|
||||||
uploadedBy: req.admin.id
|
uploadedBy: req.admin.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
await media.save();
|
await media.save();
|
||||||
@@ -109,13 +139,13 @@ router.post('/upload', adminAuth, upload.single('file'), async (req, res) => {
|
|||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'File uploaded successfully',
|
message: 'File uploaded successfully',
|
||||||
data: { media }
|
data: { media },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload error:', error);
|
console.error('Upload error:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: error.message || 'Error uploading file'
|
message: error.message || 'Error uploading file',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -128,11 +158,11 @@ router.post('/upload-multiple', adminAuth, upload.array('files', 10), async (req
|
|||||||
if (!req.files || req.files.length === 0) {
|
if (!req.files || req.files.length === 0) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'No files uploaded'
|
message: 'No files uploaded',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { folder = 'general' } = req.body;
|
const folderSafe = sanitizeFolder(req.body.folder);
|
||||||
const mediaRecords = [];
|
const mediaRecords = [];
|
||||||
|
|
||||||
for (const file of req.files) {
|
for (const file of req.files) {
|
||||||
@@ -145,15 +175,17 @@ router.post('/upload-multiple', adminAuth, upload.array('files', 10), async (req
|
|||||||
mediaType = 'document';
|
mediaType = 'document';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const url = `${PUBLIC_BASE}/${MEDIA_ROOT}/${folderSafe}/${file.filename}`;
|
||||||
|
|
||||||
const media = new Media({
|
const media = new Media({
|
||||||
filename: file.filename,
|
filename: file.filename,
|
||||||
originalName: file.originalname,
|
originalName: file.originalname,
|
||||||
url: `/images/${folder}/${file.filename}`,
|
url,
|
||||||
mimeType: file.mimetype,
|
mimeType: file.mimetype,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
type: mediaType,
|
type: mediaType,
|
||||||
folder,
|
folder: folderSafe,
|
||||||
uploadedBy: req.admin.id
|
uploadedBy: req.admin.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
await media.save();
|
await media.save();
|
||||||
@@ -163,13 +195,13 @@ router.post('/upload-multiple', adminAuth, upload.array('files', 10), async (req
|
|||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `${mediaRecords.length} files uploaded successfully`,
|
message: `${mediaRecords.length} files uploaded successfully`,
|
||||||
data: { media: mediaRecords }
|
data: { media: mediaRecords },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Multiple upload error:', error);
|
console.error('Multiple upload error:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: error.message || 'Error uploading files'
|
message: error.message || 'Error uploading files',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -190,7 +222,7 @@ router.get('/', adminAuth, async (req, res) => {
|
|||||||
.populate('uploadedBy', 'firstName lastName')
|
.populate('uploadedBy', 'firstName lastName')
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
.skip(skip)
|
.skip(skip)
|
||||||
.limit(parseInt(limit));
|
.limit(parseInt(limit, 10));
|
||||||
|
|
||||||
const total = await Media.countDocuments(query);
|
const total = await Media.countDocuments(query);
|
||||||
|
|
||||||
@@ -199,18 +231,18 @@ router.get('/', adminAuth, async (req, res) => {
|
|||||||
data: {
|
data: {
|
||||||
media,
|
media,
|
||||||
pagination: {
|
pagination: {
|
||||||
page: parseInt(page),
|
page: parseInt(page, 10),
|
||||||
limit: parseInt(limit),
|
limit: parseInt(limit, 10),
|
||||||
total,
|
total,
|
||||||
pages: Math.ceil(total / limit)
|
pages: Math.ceil(total / limit),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Get media error:', error);
|
console.error('Get media error:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Error fetching media'
|
message: 'Error fetching media',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -225,21 +257,21 @@ router.get('/search', adminAuth, async (req, res) => {
|
|||||||
if (!q) {
|
if (!q) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Search query is required'
|
message: 'Search query is required',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const media = await Media.search(q, { folder, type, limit: parseInt(limit) });
|
const media = await Media.search(q, { folder, type, limit: parseInt(limit, 10) });
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: { media }
|
data: { media },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Search media error:', error);
|
console.error('Search media error:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Error searching media'
|
message: 'Error searching media',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -253,39 +285,42 @@ router.put('/:id', adminAuth, async (req, res) => {
|
|||||||
const { alt, caption, description, tags, folder } = req.body;
|
const { alt, caption, description, tags, folder } = req.body;
|
||||||
|
|
||||||
const updateFields = {};
|
const updateFields = {};
|
||||||
if (alt) updateFields.alt = alt;
|
if (alt !== undefined) updateFields.alt = alt;
|
||||||
if (caption) updateFields.caption = caption;
|
if (caption !== undefined) updateFields.caption = caption;
|
||||||
if (description) updateFields.description = description;
|
if (description !== undefined) updateFields.description = description;
|
||||||
if (tags) updateFields.tags = tags;
|
if (tags !== undefined) updateFields.tags = tags;
|
||||||
if (folder) updateFields.folder = folder;
|
if (folder !== undefined) updateFields.folder = sanitizeFolder(folder);
|
||||||
|
|
||||||
const media = await Media.findByIdAndUpdate(
|
const media = await Media.findByIdAndUpdate(id, { $set: updateFields }, { new: true, runValidators: true });
|
||||||
id,
|
|
||||||
{ $set: updateFields },
|
|
||||||
{ new: true, runValidators: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!media) {
|
if (!media) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Media not found'
|
message: 'Media not found',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Media updated successfully',
|
message: 'Media updated successfully',
|
||||||
data: { media }
|
data: { media },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Update media error:', error);
|
console.error('Update media error:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Error updating media'
|
message: 'Error updating media',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const safeResolveInside = (base, rel) => {
|
||||||
|
const baseAbs = path.resolve(base);
|
||||||
|
const targetAbs = path.resolve(baseAbs, rel);
|
||||||
|
if (!targetAbs.startsWith(baseAbs + path.sep)) return null;
|
||||||
|
return targetAbs;
|
||||||
|
};
|
||||||
|
|
||||||
// @route DELETE /api/media/:id
|
// @route DELETE /api/media/:id
|
||||||
// @desc Delete media file
|
// @desc Delete media file
|
||||||
// @access Private (Admin)
|
// @access Private (Admin)
|
||||||
@@ -298,30 +333,42 @@ router.delete('/:id', adminAuth, async (req, res) => {
|
|||||||
if (!media) {
|
if (!media) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Media not found'
|
message: 'Media not found',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete file from filesystem
|
// Delete file from filesystem (only if inside UPLOAD_DIR)
|
||||||
const filePath = path.join(__dirname, '../../client/public', media.url);
|
|
||||||
try {
|
try {
|
||||||
await fs.unlink(filePath);
|
const url = String(media.url || '');
|
||||||
|
let rel = null;
|
||||||
|
|
||||||
|
if (url.startsWith(PUBLIC_BASE + '/')) {
|
||||||
|
rel = url.slice((PUBLIC_BASE + '/').length);
|
||||||
|
} else if (url.startsWith('/uploads/')) {
|
||||||
|
rel = url.slice('/uploads/'.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rel) {
|
||||||
|
const filePath = safeResolveInside(UPLOAD_DIR, rel);
|
||||||
|
if (filePath && fsNative.existsSync(filePath)) {
|
||||||
|
await fs.unlink(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error deleting file:', err);
|
console.error('Error deleting file:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete media record
|
|
||||||
await Media.findByIdAndDelete(id);
|
await Media.findByIdAndDelete(id);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Media deleted successfully'
|
message: 'Media deleted successfully',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Delete media error:', error);
|
console.error('Delete media error:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Error deleting media'
|
message: 'Error deleting media',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -335,16 +382,15 @@ router.get('/folders/list', adminAuth, async (req, res) => {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: { folders }
|
data: { folders },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Get folders error:', error);
|
console.error('Get folders error:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Error fetching folders'
|
message: 'Error fetching folders',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
141
routes/upload.js
141
routes/upload.js
@@ -6,29 +6,42 @@ const fs = require('fs');
|
|||||||
const sharp = require('sharp');
|
const sharp = require('sharp');
|
||||||
const adminAuth = require('../middleware/adminAuth');
|
const adminAuth = require('../middleware/adminAuth');
|
||||||
|
|
||||||
|
// =======================
|
||||||
|
// Config (LOCAL + PROD)
|
||||||
|
// =======================
|
||||||
|
const UPLOAD_DIR = process.env.UPLOAD_DIR
|
||||||
|
? path.resolve(process.env.UPLOAD_DIR)
|
||||||
|
: path.resolve(__dirname, '..', 'uploads'); // local fallback
|
||||||
|
|
||||||
|
const PUBLIC_BASE = process.env.UPLOAD_PUBLIC_URL || '/uploads';
|
||||||
|
|
||||||
// Ensure uploads directory exists
|
// Ensure uploads directory exists
|
||||||
const uploadsDir = path.join(__dirname, '../../client/public/uploads');
|
if (!fs.existsSync(UPLOAD_DIR)) {
|
||||||
if (!fs.existsSync(uploadsDir)) {
|
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
||||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure multer for file upload
|
// Configure multer for file upload
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => {
|
destination: (req, file, cb) => {
|
||||||
cb(null, uploadsDir);
|
cb(null, UPLOAD_DIR);
|
||||||
},
|
},
|
||||||
filename: (req, file, cb) => {
|
filename: (req, file, cb) => {
|
||||||
// Generate unique filename
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
const ext = path.extname(file.originalname).toLowerCase() || '.jpg';
|
||||||
const ext = path.extname(file.originalname);
|
const name = path
|
||||||
const name = path.basename(file.originalname, ext).replace(/[^a-z0-9]/gi, '-').toLowerCase();
|
.basename(file.originalname, ext)
|
||||||
cb(null, `${name}-${uniqueSuffix}${ext}`);
|
.replace(/[^a-z0-9]/gi, '-')
|
||||||
}
|
.toLowerCase()
|
||||||
|
.replace(/-+/g, '-')
|
||||||
|
.replace(/(^-|-$)/g, '');
|
||||||
|
|
||||||
|
cb(null, `${name || 'image'}-${uniqueSuffix}${ext}`);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileFilter = (req, file, cb) => {
|
const fileFilter = (req, file, cb) => {
|
||||||
// Accept images only
|
// Accept images only
|
||||||
if (file.mimetype.startsWith('image/')) {
|
if (file.mimetype && file.mimetype.startsWith('image/')) {
|
||||||
cb(null, true);
|
cb(null, true);
|
||||||
} else {
|
} else {
|
||||||
cb(new Error('Only image files are allowed'), false);
|
cb(new Error('Only image files are allowed'), false);
|
||||||
@@ -36,13 +49,40 @@ const fileFilter = (req, file, cb) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const upload = multer({
|
const upload = multer({
|
||||||
storage: storage,
|
storage,
|
||||||
fileFilter: fileFilter,
|
fileFilter,
|
||||||
limits: {
|
limits: {
|
||||||
fileSize: 10 * 1024 * 1024 // 10MB limit
|
fileSize: 10 * 1024 * 1024, // 10MB limit
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const optimizeImageInPlace = async (filePath, ext) => {
|
||||||
|
const lowerExt = (ext || '').toLowerCase();
|
||||||
|
|
||||||
|
// GIF: skip optimization (sharp may drop animation)
|
||||||
|
if (lowerExt === '.gif') return;
|
||||||
|
|
||||||
|
const tmpPath = filePath + '.tmp';
|
||||||
|
|
||||||
|
const pipeline = sharp(filePath)
|
||||||
|
.rotate()
|
||||||
|
.resize(1920, 1080, {
|
||||||
|
fit: 'inside',
|
||||||
|
withoutEnlargement: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lowerExt === '.png') {
|
||||||
|
await pipeline.png({ compressionLevel: 9 }).toFile(tmpPath);
|
||||||
|
} else if (lowerExt === '.webp') {
|
||||||
|
await pipeline.webp({ quality: 85 }).toFile(tmpPath);
|
||||||
|
} else {
|
||||||
|
// jpg/jpeg + anything else
|
||||||
|
await pipeline.jpeg({ quality: 85 }).toFile(tmpPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.renameSync(tmpPath, filePath);
|
||||||
|
};
|
||||||
|
|
||||||
// @route POST /api/upload
|
// @route POST /api/upload
|
||||||
// @desc Upload single or multiple images
|
// @desc Upload single or multiple images
|
||||||
// @access Private (Admin)
|
// @access Private (Admin)
|
||||||
@@ -51,47 +91,34 @@ router.post('/', adminAuth, upload.array('images', 10), async (req, res) => {
|
|||||||
if (!req.files || req.files.length === 0) {
|
if (!req.files || req.files.length === 0) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'No files uploaded'
|
message: 'No files uploaded',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process uploaded files
|
|
||||||
const uploadedFiles = await Promise.all(
|
const uploadedFiles = await Promise.all(
|
||||||
req.files.map(async (file) => {
|
req.files.map(async (file) => {
|
||||||
try {
|
try {
|
||||||
// Optimize image with sharp
|
const ext = path.extname(file.filename);
|
||||||
const optimizedPath = path.join(uploadsDir, `optimized-${file.filename}`);
|
await optimizeImageInPlace(file.path, ext);
|
||||||
|
|
||||||
await sharp(file.path)
|
|
||||||
.resize(1920, 1080, {
|
|
||||||
fit: 'inside',
|
|
||||||
withoutEnlargement: true
|
|
||||||
})
|
|
||||||
.jpeg({ quality: 85 })
|
|
||||||
.toFile(optimizedPath);
|
|
||||||
|
|
||||||
// Replace original with optimized
|
|
||||||
fs.unlinkSync(file.path);
|
|
||||||
fs.renameSync(optimizedPath, file.path);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filename: file.filename,
|
filename: file.filename,
|
||||||
originalName: file.originalname,
|
originalName: file.originalname,
|
||||||
url: `/uploads/${file.filename}`,
|
url: `${PUBLIC_BASE}/${file.filename}`,
|
||||||
size: file.size,
|
size: fs.statSync(file.path).size,
|
||||||
mimeType: file.mimetype,
|
mimeType: file.mimetype,
|
||||||
uploadedAt: new Date()
|
uploadedAt: new Date(),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error processing image:', error);
|
console.error('Error processing image:', error);
|
||||||
return {
|
return {
|
||||||
filename: file.filename,
|
filename: file.filename,
|
||||||
originalName: file.originalname,
|
originalName: file.originalname,
|
||||||
url: `/uploads/${file.filename}`,
|
url: `${PUBLIC_BASE}/${file.filename}`,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
mimeType: file.mimetype,
|
mimeType: file.mimetype,
|
||||||
uploadedAt: new Date(),
|
uploadedAt: new Date(),
|
||||||
error: 'Optimization failed'
|
error: 'Optimization failed',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -100,13 +127,13 @@ router.post('/', adminAuth, upload.array('images', 10), async (req, res) => {
|
|||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Files uploaded successfully',
|
message: 'Files uploaded successfully',
|
||||||
data: { files: uploadedFiles }
|
data: { files: uploadedFiles },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload error:', error);
|
console.error('Upload error:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: error.message || 'Error uploading files'
|
message: error.message || 'Error uploading files',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -116,32 +143,32 @@ router.post('/', adminAuth, upload.array('images', 10), async (req, res) => {
|
|||||||
// @access Private (Admin)
|
// @access Private (Admin)
|
||||||
router.get('/list', adminAuth, async (req, res) => {
|
router.get('/list', adminAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const files = fs.readdirSync(uploadsDir);
|
const files = fs.readdirSync(UPLOAD_DIR);
|
||||||
|
|
||||||
const fileList = files
|
const fileList = files
|
||||||
.filter(file => !file.startsWith('.')) // Ignore hidden files
|
.filter((file) => !file.startsWith('.'))
|
||||||
.map(filename => {
|
.map((filename) => {
|
||||||
const filePath = path.join(uploadsDir, filename);
|
const filePath = path.join(UPLOAD_DIR, filename);
|
||||||
const stats = fs.statSync(filePath);
|
const stats = fs.statSync(filePath);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filename,
|
filename,
|
||||||
url: `/uploads/${filename}`,
|
url: `${PUBLIC_BASE}/${filename}`,
|
||||||
size: stats.size,
|
size: stats.size,
|
||||||
uploadedAt: stats.mtime
|
uploadedAt: stats.mtime,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => b.uploadedAt - a.uploadedAt); // Sort by newest first
|
.sort((a, b) => b.uploadedAt - a.uploadedAt);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: { files: fileList, total: fileList.length }
|
data: { files: fileList, total: fileList.length },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('List files error:', error);
|
console.error('List files error:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Error listing files'
|
message: 'Error listing files',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -152,20 +179,21 @@ router.get('/list', adminAuth, async (req, res) => {
|
|||||||
router.delete('/:filename', adminAuth, async (req, res) => {
|
router.delete('/:filename', adminAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { filename } = req.params;
|
const { filename } = req.params;
|
||||||
const filePath = path.join(uploadsDir, filename);
|
|
||||||
|
|
||||||
// Security check: ensure filename doesn't contain path traversal
|
// Security check
|
||||||
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
|
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Invalid filename'
|
message: 'Invalid filename',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(UPLOAD_DIR, filename);
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'File not found'
|
message: 'File not found',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,16 +201,15 @@ router.delete('/:filename', adminAuth, async (req, res) => {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'File deleted successfully'
|
message: 'File deleted successfully',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Delete file error:', error);
|
console.error('Delete file error:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Error deleting file'
|
message: 'Error deleting file',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
Reference in New Issue
Block a user