Files
oldvine_cms/routes/media.js
2026-02-15 14:21:19 +03:00

396 lines
10 KiB
JavaScript

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 PUBLIC_BASE = process.env.UPLOAD_PUBLIC_URL || '/uploads';
const MEDIA_ROOT = process.env.MEDIA_SUBDIR || 'images';
// Ensure upload directories exist
const ensureUploadDir = async (dir) => {
try {
await fs.access(dir);
} catch {
await fs.mkdir(dir, { recursive: true });
}
};
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';
};
// 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);
cb(null, uploadPath);
} catch (e) {
cb(e);
}
},
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 || 'file') + '-' + uniqueSuffix + ext.toLowerCase());
},
});
// File filter
const fileFilter = (req, file, cb) => {
const allowedMimeTypes = [
'image/jpeg',
'image/jpg',
'image/png',
'image/gif',
'image/webp',
'image/svg+xml',
'video/mp4',
'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);
}
};
const upload = multer({
storage,
fileFilter,
limits: {
fileSize: 10 * 1024 * 1024, // 10MB limit
},
});
// @route POST /api/media/upload
// @desc Upload media file
// @access Private (Admin)
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 folderSafe = sanitizeFolder(req.body.folder);
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';
}
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,
mimeType: req.file.mimetype,
size: req.file.size,
type: mediaType,
folder: folderSafe,
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',
});
}
});
// @route POST /api/media/upload-multiple
// @desc Upload multiple media files
// @access Private (Admin)
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 folderSafe = sanitizeFolder(req.body.folder);
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}`;
const media = new Media({
filename: file.filename,
originalName: file.originalname,
url,
mimeType: file.mimetype,
size: file.size,
type: mediaType,
folder: folderSafe,
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',
});
}
});
// @route GET /api/media
// @desc Get all media
// @access Private (Admin)
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',
});
}
});
// @route GET /api/media/search
// @desc Search media
// @access Private (Admin)
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',
});
}
});
// @route PUT /api/media/:id
// @desc Update media metadata
// @access Private (Admin)
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 = sanitizeFolder(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',
});
}
});
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)
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 (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);
}
}
} 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',
});
}
});
// @route GET /api/media/folders/list
// @desc Get list of all folders
// @access Private (Admin)
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;