edit upload & media file
This commit is contained in:
141
routes/upload.js
141
routes/upload.js
@@ -6,29 +6,42 @@ 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 PUBLIC_BASE = process.env.UPLOAD_PUBLIC_URL || '/uploads';
|
||||
|
||||
// Ensure uploads directory exists
|
||||
const uploadsDir = path.join(__dirname, '../../client/public/uploads');
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
if (!fs.existsSync(UPLOAD_DIR)) {
|
||||
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Configure multer for file upload
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, uploadsDir);
|
||||
cb(null, UPLOAD_DIR);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
// Generate unique filename
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
const ext = path.extname(file.originalname);
|
||||
const name = path.basename(file.originalname, ext).replace(/[^a-z0-9]/gi, '-').toLowerCase();
|
||||
cb(null, `${name}-${uniqueSuffix}${ext}`);
|
||||
}
|
||||
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 fileFilter = (req, file, cb) => {
|
||||
// Accept images only
|
||||
if (file.mimetype.startsWith('image/')) {
|
||||
if (file.mimetype && file.mimetype.startsWith('image/')) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Only image files are allowed'), false);
|
||||
@@ -36,13 +49,40 @@ const fileFilter = (req, file, cb) => {
|
||||
};
|
||||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
fileFilter: fileFilter,
|
||||
storage,
|
||||
fileFilter,
|
||||
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
|
||||
// @desc Upload single or multiple images
|
||||
// @access Private (Admin)
|
||||
@@ -51,47 +91,34 @@ router.post('/', adminAuth, upload.array('images', 10), async (req, res) => {
|
||||
if (!req.files || req.files.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'No files uploaded'
|
||||
message: 'No files uploaded',
|
||||
});
|
||||
}
|
||||
|
||||
// Process uploaded files
|
||||
const uploadedFiles = await Promise.all(
|
||||
req.files.map(async (file) => {
|
||||
try {
|
||||
// Optimize image with sharp
|
||||
const optimizedPath = path.join(uploadsDir, `optimized-${file.filename}`);
|
||||
|
||||
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);
|
||||
const ext = path.extname(file.filename);
|
||||
await optimizeImageInPlace(file.path, ext);
|
||||
|
||||
return {
|
||||
filename: file.filename,
|
||||
originalName: file.originalname,
|
||||
url: `/uploads/${file.filename}`,
|
||||
size: file.size,
|
||||
url: `${PUBLIC_BASE}/${file.filename}`,
|
||||
size: fs.statSync(file.path).size,
|
||||
mimeType: file.mimetype,
|
||||
uploadedAt: new Date()
|
||||
uploadedAt: new Date(),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error processing image:', error);
|
||||
return {
|
||||
filename: file.filename,
|
||||
originalName: file.originalname,
|
||||
url: `/uploads/${file.filename}`,
|
||||
url: `${PUBLIC_BASE}/${file.filename}`,
|
||||
size: file.size,
|
||||
mimeType: file.mimetype,
|
||||
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({
|
||||
success: true,
|
||||
message: 'Files uploaded successfully',
|
||||
data: { files: uploadedFiles }
|
||||
data: { files: uploadedFiles },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
res.status(500).json({
|
||||
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)
|
||||
router.get('/list', adminAuth, async (req, res) => {
|
||||
try {
|
||||
const files = fs.readdirSync(uploadsDir);
|
||||
|
||||
const files = fs.readdirSync(UPLOAD_DIR);
|
||||
|
||||
const fileList = files
|
||||
.filter(file => !file.startsWith('.')) // Ignore hidden files
|
||||
.map(filename => {
|
||||
const filePath = path.join(uploadsDir, filename);
|
||||
.filter((file) => !file.startsWith('.'))
|
||||
.map((filename) => {
|
||||
const filePath = path.join(UPLOAD_DIR, filename);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
|
||||
return {
|
||||
filename,
|
||||
url: `/uploads/${filename}`,
|
||||
url: `${PUBLIC_BASE}/${filename}`,
|
||||
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({
|
||||
success: true,
|
||||
data: { files: fileList, total: fileList.length }
|
||||
data: { files: fileList, total: fileList.length },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('List files error:', error);
|
||||
res.status(500).json({
|
||||
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) => {
|
||||
try {
|
||||
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('\\')) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid filename'
|
||||
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'
|
||||
message: 'File not found',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -173,16 +201,15 @@ router.delete('/:filename', adminAuth, async (req, res) => {
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'File deleted successfully'
|
||||
message: 'File deleted successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Delete file error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error deleting file'
|
||||
message: 'Error deleting file',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user