188 lines
6.5 KiB
TypeScript

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<Buffer> => {
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<string> => {
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<string, string> = {
'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<string> => {
return uploadToS3(s3Client, bucketName, folder, file, resolutionId, quality);
};