edit media
This commit is contained in:
131
routes/media.js
131
routes/media.js
@@ -6,17 +6,17 @@ 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');
|
||||||
|
|
||||||
// --- Helpers ---
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
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 baseDir = getUploadsDir();
|
// In dev: oldvine_cms/client/public/images
|
||||||
const urlPrefix = (process.env.UPLOADS_URL_PREFIX || '/uploads').replace(/\/$/, '');
|
const devImagesRoot = path.join(__dirname, '../../client/public/images');
|
||||||
|
|
||||||
const ensureUploadDir = async (dir) => {
|
// On server: nginx serves /images from /var/www/oldvine/images (recommended)
|
||||||
|
const imagesRoot =
|
||||||
|
process.env.IMAGES_DIR ||
|
||||||
|
(isProd ? '/var/www/oldvine/images' : devImagesRoot);
|
||||||
|
|
||||||
|
const ensureDir = async (dir) => {
|
||||||
try {
|
try {
|
||||||
await fs.access(dir);
|
await fs.access(dir);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -24,46 +24,40 @@ const ensureUploadDir = async (dir) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizeFolder = (folder) => {
|
const safeFolder = (s) =>
|
||||||
const f = String(folder || 'general').trim().toLowerCase();
|
String(s || 'general')
|
||||||
return f.replace(/[^a-z0-9/_-]/g, '').replace(/\/+/g, '/').replace(/^\/|\/$/g, '') || 'general';
|
.toLowerCase()
|
||||||
};
|
.replace(/[^a-z0-9]/g, '-')
|
||||||
|
.replace(/-+/g, '-')
|
||||||
const resolveSafePath = (root, relative) => {
|
.replace(/^-|-$/g, '') || 'general';
|
||||||
const safeRel = String(relative || '').replace(/^\/+/, '');
|
|
||||||
const full = path.resolve(root, safeRel);
|
|
||||||
const rootResolved = path.resolve(root);
|
|
||||||
if (!full.startsWith(rootResolved + path.sep) && full !== rootResolved) {
|
|
||||||
throw new Error('Invalid path');
|
|
||||||
}
|
|
||||||
return full;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Configure multer storage
|
// Configure multer storage
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => {
|
destination: async (req, file, cb) => {
|
||||||
const folder = sanitizeFolder(req.body.folder || 'general');
|
try {
|
||||||
const uploadPath = path.join(baseDir, folder);
|
const folder = safeFolder(req.body.folder || 'general');
|
||||||
|
const uploadPath = path.join(imagesRoot, folder);
|
||||||
ensureUploadDir(uploadPath)
|
await ensureDir(uploadPath);
|
||||||
.then(() => cb(null, uploadPath))
|
cb(null, uploadPath);
|
||||||
.catch((e) => cb(e));
|
} catch (err) {
|
||||||
|
cb(err);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
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).toLowerCase();
|
const ext = path.extname(file.originalname);
|
||||||
const basename = path
|
const basename = path
|
||||||
.basename(file.originalname, ext)
|
.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 || 'file'}-${uniqueSuffix}${ext || ''}`);
|
cb(null, `${basename || 'media'}-${uniqueSuffix}${ext.toLowerCase()}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileFilter = (req, file, cb) => {
|
const fileFilter = (req, file, cb) => {
|
||||||
const allowedMimeTypes = [
|
const allowed = [
|
||||||
'image/jpeg',
|
'image/jpeg',
|
||||||
'image/jpg',
|
'image/jpg',
|
||||||
'image/png',
|
'image/png',
|
||||||
@@ -74,27 +68,22 @@ const fileFilter = (req, file, cb) => {
|
|||||||
'video/quicktime',
|
'video/quicktime',
|
||||||
'application/pdf',
|
'application/pdf',
|
||||||
];
|
];
|
||||||
|
if (allowed.includes(file.mimetype)) return cb(null, true);
|
||||||
if (allowedMimeTypes.includes(file.mimetype)) return cb(null, true);
|
|
||||||
cb(new Error('Invalid file type. Only images, videos, and PDFs are allowed.'), false);
|
cb(new Error('Invalid file type. Only images, videos, and PDFs are allowed.'), false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const upload = multer({
|
const upload = multer({
|
||||||
storage,
|
storage,
|
||||||
fileFilter,
|
fileFilter,
|
||||||
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
|
limits: { fileSize: 10 * 1024 * 1024 },
|
||||||
});
|
});
|
||||||
|
|
||||||
// @route POST /api/media/upload
|
// POST /api/media/upload
|
||||||
// @desc Upload media file
|
|
||||||
// @access Private (Admin)
|
|
||||||
router.post('/upload', adminAuth, upload.single('file'), async (req, res) => {
|
router.post('/upload', adminAuth, upload.single('file'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (!req.file) {
|
if (!req.file) return res.status(400).json({ success: false, message: 'No file uploaded' });
|
||||||
return res.status(400).json({ success: false, message: 'No file uploaded' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const folder = sanitizeFolder(req.body.folder || 'general');
|
const folder = safeFolder(req.body.folder || 'general');
|
||||||
const { alt, caption, description, tags } = req.body;
|
const { alt, caption, description, tags } = req.body;
|
||||||
|
|
||||||
let mediaType = 'other';
|
let mediaType = 'other';
|
||||||
@@ -105,7 +94,7 @@ router.post('/upload', adminAuth, upload.single('file'), async (req, res) => {
|
|||||||
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: `${urlPrefix}/${folder}/${req.file.filename}`,
|
url: `/images/${folder}/${req.file.filename}`,
|
||||||
mimeType: req.file.mimetype,
|
mimeType: req.file.mimetype,
|
||||||
size: req.file.size,
|
size: req.file.size,
|
||||||
type: mediaType,
|
type: mediaType,
|
||||||
@@ -113,31 +102,27 @@ router.post('/upload', adminAuth, upload.single('file'), async (req, res) => {
|
|||||||
alt,
|
alt,
|
||||||
caption,
|
caption,
|
||||||
description,
|
description,
|
||||||
tags: tags ? tags.split(',').map((t) => t.trim()).filter(Boolean) : [],
|
tags: tags ? tags.split(',').map((t) => t.trim()) : [],
|
||||||
uploadedBy: req.admin.id,
|
uploadedBy: req.admin.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
await media.save();
|
await media.save();
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({ success: true, message: 'File uploaded successfully', data: { media } });
|
||||||
success: true,
|
|
||||||
message: 'File uploaded successfully',
|
|
||||||
data: { media },
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload error:', error);
|
console.error('Upload error:', error);
|
||||||
res.status(500).json({ success: false, message: error.message || 'Error uploading file' });
|
res.status(500).json({ success: false, message: error.message || 'Error uploading file' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// @route POST /api/media/upload-multiple
|
// POST /api/media/upload-multiple
|
||||||
router.post('/upload-multiple', adminAuth, upload.array('files', 10), async (req, res) => {
|
router.post('/upload-multiple', adminAuth, upload.array('files', 10), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (!req.files || req.files.length === 0) {
|
if (!req.files || req.files.length === 0) {
|
||||||
return res.status(400).json({ success: false, message: 'No files uploaded' });
|
return res.status(400).json({ success: false, message: 'No files uploaded' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const folder = sanitizeFolder(req.body.folder || 'general');
|
const folder = safeFolder(req.body.folder || 'general');
|
||||||
const mediaRecords = [];
|
const mediaRecords = [];
|
||||||
|
|
||||||
for (const file of req.files) {
|
for (const file of req.files) {
|
||||||
@@ -149,7 +134,7 @@ router.post('/upload-multiple', adminAuth, upload.array('files', 10), async (req
|
|||||||
const media = new Media({
|
const media = new Media({
|
||||||
filename: file.filename,
|
filename: file.filename,
|
||||||
originalName: file.originalname,
|
originalName: file.originalname,
|
||||||
url: `${urlPrefix}/${folder}/${file.filename}`,
|
url: `/images/${folder}/${file.filename}`,
|
||||||
mimeType: file.mimetype,
|
mimeType: file.mimetype,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
type: mediaType,
|
type: mediaType,
|
||||||
@@ -172,7 +157,7 @@ router.post('/upload-multiple', adminAuth, upload.array('files', 10), async (req
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// @route GET /api/media
|
// GET /api/media
|
||||||
router.get('/', adminAuth, async (req, res) => {
|
router.get('/', adminAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { folder, type, page = 1, limit = 50 } = req.query;
|
const { folder, type, page = 1, limit = 50 } = req.query;
|
||||||
@@ -192,15 +177,7 @@ router.get('/', adminAuth, async (req, res) => {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: { media, pagination: { page: parseInt(page, 10), limit: parseInt(limit, 10), total, pages: Math.ceil(total / limit) } },
|
||||||
media,
|
|
||||||
pagination: {
|
|
||||||
page: parseInt(page, 10),
|
|
||||||
limit: parseInt(limit, 10),
|
|
||||||
total,
|
|
||||||
pages: Math.ceil(total / limit),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Get media error:', error);
|
console.error('Get media error:', error);
|
||||||
@@ -208,17 +185,13 @@ router.get('/', adminAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// @route GET /api/media/search
|
// GET /api/media/search
|
||||||
router.get('/search', adminAuth, async (req, res) => {
|
router.get('/search', adminAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { q, folder, type, limit = 50 } = req.query;
|
const { q, folder, type, limit = 50 } = req.query;
|
||||||
|
if (!q) return res.status(400).json({ success: false, message: 'Search query is required' });
|
||||||
if (!q) {
|
|
||||||
return res.status(400).json({ success: false, message: 'Search query is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const media = await Media.search(q, { folder, type, limit: parseInt(limit, 10) });
|
const media = await Media.search(q, { folder, type, limit: parseInt(limit, 10) });
|
||||||
|
|
||||||
res.json({ success: true, data: { media } });
|
res.json({ success: true, data: { media } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Search media error:', error);
|
console.error('Search media error:', error);
|
||||||
@@ -226,7 +199,7 @@ router.get('/search', adminAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// @route PUT /api/media/:id
|
// PUT /api/media/:id
|
||||||
router.put('/:id', adminAuth, async (req, res) => {
|
router.put('/:id', adminAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
@@ -237,10 +210,9 @@ router.put('/:id', adminAuth, async (req, res) => {
|
|||||||
if (caption !== undefined) updateFields.caption = caption;
|
if (caption !== undefined) updateFields.caption = caption;
|
||||||
if (description !== undefined) updateFields.description = description;
|
if (description !== undefined) updateFields.description = description;
|
||||||
if (tags !== undefined) updateFields.tags = tags;
|
if (tags !== undefined) updateFields.tags = tags;
|
||||||
if (folder !== undefined) updateFields.folder = sanitizeFolder(folder);
|
if (folder !== undefined) updateFields.folder = safeFolder(folder);
|
||||||
|
|
||||||
const media = await Media.findByIdAndUpdate(id, { $set: updateFields }, { new: true, runValidators: true });
|
const media = await Media.findByIdAndUpdate(id, { $set: updateFields }, { new: true, runValidators: true });
|
||||||
|
|
||||||
if (!media) return res.status(404).json({ success: false, message: 'Media not found' });
|
if (!media) return res.status(404).json({ success: false, message: 'Media not found' });
|
||||||
|
|
||||||
res.json({ success: true, message: 'Media updated successfully', data: { media } });
|
res.json({ success: true, message: 'Media updated successfully', data: { media } });
|
||||||
@@ -250,27 +222,24 @@ router.put('/:id', adminAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// @route DELETE /api/media/:id
|
// DELETE /api/media/:id
|
||||||
router.delete('/:id', adminAuth, async (req, res) => {
|
router.delete('/:id', adminAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const media = await Media.findById(id);
|
const media = await Media.findById(id);
|
||||||
|
|
||||||
if (!media) return res.status(404).json({ success: false, message: 'Media not found' });
|
if (!media) return res.status(404).json({ success: false, message: 'Media not found' });
|
||||||
|
|
||||||
// Delete file from filesystem (based on /uploads/.. url)
|
// media.url example: /images/general/file.jpg
|
||||||
|
const rel = media.url.replace(/^\/images\//, '');
|
||||||
|
const filePath = path.join(imagesRoot, rel);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (media.url && media.url.startsWith(urlPrefix + '/')) {
|
|
||||||
const relative = media.url.slice(urlPrefix.length + 1); // remove "/uploads/"
|
|
||||||
const filePath = resolveSafePath(baseDir, relative);
|
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error deleting file:', err);
|
console.error('Error deleting file:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Media.findByIdAndDelete(id);
|
await Media.findByIdAndDelete(id);
|
||||||
|
|
||||||
res.json({ success: true, message: 'Media deleted successfully' });
|
res.json({ success: true, message: 'Media deleted successfully' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Delete media error:', error);
|
console.error('Delete media error:', error);
|
||||||
@@ -278,7 +247,7 @@ router.delete('/:id', adminAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// @route GET /api/media/folders/list
|
// GET /api/media/folders/list
|
||||||
router.get('/folders/list', adminAuth, async (req, res) => {
|
router.get('/folders/list', adminAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const folders = await Media.distinct('folder');
|
const folders = await Media.distinct('folder');
|
||||||
|
|||||||
135
routes/upload.js
135
routes/upload.js
@@ -6,40 +6,49 @@ const fs = require('fs');
|
|||||||
const sharp = require('sharp');
|
const sharp = require('sharp');
|
||||||
const adminAuth = require('../middleware/adminAuth');
|
const adminAuth = require('../middleware/adminAuth');
|
||||||
|
|
||||||
// --- Helpers ---
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
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();
|
// In dev: oldvine_cms/client/public
|
||||||
const urlPrefix = (process.env.UPLOADS_URL_PREFIX || '/uploads').replace(/\/$/, '');
|
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
|
// Ensure uploads directory exists
|
||||||
if (!fs.existsSync(uploadsDir)) {
|
if (!fs.existsSync(uploadsDir)) {
|
||||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
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
|
// Configure multer for file upload
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => cb(null, uploadsDir),
|
destination: (req, file, cb) => cb(null, uploadsDir),
|
||||||
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).toLowerCase();
|
const base = safeBaseName(file.originalname);
|
||||||
const name = path
|
const { ext } = pickOutput(file.mimetype);
|
||||||
.basename(file.originalname, ext)
|
cb(null, `${base || 'upload'}-${uniqueSuffix}${ext}`);
|
||||||
.replace(/[^a-z0-9]/gi, '-')
|
|
||||||
.replace(/-+/g, '-')
|
|
||||||
.replace(/^-|-$/g, '')
|
|
||||||
.toLowerCase();
|
|
||||||
|
|
||||||
cb(null, `${name || 'file'}-${uniqueSuffix}${ext || ''}`);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileFilter = (req, file, cb) => {
|
const fileFilter = (req, file, cb) => {
|
||||||
// Accept images only
|
|
||||||
if (file.mimetype && file.mimetype.startsWith('image/')) return cb(null, true);
|
if (file.mimetype && file.mimetype.startsWith('image/')) return cb(null, true);
|
||||||
cb(new Error('Only image files are allowed'), false);
|
cb(new Error('Only image files are allowed'), false);
|
||||||
};
|
};
|
||||||
@@ -50,29 +59,6 @@ const upload = multer({
|
|||||||
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
|
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
|
// @route POST /api/upload
|
||||||
// @desc Upload single or multiple images
|
// @desc Upload single or multiple images
|
||||||
// @access Private (Admin)
|
// @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' });
|
return res.status(400).json({ success: false, message: 'No files uploaded' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadedFiles = [];
|
const uploadedFiles = await Promise.all(
|
||||||
|
req.files.map(async (file) => {
|
||||||
for (const file of req.files) {
|
|
||||||
try {
|
try {
|
||||||
const extLower = path.extname(file.filename).toLowerCase();
|
const tmpPath = `${file.path}.opt`;
|
||||||
await optimizeImageIfPossible(file.path, extLower);
|
const { fmt } = pickOutput(file.mimetype);
|
||||||
|
|
||||||
uploadedFiles.push({
|
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,
|
filename: file.filename,
|
||||||
originalName: file.originalname,
|
originalName: file.originalname,
|
||||||
url: `${urlPrefix}/${file.filename}`,
|
url: `/uploads/${file.filename}`,
|
||||||
|
size: fs.statSync(file.path).size,
|
||||||
mimeType: file.mimetype,
|
mimeType: file.mimetype,
|
||||||
uploadedAt: new Date(),
|
uploadedAt: new Date(),
|
||||||
});
|
};
|
||||||
} catch (e) {
|
} catch (err) {
|
||||||
console.error('Error processing image:', e);
|
console.error('Error processing image:', err);
|
||||||
uploadedFiles.push({
|
return {
|
||||||
filename: file.filename,
|
filename: file.filename,
|
||||||
originalName: file.originalname,
|
originalName: file.originalname,
|
||||||
url: `${urlPrefix}/${file.filename}`,
|
url: `/uploads/${file.filename}`,
|
||||||
|
size: file.size,
|
||||||
mimeType: file.mimetype,
|
mimeType: file.mimetype,
|
||||||
uploadedAt: new Date(),
|
uploadedAt: new Date(),
|
||||||
error: 'Optimization failed',
|
error: 'Optimization failed',
|
||||||
});
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({ success: true, message: 'Files uploaded successfully', data: { files: uploadedFiles } });
|
||||||
success: true,
|
|
||||||
message: 'Files uploaded successfully',
|
|
||||||
data: { files: uploadedFiles },
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload error:', error);
|
console.error('Upload error:', error);
|
||||||
res.status(500).json({ success: false, message: error.message || 'Error uploading files' });
|
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
|
// @route GET /api/upload/list
|
||||||
// @desc Get list of uploaded files
|
|
||||||
// @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(uploadsDir);
|
||||||
@@ -132,12 +128,7 @@ router.get('/list', adminAuth, async (req, res) => {
|
|||||||
.map((filename) => {
|
.map((filename) => {
|
||||||
const filePath = path.join(uploadsDir, filename);
|
const filePath = path.join(uploadsDir, filename);
|
||||||
const stats = fs.statSync(filePath);
|
const stats = fs.statSync(filePath);
|
||||||
return {
|
return { filename, url: `/uploads/${filename}`, size: stats.size, uploadedAt: stats.mtime };
|
||||||
filename,
|
|
||||||
url: `${urlPrefix}/${filename}`,
|
|
||||||
size: stats.size,
|
|
||||||
uploadedAt: stats.mtime,
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
.sort((a, b) => b.uploadedAt - a.uploadedAt);
|
.sort((a, b) => b.uploadedAt - a.uploadedAt);
|
||||||
|
|
||||||
@@ -149,22 +140,16 @@ router.get('/list', adminAuth, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// @route DELETE /api/upload/:filename
|
// @route DELETE /api/upload/:filename
|
||||||
// @desc Delete an uploaded file
|
|
||||||
// @access Private (Admin)
|
|
||||||
router.delete('/:filename', adminAuth, async (req, res) => {
|
router.delete('/:filename', adminAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { filename } = req.params;
|
const { filename } = req.params;
|
||||||
|
|
||||||
// Security check: prevent path traversal
|
|
||||||
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
|
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
|
||||||
return res.status(400).json({ success: false, message: 'Invalid filename' });
|
return res.status(400).json({ success: false, message: 'Invalid filename' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = path.join(uploadsDir, 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);
|
fs.unlinkSync(filePath);
|
||||||
res.json({ success: true, message: 'File deleted successfully' });
|
res.json({ success: true, message: 'File deleted successfully' });
|
||||||
|
|||||||
Reference in New Issue
Block a user