261 lines
8.2 KiB
JavaScript
261 lines
8.2 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const multer = require('multer');
|
|
const path = require('path');
|
|
const fs = require('fs').promises;
|
|
const Media = require('../models/Media');
|
|
const adminAuth = require('../middleware/adminAuth');
|
|
|
|
const isProd = process.env.NODE_ENV === 'production';
|
|
|
|
// In dev: oldvine_cms/client/public/images
|
|
const devImagesRoot = path.join(__dirname, '../../client/public/images');
|
|
|
|
// 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 {
|
|
await fs.mkdir(dir, { recursive: true });
|
|
}
|
|
};
|
|
|
|
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 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);
|
|
const basename = path
|
|
.basename(file.originalname, ext)
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]/g, '-')
|
|
.replace(/-+/g, '-')
|
|
.replace(/^-|-$/g, '');
|
|
cb(null, `${basename || 'media'}-${uniqueSuffix}${ext.toLowerCase()}`);
|
|
},
|
|
});
|
|
|
|
const fileFilter = (req, file, cb) => {
|
|
const allowed = [
|
|
'image/jpeg',
|
|
'image/jpg',
|
|
'image/png',
|
|
'image/gif',
|
|
'image/webp',
|
|
'image/svg+xml',
|
|
'video/mp4',
|
|
'video/quicktime',
|
|
'application/pdf',
|
|
];
|
|
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 },
|
|
});
|
|
|
|
// 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' });
|
|
|
|
const folder = safeFolder(req.body.folder || 'general');
|
|
const { alt, caption, description, tags } = req.body;
|
|
|
|
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';
|
|
|
|
const media = new Media({
|
|
filename: req.file.filename,
|
|
originalName: req.file.originalname,
|
|
url: `/images/${folder}/${req.file.filename}`,
|
|
mimeType: req.file.mimetype,
|
|
size: req.file.size,
|
|
type: mediaType,
|
|
folder,
|
|
alt,
|
|
caption,
|
|
description,
|
|
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 } });
|
|
} catch (error) {
|
|
console.error('Upload error:', error);
|
|
res.status(500).json({ success: false, message: error.message || 'Error uploading file' });
|
|
}
|
|
});
|
|
|
|
// 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 = 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 media = new Media({
|
|
filename: file.filename,
|
|
originalName: file.originalname,
|
|
url: `/images/${folder}/${file.filename}`,
|
|
mimeType: file.mimetype,
|
|
size: file.size,
|
|
type: mediaType,
|
|
folder,
|
|
uploadedBy: req.admin.id,
|
|
});
|
|
|
|
await media.save();
|
|
mediaRecords.push(media);
|
|
}
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: `${mediaRecords.length} files uploaded successfully`,
|
|
data: { media: mediaRecords },
|
|
});
|
|
} catch (error) {
|
|
console.error('Multiple upload error:', error);
|
|
res.status(500).json({ success: false, message: error.message || 'Error uploading files' });
|
|
}
|
|
});
|
|
|
|
// GET /api/media
|
|
router.get('/', adminAuth, async (req, res) => {
|
|
try {
|
|
const { folder, type, page = 1, limit = 50 } = req.query;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const query = {};
|
|
if (folder) query.folder = folder;
|
|
if (type) query.type = type;
|
|
|
|
const media = await Media.find(query)
|
|
.populate('uploadedBy', 'firstName lastName')
|
|
.sort({ createdAt: -1 })
|
|
.skip(skip)
|
|
.limit(parseInt(limit, 10));
|
|
|
|
const total = await Media.countDocuments(query);
|
|
|
|
res.json({
|
|
success: true,
|
|
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' });
|
|
}
|
|
});
|
|
|
|
// 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' });
|
|
|
|
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);
|
|
res.status(500).json({ success: false, message: 'Error searching media' });
|
|
}
|
|
});
|
|
|
|
// PUT /api/media/:id
|
|
router.put('/:id', adminAuth, async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { alt, caption, description, tags, folder } = req.body;
|
|
|
|
const updateFields = {};
|
|
if (alt !== undefined) updateFields.alt = alt;
|
|
if (caption !== undefined) updateFields.caption = caption;
|
|
if (description !== undefined) updateFields.description = description;
|
|
if (tags !== undefined) updateFields.tags = tags;
|
|
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 } });
|
|
} catch (error) {
|
|
console.error('Update media error:', error);
|
|
res.status(500).json({ success: false, message: 'Error updating media' });
|
|
}
|
|
});
|
|
|
|
// 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' });
|
|
|
|
// media.url example: /images/general/file.jpg
|
|
const rel = media.url.replace(/^\/images\//, '');
|
|
const filePath = path.join(imagesRoot, rel);
|
|
|
|
try {
|
|
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);
|
|
res.status(500).json({ success: false, message: 'Error deleting media' });
|
|
}
|
|
});
|
|
|
|
// 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 } });
|
|
} catch (error) {
|
|
console.error('Get folders error:', error);
|
|
res.status(500).json({ success: false, message: 'Error fetching folders' });
|
|
}
|
|
});
|
|
|
|
module.exports = router; |