edit media

This commit is contained in:
yotakii
2026-02-18 15:40:31 +03:00
parent 2909b675a1
commit 3c3aef5446
2 changed files with 121 additions and 167 deletions

View File

@@ -6,17 +6,17 @@ const fs = require('fs').promises;
const Media = require('../models/Media');
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 baseDir = getUploadsDir();
const urlPrefix = (process.env.UPLOADS_URL_PREFIX || '/uploads').replace(/\/$/, '');
// In dev: oldvine_cms/client/public/images
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 {
await fs.access(dir);
} catch {
@@ -24,46 +24,40 @@ const ensureUploadDir = async (dir) => {
}
};
const sanitizeFolder = (folder) => {
const f = String(folder || 'general').trim().toLowerCase();
return f.replace(/[^a-z0-9/_-]/g, '').replace(/\/+/g, '/').replace(/^\/|\/$/g, '') || 'general';
};
const resolveSafePath = (root, relative) => {
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;
};
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: (req, file, cb) => {
const folder = sanitizeFolder(req.body.folder || 'general');
const uploadPath = path.join(baseDir, folder);
ensureUploadDir(uploadPath)
.then(() => cb(null, uploadPath))
.catch((e) => cb(e));
destination: async (req, file, cb) => {
try {
const folder = safeFolder(req.body.folder || 'general');
const uploadPath = path.join(imagesRoot, folder);
await ensureDir(uploadPath);
cb(null, uploadPath);
} catch (err) {
cb(err);
}
},
filename: (req, file, cb) => {
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
.basename(file.originalname, ext)
.toLowerCase()
.replace(/[^a-z0-9]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
cb(null, `${basename || 'file'}-${uniqueSuffix}${ext || ''}`);
cb(null, `${basename || 'media'}-${uniqueSuffix}${ext.toLowerCase()}`);
},
});
const fileFilter = (req, file, cb) => {
const allowedMimeTypes = [
const allowed = [
'image/jpeg',
'image/jpg',
'image/png',
@@ -74,27 +68,22 @@ const fileFilter = (req, file, cb) => {
'video/quicktime',
'application/pdf',
];
if (allowedMimeTypes.includes(file.mimetype)) return cb(null, true);
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
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 folder = sanitizeFolder(req.body.folder || 'general');
const folder = safeFolder(req.body.folder || 'general');
const { alt, caption, description, tags } = req.body;
let mediaType = 'other';
@@ -105,7 +94,7 @@ router.post('/upload', adminAuth, upload.single('file'), async (req, res) => {
const media = new Media({
filename: req.file.filename,
originalName: req.file.originalname,
url: `${urlPrefix}/${folder}/${req.file.filename}`,
url: `/images/${folder}/${req.file.filename}`,
mimeType: req.file.mimetype,
size: req.file.size,
type: mediaType,
@@ -113,31 +102,27 @@ router.post('/upload', adminAuth, upload.single('file'), async (req, res) => {
alt,
caption,
description,
tags: tags ? tags.split(',').map((t) => t.trim()).filter(Boolean) : [],
tags: tags ? tags.split(',').map((t) => t.trim()) : [],
uploadedBy: req.admin.id,
});
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' });
}
});
// @route POST /api/media/upload-multiple
// 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' });
}
const folder = sanitizeFolder(req.body.folder || 'general');
const folder = safeFolder(req.body.folder || 'general');
const mediaRecords = [];
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({
filename: file.filename,
originalName: file.originalname,
url: `${urlPrefix}/${folder}/${file.filename}`,
url: `/images/${folder}/${file.filename}`,
mimeType: file.mimetype,
size: file.size,
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) => {
try {
const { folder, type, page = 1, limit = 50 } = req.query;
@@ -192,15 +177,7 @@ 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);
@@ -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) => {
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 } });
} catch (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) => {
try {
const { id } = req.params;
@@ -237,10 +210,9 @@ 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' });
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) => {
try {
const { id } = req.params;
const media = await Media.findById(id);
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 {
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) {
console.error('Error deleting file:', err);
}
await Media.findByIdAndDelete(id);
res.json({ success: true, message: 'Media deleted successfully' });
} catch (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) => {
try {
const folders = await Media.distinct('folder');