nakama/backend/middleware/upload.js

170 lines
5.5 KiB
JavaScript
Raw Normal View History

2025-11-20 22:07:37 +00:00
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')
};