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', // Дополнительные форматы изображений 'image/bmp', 'image/x-ms-bmp', 'image/tiff', 'image/tif', 'image/heic', 'image/heif', 'image/avif', 'image/x-icon', // Видео форматы 'video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/webm', 'video/x-matroska', 'video/avi' ]; // Также проверяем по расширению файла (на случай неправильного MIME типа) const allowedExts = [ '.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff', '.tif', '.heic', '.heif', '.avif', '.ico', '.mp4', '.mov', '.avi', '.webm', '.mkv' ]; const fileExt = ext.toLowerCase(); const isValidMime = allowedMimes.includes(file.mimetype); const isValidExt = allowedExts.includes(fileExt); if (!isValidMime && !isValidExt) { return cb(new Error(`Неподдерживаемый формат файла. Разрешены: JPEG, PNG, GIF, WebP, BMP, HEIC, MP4, MOV, AVI, WebM. Получен: ${file.mimetype || 'неизвестно'}`)); } 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') };