301 lines
10 KiB
JavaScript
301 lines
10 KiB
JavaScript
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);
|
|
};
|
|
}
|
|
|
|
// Специальная конфигурация для музыки (больший лимит размера)
|
|
const musicMulterConfig = {
|
|
storage: tempStorage,
|
|
limits: {
|
|
fileSize: 105 * 1024 * 1024, // 105MB для ZIP альбомов
|
|
files: 1
|
|
},
|
|
fileFilter: (req, file, cb) => {
|
|
const allowedMimeTypes = ['audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/ogg', 'audio/mp4', 'audio/m4a', 'application/zip', 'application/x-zip-compressed'];
|
|
const ext = path.extname(file.originalname).toLowerCase();
|
|
const allowedExts = ['.mp3', '.wav', '.ogg', '.m4a', '.flac', '.zip'];
|
|
|
|
if (allowedMimeTypes.includes(file.mimetype) || allowedExts.includes(ext)) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(new Error('Неподдерживаемый формат файла. Разрешены: MP3, WAV, OGG, M4A, FLAC, ZIP'));
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Middleware для загрузки музыки (треки и ZIP альбомы)
|
|
*/
|
|
function createMusicUploadMiddleware(fieldName = 'track') {
|
|
const upload = multer(musicMulterConfig);
|
|
const multerMiddleware = upload.single(fieldName);
|
|
|
|
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 });
|
|
}
|
|
|
|
if (!req.file) {
|
|
return next();
|
|
}
|
|
|
|
try {
|
|
// Если MinIO включен, загрузить туда
|
|
if (isMinioEnabled()) {
|
|
try {
|
|
const fileUrl = await uploadFile(
|
|
req.file.buffer,
|
|
req.file.originalname,
|
|
req.file.mimetype,
|
|
'music'
|
|
);
|
|
req.uploadedMusicFile = {
|
|
url: fileUrl,
|
|
buffer: req.file.buffer,
|
|
originalname: req.file.originalname,
|
|
mimetype: req.file.mimetype,
|
|
size: req.file.size
|
|
};
|
|
req.uploadMethod = 'minio';
|
|
|
|
log('info', 'Музыкальный файл загружен в MinIO', {
|
|
filename: req.file.originalname,
|
|
size: req.file.size,
|
|
url: fileUrl
|
|
});
|
|
} catch (uploadError) {
|
|
log('error', 'Ошибка загрузки в MinIO', {
|
|
error: uploadError.message,
|
|
filename: req.file.originalname
|
|
});
|
|
throw uploadError;
|
|
}
|
|
} else {
|
|
// Локальное хранилище (fallback)
|
|
const uploadDir = path.join(__dirname, '../uploads/music');
|
|
|
|
if (!fs.existsSync(uploadDir)) {
|
|
fs.mkdirSync(uploadDir, { recursive: true });
|
|
}
|
|
|
|
const timestamp = Date.now();
|
|
const random = Math.round(Math.random() * 1E9);
|
|
const ext = path.extname(req.file.originalname);
|
|
const filename = `${timestamp}-${random}${ext}`;
|
|
const filepath = path.join(uploadDir, filename);
|
|
|
|
fs.writeFileSync(filepath, req.file.buffer);
|
|
|
|
const relativePath = `/uploads/music/${filename}`;
|
|
req.uploadedMusicFile = {
|
|
url: relativePath,
|
|
path: filepath,
|
|
buffer: req.file.buffer,
|
|
originalname: req.file.originalname,
|
|
mimetype: req.file.mimetype,
|
|
size: req.file.size
|
|
};
|
|
req.uploadMethod = 'local';
|
|
|
|
log('info', 'Музыкальный файл загружен локально', {
|
|
filename,
|
|
path: relativePath
|
|
});
|
|
}
|
|
|
|
next();
|
|
} catch (error) {
|
|
log('error', 'Ошибка обработки музыкального файла', { error: error.message });
|
|
return res.status(500).json({ error: 'Ошибка загрузки файла' });
|
|
}
|
|
});
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
createUploadMiddleware,
|
|
cleanupOnError,
|
|
createMusicUploadMiddleware,
|
|
|
|
// Готовые middleware для разных случаев
|
|
uploadPostImages: createUploadMiddleware('images', 5, 'posts'),
|
|
uploadAvatar: createUploadMiddleware('avatar', 1, 'avatars'),
|
|
uploadChannelMedia: createUploadMiddleware('images', 10, 'channel'),
|
|
uploadMusicTrack: createMusicUploadMiddleware('track'),
|
|
uploadMusicAlbum: createMusicUploadMiddleware('album')
|
|
};
|
|
|