const multer = require('multer'); const path = require('path'); const { uploadFile, isEnabled: isMinioEnabled } = require('../utils/minio'); const { log } = require('./logger'); const fs = require('fs'); // Временное хранилище для файлов const tempStorage = multer.memoryStorage(); // Конфигурация multer const multerConfig = { storage: tempStorage, limits: { fileSize: 10 * 1024 * 1024, // 10MB files: 10 }, fileFilter: (req, file, cb) => { // Запрещенные расширения (исполняемые файлы) const forbiddenExts = [ '.exe', '.bat', '.cmd', '.sh', '.ps1', '.js', '.jar', '.app', '.dmg', '.deb', '.rpm', '.msi', '.scr', '.vbs', '.com', '.pif', '.cpl' ]; const ext = path.extname(file.originalname).toLowerCase(); // Проверить на запрещенные расширения if (forbiddenExts.includes(ext)) { return cb(new Error('Запрещенный тип файла')); } // Разрешенные типы изображений и видео const allowedMimes = [ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'video/mp4', 'video/quicktime', 'video/x-msvideo' ]; if (!allowedMimes.includes(file.mimetype)) { return cb(new Error('Только изображения и видео разрешены')); } cb(null, true); } }; /** * Middleware для загрузки файлов * Автоматически загружает в MinIO если включен, иначе локально */ function createUploadMiddleware(fieldName, maxCount = 5, folder = 'posts') { const upload = multer(multerConfig); const multerMiddleware = maxCount === 1 ? upload.single(fieldName) : upload.array(fieldName, maxCount); return async (req, res, next) => { multerMiddleware(req, res, async (err) => { if (err) { log('error', 'Ошибка multer', { error: err.message }); return res.status(400).json({ error: err.message }); } try { // Проверить наличие файлов const files = req.files || (req.file ? [req.file] : []); if (!files.length) { return next(); } // Если MinIO включен, загрузить туда if (isMinioEnabled()) { const uploadedUrls = []; for (const file of files) { try { const fileUrl = await uploadFile( file.buffer, file.originalname, file.mimetype, folder ); uploadedUrls.push(fileUrl); } catch (uploadError) { log('error', 'Ошибка загрузки в MinIO', { error: uploadError.message, filename: file.originalname }); throw uploadError; } } // Сохранить URLs в req для дальнейшей обработки req.uploadedFiles = uploadedUrls; req.uploadMethod = 'minio'; log('info', 'Файлы загружены в MinIO', { count: uploadedUrls.length, folder }); } else { // Локальное хранилище (fallback) const uploadDir = path.join(__dirname, '../uploads', folder); // Создать директорию если не существует if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } const uploadedPaths = []; for (const file of files) { const timestamp = Date.now(); const random = Math.round(Math.random() * 1E9); const ext = path.extname(file.originalname); const filename = `${timestamp}-${random}${ext}`; const filepath = path.join(uploadDir, filename); // Сохранить файл fs.writeFileSync(filepath, file.buffer); // Относительный путь для URL const relativePath = `/uploads/${folder}/${filename}`; uploadedPaths.push(relativePath); } req.uploadedFiles = uploadedPaths; req.uploadMethod = 'local'; log('info', 'Файлы загружены локально', { count: uploadedPaths.length, folder }); } next(); } catch (error) { log('error', 'Ошибка обработки загруженных файлов', { error: error.message }); return res.status(500).json({ error: 'Ошибка загрузки файлов' }); } }); }; } /** * Middleware для удаления файлов из MinIO при ошибке */ function cleanupOnError() { return (err, req, res, next) => { if (req.uploadedFiles && req.uploadMethod === 'minio') { const { deleteFiles } = require('../utils/minio'); deleteFiles(req.uploadedFiles).catch(cleanupErr => { log('error', 'Ошибка очистки файлов MinIO', { error: cleanupErr.message }); }); } next(err); }; } module.exports = { createUploadMiddleware, cleanupOnError, // Готовые middleware для разных случаев uploadPostImages: createUploadMiddleware('images', 5, 'posts'), uploadAvatar: createUploadMiddleware('avatar', 1, 'avatars'), uploadChannelMedia: createUploadMiddleware('images', 10, 'channel') };