From 19b40b91609d3cfc9584b4ccd4aedef2628e253d Mon Sep 17 00:00:00 2001 From: glpshchn <464976@niuitmo.ru> Date: Wed, 3 Dec 2025 01:31:02 +0300 Subject: [PATCH] Update files --- backend/utils/minio.js | 81 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 6 deletions(-) diff --git a/backend/utils/minio.js b/backend/utils/minio.js index 370359f..62ca534 100644 --- a/backend/utils/minio.js +++ b/backend/utils/minio.js @@ -94,6 +94,67 @@ async function ensureBucket() { } } +/** + * Нормализовать имя файла для MinIO (только ASCII, транслитерация кириллицы) + * @param {string} filename - Оригинальное имя файла + * @returns {string} - Безопасное имя файла + */ +function normalizeFilename(filename) { + if (!filename) return 'file'; + + // Извлечь расширение + const ext = filename.split('.').pop() || ''; + const nameWithoutExt = filename.substring(0, filename.lastIndexOf('.')) || filename; + + // Простая транслитерация кириллицы + const translitMap = { + 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo', + 'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', + 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', + 'ф': 'f', 'х': 'h', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'sch', + 'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya', + 'А': 'A', 'Б': 'B', 'В': 'V', 'Г': 'G', 'Д': 'D', 'Е': 'E', 'Ё': 'Yo', + 'Ж': 'Zh', 'З': 'Z', 'И': 'I', 'Й': 'Y', 'К': 'K', 'Л': 'L', 'М': 'M', + 'Н': 'N', 'О': 'O', 'П': 'P', 'Р': 'R', 'С': 'S', 'Т': 'T', 'У': 'U', + 'Ф': 'F', 'Х': 'H', 'Ц': 'Ts', 'Ч': 'Ch', 'Ш': 'Sh', 'Щ': 'Sch', + 'Ъ': '', 'Ы': 'Y', 'Ь': '', 'Э': 'E', 'Ю': 'Yu', 'Я': 'Ya' + }; + + // Транслитерировать имя + let normalized = nameWithoutExt + .split('') + .map(char => translitMap[char] || char) + .join(''); + + // Убрать все не-ASCII символы (оставить только буквы, цифры, дефис, подчеркивание) + normalized = normalized.replace(/[^a-zA-Z0-9_-]/g, '_'); + + // Если имя пустое после нормализации, использовать 'file' + if (!normalized || normalized.trim() === '') { + normalized = 'file'; + } + + // Вернуть с расширением (расширение тоже нормализуем) + const safeExt = ext.replace(/[^a-zA-Z0-9]/g, '') || 'bin'; + return safeExt ? `${normalized}.${safeExt}` : normalized; +} + +/** + * Извлечь расширение файла безопасным способом + * @param {string} filename - Имя файла + * @returns {string} - Расширение файла + */ +function getFileExtension(filename) { + if (!filename) return 'bin'; + + const parts = filename.split('.'); + if (parts.length < 2) return 'bin'; + + const ext = parts.pop().toLowerCase(); + // Оставить только безопасные символы в расширении + return ext.replace(/[^a-z0-9]/g, '') || 'bin'; +} + /** * Загрузить файл в MinIO через S3 SDK * @param {Buffer} buffer - Буфер файла @@ -108,10 +169,13 @@ async function uploadFile(buffer, filename, contentType, folder = 'posts') { } try { - // Генерировать уникальное имя файла + // Нормализовать имя файла для MinIO (только ASCII) + const safeFilename = normalizeFilename(filename); + const ext = getFileExtension(filename); + + // Генерировать уникальное имя файла (используем только timestamp и random, без оригинального имени) const timestamp = Date.now(); const random = Math.round(Math.random() * 1E9); - const ext = filename.split('.').pop(); const objectName = `${folder}/${timestamp}-${random}.${ext}`; // Загрузить файл через S3 SDK @@ -119,12 +183,13 @@ async function uploadFile(buffer, filename, contentType, folder = 'posts') { client: s3Client, params: { Bucket: config.minio.bucket, - Key: objectName, + Key: objectName, // Используем безопасное имя для Key Body: buffer, ContentType: contentType, CacheControl: 'public, max-age=31536000', // 1 год Metadata: { - originalname: filename, + // Сохраняем оригинальное имя в метаданных (URL-encoded для безопасности) + originalname: encodeURIComponent(filename), uploadedAt: new Date().toISOString() } } @@ -137,13 +202,17 @@ async function uploadFile(buffer, filename, contentType, folder = 'posts') { log('info', 'Файл загружен в MinIO через S3', { objectName, + originalName: filename, size: buffer.length, url: fileUrl }); return fileUrl; } catch (error) { - log('error', 'Ошибка загрузки файла в MinIO', { error: error.message }); + log('error', 'Ошибка загрузки файла в MinIO', { + error: error.message, + filename: filename + }); throw error; } } @@ -186,7 +255,7 @@ async function deleteFile(fileUrl) { * @returns {Promise} - Количество удаленных файлов */ async function deleteFiles(fileUrls) { - if (!minioClient || !fileUrls || !fileUrls.length) { + if (!s3Client || !fileUrls || !fileUrls.length) { return 0; }