Compare commits
3 Commits
6417fd6b01
...
212c86d29d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
212c86d29d | ||
|
|
3c3aef5446 | ||
|
|
2909b675a1 |
269
routes/media.js
269
routes/media.js
@@ -2,24 +2,21 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fsNative = require('fs');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
const Media = require('../models/Media');
|
||||
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 isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
const PUBLIC_BASE = process.env.UPLOAD_PUBLIC_URL || '/uploads';
|
||||
const MEDIA_ROOT = process.env.MEDIA_SUBDIR || 'images';
|
||||
// In dev: oldvine_cms/client/public/images
|
||||
const devImagesRoot = path.join(__dirname, '../../client/public/images');
|
||||
|
||||
// Ensure upload directories exist
|
||||
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 {
|
||||
await fs.access(dir);
|
||||
} catch {
|
||||
@@ -27,26 +24,23 @@ 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';
|
||||
};
|
||||
const safeFolder = (s) =>
|
||||
String(s || 'general')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '') || 'general';
|
||||
|
||||
// Configure multer storage
|
||||
const storage = multer.diskStorage({
|
||||
destination: async (req, file, cb) => {
|
||||
try {
|
||||
const folderSafe = sanitizeFolder(req.body.folder);
|
||||
const uploadPath = path.join(UPLOAD_DIR, MEDIA_ROOT, folderSafe);
|
||||
await ensureUploadDir(uploadPath);
|
||||
const folder = safeFolder(req.body.folder || 'general');
|
||||
const uploadPath = path.join(imagesRoot, folder);
|
||||
await ensureDir(uploadPath);
|
||||
cb(null, uploadPath);
|
||||
} catch (e) {
|
||||
cb(e);
|
||||
} catch (err) {
|
||||
cb(err);
|
||||
}
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
@@ -58,13 +52,12 @@ const storage = multer.diskStorage({
|
||||
.replace(/[^a-z0-9]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
cb(null, (basename || 'file') + '-' + uniqueSuffix + ext.toLowerCase());
|
||||
cb(null, `${basename || 'media'}-${uniqueSuffix}${ext.toLowerCase()}`);
|
||||
},
|
||||
});
|
||||
|
||||
// File filter
|
||||
const fileFilter = (req, file, cb) => {
|
||||
const allowedMimeTypes = [
|
||||
const allowed = [
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png',
|
||||
@@ -75,58 +68,37 @@ const fileFilter = (req, file, cb) => {
|
||||
'video/quicktime',
|
||||
'application/pdf',
|
||||
];
|
||||
|
||||
if (allowedMimeTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Invalid file type. Only images, videos, and PDFs are allowed.'), false);
|
||||
}
|
||||
if (allowed.includes(file.mimetype)) return cb(null, true);
|
||||
cb(new Error('Invalid file type. Only images, videos, and PDFs are allowed.'), false);
|
||||
};
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
fileFilter,
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024, // 10MB limit
|
||||
},
|
||||
limits: { fileSize: 10 * 1024 * 1024 },
|
||||
});
|
||||
|
||||
// @route POST /api/media/upload
|
||||
// @desc Upload media file
|
||||
// @access Private (Admin)
|
||||
// POST /api/media/upload
|
||||
router.post('/upload', adminAuth, upload.single('file'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'No file uploaded',
|
||||
});
|
||||
}
|
||||
if (!req.file) return res.status(400).json({ success: false, message: 'No file uploaded' });
|
||||
|
||||
const folderSafe = sanitizeFolder(req.body.folder);
|
||||
const folder = safeFolder(req.body.folder || 'general');
|
||||
const { alt, caption, description, tags } = req.body;
|
||||
|
||||
// Determine media type
|
||||
let mediaType = 'other';
|
||||
if (req.file.mimetype.startsWith('image/')) {
|
||||
mediaType = 'image';
|
||||
} else if (req.file.mimetype.startsWith('video/')) {
|
||||
mediaType = 'video';
|
||||
} else if (req.file.mimetype === 'application/pdf') {
|
||||
mediaType = 'document';
|
||||
}
|
||||
if (req.file.mimetype.startsWith('image/')) mediaType = 'image';
|
||||
else if (req.file.mimetype.startsWith('video/')) mediaType = 'video';
|
||||
else if (req.file.mimetype === 'application/pdf') mediaType = 'document';
|
||||
|
||||
const url = `${PUBLIC_BASE}/${MEDIA_ROOT}/${folderSafe}/${req.file.filename}`;
|
||||
|
||||
// Create media record
|
||||
const media = new Media({
|
||||
filename: req.file.filename,
|
||||
originalName: req.file.originalname,
|
||||
url,
|
||||
url: `/images/${folder}/${req.file.filename}`,
|
||||
mimeType: req.file.mimetype,
|
||||
size: req.file.size,
|
||||
type: mediaType,
|
||||
folder: folderSafe,
|
||||
folder,
|
||||
alt,
|
||||
caption,
|
||||
description,
|
||||
@@ -136,55 +108,37 @@ router.post('/upload', adminAuth, upload.single('file'), async (req, res) => {
|
||||
|
||||
await media.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'File uploaded successfully',
|
||||
data: { media },
|
||||
});
|
||||
res.status(201).json({ success: true, message: 'File uploaded successfully', data: { media } });
|
||||
} catch (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
|
||||
// @desc Upload multiple media files
|
||||
// @access Private (Admin)
|
||||
// POST /api/media/upload-multiple
|
||||
router.post('/upload-multiple', adminAuth, upload.array('files', 10), async (req, res) => {
|
||||
try {
|
||||
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 folderSafe = sanitizeFolder(req.body.folder);
|
||||
const folder = safeFolder(req.body.folder || 'general');
|
||||
const mediaRecords = [];
|
||||
|
||||
for (const file of req.files) {
|
||||
let mediaType = 'other';
|
||||
if (file.mimetype.startsWith('image/')) {
|
||||
mediaType = 'image';
|
||||
} else if (file.mimetype.startsWith('video/')) {
|
||||
mediaType = 'video';
|
||||
} else if (file.mimetype === 'application/pdf') {
|
||||
mediaType = 'document';
|
||||
}
|
||||
|
||||
const url = `${PUBLIC_BASE}/${MEDIA_ROOT}/${folderSafe}/${file.filename}`;
|
||||
if (file.mimetype.startsWith('image/')) mediaType = 'image';
|
||||
else if (file.mimetype.startsWith('video/')) mediaType = 'video';
|
||||
else if (file.mimetype === 'application/pdf') mediaType = 'document';
|
||||
|
||||
const media = new Media({
|
||||
filename: file.filename,
|
||||
originalName: file.originalname,
|
||||
url,
|
||||
url: `/images/${folder}/${file.filename}`,
|
||||
mimeType: file.mimetype,
|
||||
size: file.size,
|
||||
type: mediaType,
|
||||
folder: folderSafe,
|
||||
folder,
|
||||
uploadedBy: req.admin.id,
|
||||
});
|
||||
|
||||
@@ -199,16 +153,11 @@ router.post('/upload-multiple', adminAuth, upload.array('files', 10), async (req
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Multiple 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' });
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/media
|
||||
// @desc Get all media
|
||||
// @access Private (Admin)
|
||||
// GET /api/media
|
||||
router.get('/', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { folder, type, page = 1, limit = 50 } = req.query;
|
||||
@@ -228,57 +177,29 @@ router.get('/', adminAuth, async (req, res) => {
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
media,
|
||||
pagination: {
|
||||
page: parseInt(page, 10),
|
||||
limit: parseInt(limit, 10),
|
||||
total,
|
||||
pages: Math.ceil(total / limit),
|
||||
},
|
||||
},
|
||||
data: { media, pagination: { page: parseInt(page, 10), limit: parseInt(limit, 10), total, pages: Math.ceil(total / limit) } },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get media error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching media',
|
||||
});
|
||||
res.status(500).json({ success: false, message: 'Error fetching media' });
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/media/search
|
||||
// @desc Search media
|
||||
// @access Private (Admin)
|
||||
// GET /api/media/search
|
||||
router.get('/search', adminAuth, async (req, res) => {
|
||||
try {
|
||||
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) });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { media },
|
||||
});
|
||||
res.json({ success: true, data: { media } });
|
||||
} catch (error) {
|
||||
console.error('Search media error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error searching media',
|
||||
});
|
||||
res.status(500).json({ success: false, message: 'Error searching media' });
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/media/:id
|
||||
// @desc Update media metadata
|
||||
// @access Private (Admin)
|
||||
// PUT /api/media/:id
|
||||
router.put('/:id', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
@@ -289,107 +210,51 @@ router.put('/:id', adminAuth, async (req, res) => {
|
||||
if (caption !== undefined) updateFields.caption = caption;
|
||||
if (description !== undefined) updateFields.description = description;
|
||||
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 });
|
||||
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 } });
|
||||
} catch (error) {
|
||||
console.error('Update media error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error updating media',
|
||||
});
|
||||
res.status(500).json({ success: false, 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
|
||||
// @desc Delete media file
|
||||
// @access Private (Admin)
|
||||
// DELETE /api/media/:id
|
||||
router.delete('/:id', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
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',
|
||||
});
|
||||
}
|
||||
// media.url example: /images/general/file.jpg
|
||||
const rel = media.url.replace(/^\/images\//, '');
|
||||
const filePath = path.join(imagesRoot, rel);
|
||||
|
||||
// Delete file from filesystem (only if inside UPLOAD_DIR)
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
await fs.unlink(filePath);
|
||||
} catch (err) {
|
||||
console.error('Error deleting file:', err);
|
||||
}
|
||||
|
||||
await Media.findByIdAndDelete(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Media deleted successfully',
|
||||
});
|
||||
res.json({ success: true, message: 'Media deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Delete media error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error deleting media',
|
||||
});
|
||||
res.status(500).json({ success: false, message: 'Error deleting media' });
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/media/folders/list
|
||||
// @desc Get list of all folders
|
||||
// @access Private (Admin)
|
||||
// GET /api/media/folders/list
|
||||
router.get('/folders/list', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const folders = await Media.distinct('folder');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { folders },
|
||||
});
|
||||
res.json({ success: true, data: { folders } });
|
||||
} catch (error) {
|
||||
console.error('Get folders error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching folders',
|
||||
});
|
||||
res.status(500).json({ success: false, message: 'Error fetching folders' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
186
routes/upload.js
186
routes/upload.js
@@ -6,115 +6,101 @@ const fs = require('fs');
|
||||
const sharp = require('sharp');
|
||||
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 isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
const PUBLIC_BASE = process.env.UPLOAD_PUBLIC_URL || '/uploads';
|
||||
// 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(UPLOAD_DIR)) {
|
||||
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
||||
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, UPLOAD_DIR);
|
||||
},
|
||||
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() || '.jpg';
|
||||
const name = path
|
||||
.basename(file.originalname, ext)
|
||||
.replace(/[^a-z0-9]/gi, '-')
|
||||
.toLowerCase()
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
|
||||
cb(null, `${name || 'image'}-${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/')) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Only image files are allowed'), false);
|
||||
}
|
||||
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 limit
|
||||
},
|
||||
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
|
||||
});
|
||||
|
||||
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
|
||||
// @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',
|
||||
});
|
||||
return res.status(400).json({ success: false, message: 'No files uploaded' });
|
||||
}
|
||||
|
||||
const uploadedFiles = await Promise.all(
|
||||
req.files.map(async (file) => {
|
||||
try {
|
||||
const ext = path.extname(file.filename);
|
||||
await optimizeImageInPlace(file.path, ext);
|
||||
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: `${PUBLIC_BASE}/${file.filename}`,
|
||||
url: `/uploads/${file.filename}`,
|
||||
size: fs.statSync(file.path).size,
|
||||
mimeType: file.mimetype,
|
||||
uploadedAt: new Date(),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error processing image:', error);
|
||||
} catch (err) {
|
||||
console.error('Error processing image:', err);
|
||||
return {
|
||||
filename: file.filename,
|
||||
originalName: file.originalname,
|
||||
url: `${PUBLIC_BASE}/${file.filename}`,
|
||||
url: `/uploads/${file.filename}`,
|
||||
size: file.size,
|
||||
mimeType: file.mimetype,
|
||||
uploadedAt: new Date(),
|
||||
@@ -124,91 +110,51 @@ router.post('/', adminAuth, upload.array('images', 10), async (req, res) => {
|
||||
})
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Files uploaded successfully',
|
||||
data: { files: uploadedFiles },
|
||||
});
|
||||
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',
|
||||
});
|
||||
res.status(500).json({ success: false, message: error.message || 'Error uploading files' });
|
||||
}
|
||||
});
|
||||
|
||||
// @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(UPLOAD_DIR);
|
||||
const files = fs.readdirSync(uploadsDir);
|
||||
|
||||
const fileList = files
|
||||
.filter((file) => !file.startsWith('.'))
|
||||
.filter((f) => !f.startsWith('.'))
|
||||
.map((filename) => {
|
||||
const filePath = path.join(UPLOAD_DIR, filename);
|
||||
const filePath = path.join(uploadsDir, filename);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
return {
|
||||
filename,
|
||||
url: `${PUBLIC_BASE}/${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);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { files: fileList, total: fileList.length },
|
||||
});
|
||||
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',
|
||||
});
|
||||
res.status(500).json({ success: false, message: 'Error listing files' });
|
||||
}
|
||||
});
|
||||
|
||||
// @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
|
||||
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(UPLOAD_DIR, filename);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'File not found',
|
||||
});
|
||||
}
|
||||
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',
|
||||
});
|
||||
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',
|
||||
});
|
||||
res.status(500).json({ success: false, message: 'Error deleting file' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user