Работает загрузк файлов на S3

This commit is contained in:
anibilag 2025-02-03 23:28:05 +03:00
parent d184307981
commit 64df06db36
5 changed files with 850 additions and 203 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
node_modules node_modules
logs
*.log
.idea
# Keep environment variables out of version control # Keep environment variables out of version control
.env .env

736
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { AuthRequest } from '../../../middleware/auth'; import { AuthRequest } from '../../../middleware/auth';
import { galleryService } from '../../../services/galleryService'; import { galleryService } from '../../../services/galleryService';
import { s3Service } from '../../../services/s3Service';
import { logger } from '../../../config/logger'; import { logger } from '../../../config/logger';
export async function createGalleryImage(req: AuthRequest, res: Response) { export async function createGalleryImage(req: AuthRequest, res: Response) {

View File

@ -1,95 +1,52 @@
import express from 'express'; import express from 'express';
import multer from 'multer';
import multerS3 from 'multer-s3';
import { auth } from '../../middleware/auth'; import { auth } from '../../middleware/auth';
import {createS3Client, s3Service} from '../../services/s3MulterService'; import {createS3Client, getMulterS3Config, uploadToS3} from '../../services/s3Service';
import { logger } from '../../config/logger'; import { logger } from '../../config/logger';
import { imageResolutions } from '../../config/imageResolutions';
const router = express.Router(); const router = express.Router();
//const upload = multer();
// Инициализация Multer-S3 // Конфигурация для подключения к S3
const s3Client = createS3Client(); // Экспортируем клиент из s3Service const BUCKET_NAME = process.env.AWS_S3_BUCKET || '';
const upload = multer({ const REGION = process.env.AWS_REGION || 'ru-central1'
storage: multerS3({ const AWS_ENDPOINT = process.env.AWS_ENDPOINT || ''
s3: s3Client, const ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID || '';
bucket: process.env.AWS_S3_BUCKET || '', const SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY || '';
metadata: (req, file, cb) => {
cb(null, { fieldName: file.fieldname });
},
key: (req, file, cb) => {
const fileName = `${Date.now()}-${file.originalname}`;
cb(null, `uploads/${fileName}`);
},
contentType: multerS3.AUTO_CONTENT_TYPE,
}),
});
/* // Создаем клиент S3
router.post('/upload-url', auth, async (req, res) : Promise<void> => { const s3Client = createS3Client(REGION, AWS_ENDPOINT, ACCESS_KEY_ID, SECRET_ACCESS_KEY);
try {
const { fileName, fileType, resolution } = req.body;
if (!fileName || !fileType || !resolution) { // Настройка Multer
res.status(400).json({ error: 'Missing required fields' }); const upload = getMulterS3Config(s3Client, BUCKET_NAME);
// Роут для загрузки изображения
router.post('/upload-url', auth, upload, async (req, res): Promise<void> => {
if (!req.file) {
res.status(400).json({ error: 'No file uploaded' });
return return
} }
const selectedResolution = imageResolutions.find(r => r.id === resolution);
if (!selectedResolution) {
res.status(400).json({ error: 'Invalid resolution' });
return
}
const { uploadUrl, imageId, key } = await s3Service.getUploadUrl(fileName, fileType);
logger.info(`Generated upload URL for image: ${fileName}`);
res.json({ uploadUrl, imageId, key });
} catch (error) {
logger.error('Error generating upload URL:', error);
res.status(500).json({ error: 'Failed to generate upload URL' });
}
});
*/
// Маршрут для загрузки изображения
router.post('/upload-url', auth, upload.single('file'), async (req, res): Promise<void> => {
try {
const file = req.file as Express.MulterS3.File; const file = req.file as Express.MulterS3.File;
const { resolutionId } = req.body; const folder = req.body.folder as string;
const resolutionId = req.body.resolutionId as string;
const quality = 80;
if (!file) { try {
res.status(400).json({ error: 'Файл не найден' }); // Загружаем файл в S3 после оптимизации
return; const fileUrl = await uploadToS3(s3Client, BUCKET_NAME, folder, file, resolutionId, quality);
} res.json({ message: 'Файл успешно загружен', fileUrl });
// Сохраняем оригинал и обрабатываем изображение
const originalKey = await s3Service.uploadOriginalFile(file);
const optimizedResult = await s3Service.optimizeAndUpload(file.buffer, originalKey, resolutionId);
res.status(200).json({
message: 'Файл успешно загружен и оптимизирован',
originalKey,
optimizedKey: optimizedResult.key,
dimensions: {
width: optimizedResult.width,
height: optimizedResult.height,
},
format: optimizedResult.format,
size: optimizedResult.size,
});
} catch (error) { } catch (error) {
logger.error('Ошибка загрузки изображения:', error); logger.error(`Ошибка загрузки файла в S3: ${file.key}`);
res.status(500).json({ error: 'Ошибка загрузки изображения' }); console.error(error);
res.status(500).json({ error: 'Ошибка загрузки файла в S3' });
} }
}); });
/* /*
router.get('/:id', auth, async (req, res) => { router.get('/:id', auth, async (req, res) => {
try { try {
const { id } = req.params; const { id } = req.params;
const image = await s3Service.getImage(id); const image = await getImage(id, BUCKET_NAME);
res.json(image); res.json(image);
} catch (error) { } catch (error) {
logger.error('Error fetching image:', error); logger.error('Error fetching image:', error);
@ -97,4 +54,5 @@ router.get('/:id', auth, async (req, res) => {
} }
}); });
*/ */
export default router; export default router;

View File

@ -1,128 +1,105 @@
import { S3Client, PutObjectCommand, GetObjectCommand, PutBucketCorsCommand } from '@aws-sdk/client-s3'; import { S3Client, PutObjectCommand, GetObjectCommand} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { v4 as uuidv4 } from 'uuid'; import multer from 'multer';
import sharp from 'sharp'; import sharp from 'sharp';
import { logger } from '../config/logger'; import { imageResolutions } from '../config/imageResolutions';
// Инициализация клиента S3 // Функция для создания клиента S3
const s3Client = new S3Client({ export const createS3Client = (region: string, endpoint: string, accessKeyId: string, secretAccessKey: string) => {
region: process.env.AWS_REGION || 'ru-central1', return new S3Client({
endpoint: process.env.AWS_ENDPOINT || '', region: region,
endpoint: endpoint,
credentials: { credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', accessKeyId: accessKeyId,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '' secretAccessKey: secretAccessKey,
},
forcePathStyle: true
});
const BUCKET_NAME = process.env.AWS_S3_BUCKET || '';
// Конфигурация CORS
const corsConfig = {
Bucket: BUCKET_NAME,
CORSConfiguration: {
CORSRules: [
{
AllowedOrigins: ['http://127.0.0.1:5173'], // Домен фронтенда
AllowedMethods: ['GET', 'PUT', 'POST', 'DELETE'], // Разрешенные методы
AllowedHeaders: ['*'], // Разрешенные заголовки
ExposeHeaders: ['ETag', 'x-amz-meta-custom-header'], // Заголовки в ответе
MaxAgeSeconds: 3000, // Кеширование CORS
},
],
},
};
// Применение конфигурации
const applyCors = async () => {
try {
const command = new PutBucketCorsCommand(corsConfig);
await s3Client.send(command);
console.log('CORS успешно настроен.');
} catch (error) {
console.error('Ошибка при настройке CORS:', error);
} }
});
}; };
//applyCors(); // Функция для изменения размеров и оптимизации изображения
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();
};
export const s3Service = { // Функция для получения конфигурации Multer-S3
getUploadUrl: async (fileName: string, fileType: string) => { export const getMulterS3Config = (
const imageId = uuidv4(); s3Client: S3Client,
const key = `uploads/${imageId}-${fileName}`; 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'); // Обрабатываем один файл
};
// Функция для загрузки файла в 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({ const command = new PutObjectCommand({
Bucket: BUCKET_NAME, Bucket: bucketName,
Key: key, Key: fileName,
ContentType: fileType Body: optimizedBuffer,
ContentType: file.mimetype,
ACL: 'public-read',
}); });
try { await s3Client.send(command);
const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 });
logger.info(`Generated pre-signed URL for upload: ${key}`);
return { uploadUrl, imageId, key };
} catch (error) {
logger.error('Error generating pre-signed URL:', error);
throw error;
}
},
getImage: async (imageId: string) => { // Возвращаем URL загруженного файла
return `https://${bucketName}.s3.regru.cloud/${fileName}`;
} catch (error) {
throw new Error(`Ошибка загрузки файла в S3: ${error}`);
}
};
/*
export const getImage = async (imageId: string, bucketName: string) => {
try { try {
const command = new GetObjectCommand({ const command = new GetObjectCommand({
Bucket: BUCKET_NAME, Bucket: bucketName,
Key: `uploads/${imageId}` Key: `uploads/${imageId}`
}); });
const url = await getSignedUrl(s3Client, command, { expiresIn: 3600 }); const url = await getSignedUrl(s3Client, command, { expiresIn: 3600 });
return { url }; return { url };
} catch (error) { } catch (error) {
logger.error('Error getting image:', error);
throw error; throw error;
} }
},
optimizeAndUpload: async (buffer: Buffer, key: string, resolution: { width: number; height: number }) => {
try {
let sharpInstance = sharp(buffer);
// Get image metadata
const metadata = await sharpInstance.metadata();
// Resize if resolution is specified
if (resolution.width > 0 && resolution.height > 0) {
sharpInstance = sharpInstance.resize(resolution.width, resolution.height, {
fit: 'inside',
withoutEnlargement: true
});
}
// Convert to WebP for better compression
const optimizedBuffer = await sharpInstance
.webp({ quality: 80 })
.toBuffer();
// Upload optimized image
const optimizedKey = key.replace(/\.[^/.]+$/, '.webp');
await s3Client.send(new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: optimizedKey,
Body: optimizedBuffer,
ContentType: 'image/webp'
}));
logger.info(`Successfully optimized and uploaded image: ${optimizedKey}`);
return {
key: optimizedKey,
width: metadata.width,
height: metadata.height,
format: 'webp',
size: optimizedBuffer.length
};
} catch (error) {
logger.error('Error optimizing and uploading image:', error);
throw error;
}
}
}; };
*/