import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; import { v4 as uuidv4 } from 'uuid'; import multer from 'multer'; import sharp from 'sharp'; import { logger } from '../config/logger'; import { imageResolutions } from '../config/imageResolutions'; // Конфигурация для подключения к S3 const BUCKET_NAME = process.env.AWS_S3_BUCKET || ''; const REGION = process.env.AWS_REGION || 'ru-central1'; const AWS_ENDPOINT = process.env.AWS_ENDPOINT || ''; const ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID || ''; const SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY || ''; // Создаем клиент S3 export const s3Client = new S3Client({ region: REGION, endpoint: AWS_ENDPOINT, credentials: { accessKeyId: ACCESS_KEY_ID, secretAccessKey: SECRET_ACCESS_KEY, }, }); // Наименование бакета export const bucketName = BUCKET_NAME; // Функция для изменения размеров и оптимизации изображения const resizeAndOptimizeImage = async (buffer: Buffer, width: number, height: number, quality: number): Promise => { return await sharp(buffer) .resize(width, height, { fit: sharp.fit.inside }) .webp({ quality }) .toBuffer(); }; // Функция для получения конфигурации Multer-S3 const getMulterS3Config = ( s3Client: S3Client, bucketName: string ) => { return multer({ storage: multer.memoryStorage(), // Храним файл в памяти перед обработкой fileFilter: (req, file, cb) => { if (!file.mimetype.startsWith('image/')) { return cb(new Error('Разрешены только файлы изображений!')); } cb(null, true); }, }).single('file'); // Обрабатываем один файл }; // Middleware по умолчанию, использующее дефолтовый S3 клиент export const multerUpload = getMulterS3Config(s3Client, bucketName); // Функция для загрузки файла в S3 после оптимизации export const uploadToS3 = async ( s3Client: S3Client, bucketName: string, folder: string, file: Express.Multer.File, resolutionId: string, quality: number ) => { try { const selectedResolution = imageResolutions.find(r => r.id === resolutionId); if (!selectedResolution) { throw new Error('Недопустимое разрешение изображения'); } // Оптимизируем изображение const optimizedBuffer = await resizeAndOptimizeImage( file.buffer, selectedResolution.width, selectedResolution.height, quality ); // Генерируем уникальное имя файла const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); const fileNameWithoutExt = file.originalname.replace(/\.[^.]+$/, ''); const fileName = `${folder}/${uniqueSuffix}-${fileNameWithoutExt}.webp`; // Загружаем файл в S3 const command = new PutObjectCommand({ Bucket: bucketName, Key: fileName, Body: optimizedBuffer, ContentType: file.mimetype, ACL: 'public-read', }); await s3Client.send(command); // Возвращаем URL загруженного файла return `https://${bucketName}.s3.regru.cloud/${fileName}`; } catch (error) { throw new Error(`Ошибка загрузки файла в S3: ${error}`); } }; /** * Функция для загрузки буфера в S3 * @param buffer - Буфер для загрузки * @param folder - Путь к папке в S3 * @param filename - Опциональное имя файла (если пусто, то будет сгенерирован UUID как имя) * @param mimetype - MIME тип контента буфера * @param resolutionId - Опциональный идентификатор разрешения изображения * @param quality - Отцтональное значение качества при оптимизации * @returns URL загрущенного файла */ export const uploadBufferToS3 = async ( buffer: Buffer, folder: string, filename?: string, mimetype?: string, resolutionId?: string, quality: number = 80 ): Promise => { try { // Генерация имени файла, если оно не задано const finalFilename = filename || `${uuidv4()}${mimetype ? getExtensionFromMimeType(mimetype) : '.bin'}`; // Создание ключа S3 (путь в бакете) const key = folder ? `${folder}/${finalFilename}` : finalFilename; const selectedResolution = imageResolutions.find(r => r.id === resolutionId); if (!selectedResolution) { throw new Error('Недопустимое разрешение изображения'); } // Оптимизируем изображение const optimizedBuffer = await resizeAndOptimizeImage( buffer, selectedResolution.width, selectedResolution.height, quality ); // Загрузка буфера в S3 await s3Client.send( new PutObjectCommand({ Bucket: bucketName, Key: key, Body: optimizedBuffer, ContentType: mimetype, }) ); // Возвращаем URL файла const fileUrl = `https://${bucketName}.s3.regru.cloud/${finalFilename}`; logger.info(`Buffer successfully uploaded to S3: ${key}`); return fileUrl; } catch (error) { logger.error('Error uploading buffer to S3'); console.error(error); throw new Error('Error uploading buffer to S3'); } }; // Функция получения пасширения файла из MIME типа function getExtensionFromMimeType(mimetype: string): string { const mimeToExt: Record = { 'image/jpeg': '.jpg', 'image/png': '.png', 'image/gif': '.gif', 'image/webp': '.webp', 'image/svg+xml': '.svg', 'application/pdf': '.pdf', 'text/plain': '.txt', 'text/html': '.html', 'application/json': '.json', }; return mimeToExt[mimetype] || ''; } // Упрощенная версия uploadToS3 которая использует клиента и бакет по умолчанию export const uploadFile = async ( folder: string, file: Express.MulterS3.File, resolutionId: string, quality: number = 80 ): Promise => { return uploadToS3(s3Client, bucketName, folder, file, resolutionId, quality); };