import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { env } from '../config/env.js'; // S3 client for internal server-to-server communication const s3Client = new S3Client({ endpoint: env.S3_ENDPOINT, credentials: { accessKeyId: env.S3_ACCESS_KEY, secretAccessKey: env.S3_SECRET_KEY, }, region: 'us-east-1', forcePathStyle: true, // Оптимизация для больших файлов requestHandler: { connectionTimeout: 300000, // 5 минут socketTimeout: 600000, // 10 минут }, }); // S3 client for generating presigned URLs for browser access // Uses the same credentials but will generate URLs with public endpoint const s3ClientForPresigned = new S3Client({ endpoint: env.S3_ENDPOINT, credentials: { accessKeyId: env.S3_ACCESS_KEY, secretAccessKey: env.S3_SECRET_KEY, }, region: 'us-east-1', forcePathStyle: true, }); export async function uploadFile(key, body, contentType) { const command = new PutObjectCommand({ Bucket: env.S3_BUCKET, Key: key, Body: body, ContentType: contentType, }); await s3Client.send(command); return key; } export async function getPresignedUrl(key, expiresIn = env.S3_PRESIGNED_URL_EXPIRES_IN) { const command = new GetObjectCommand({ Bucket: env.S3_BUCKET, Key: key, }); const signedUrl = await getSignedUrl(s3Client, command, { expiresIn }); return signedUrl; } export async function deleteFile(key) { const command = new DeleteObjectCommand({ Bucket: env.S3_BUCKET, Key: key, }); await s3Client.send(command); return true; } export function getImageInputKey(filename, userId, folder = env.S3_IMAGES_INPUT_FOLDER) { const timestamp = Date.now(); const ext = filename.split('.').pop() || 'jpg'; return `${folder}/${userId}/${timestamp}-${filename}`; } export function getVideoInputKey(filename, userId, folder = 'videos_input') { const timestamp = Date.now(); const ext = filename.split('.').pop() || 'mp4'; return `${folder}/${userId}/${timestamp}-${filename}`; } // ────────────────────────────── // Multipart Upload для прямой загрузки в S3 // ────────────────────────────── /** * Инициировать multipart upload */ export async function createMultipartUpload(key, contentType) { const command = new CreateMultipartUploadCommand({ Bucket: env.S3_BUCKET, Key: key, ContentType: contentType, }); const result = await s3Client.send(command); return { uploadId: result.UploadId, key, }; } /** * Создать presigned URL для загрузки части файла * Возвращает URL для прямой загрузки из браузера через nginx proxy */ export async function getPresignedPartUrl(key, uploadId, partNumber, expiresIn = 3600) { const command = new UploadPartCommand({ Bucket: env.S3_BUCKET, Key: key, UploadId: uploadId, PartNumber: partNumber, }); // Для presigned URL используем endpoint который совпадает с public endpoint // Браузер будет отправлять запрос на https://uno-click.pip-test.ru/s3-upload/uno-click/ // Нginx будет проксировать на minio:9000/uno-click/ // MinIO проверяет подпись используя только путь и query параметры, не host const signedUrl = await getSignedUrl(s3ClientForPresigned, command, { expiresIn }); // Заменяем внутренний endpoint на публичный для доступа из браузера // Превращаем http://minio:9000/uno-click/... в https://uno-click.pip-test.ru/s3-upload/uno-click/... const publicUrl = signedUrl.replace( `${env.S3_ENDPOINT}/${env.S3_BUCKET}/`, env.S3_PUBLIC_ENDPOINT ); return publicUrl; } /** * Завершить multipart upload */ export async function completeMultipartUpload(key, uploadId, parts) { const command = new CompleteMultipartUploadCommand({ Bucket: env.S3_BUCKET, Key: key, UploadId: uploadId, MultipartUpload: { Parts: parts.map((p, index) => ({ PartNumber: index + 1, ETag: p.ETag, })), }, }); const result = await s3Client.send(command); return { key, location: result.Location, bucket: result.Bucket, etag: result.ETag, }; } /** * Отменить multipart upload */ export async function abortMultipartUpload(key, uploadId) { const command = new AbortMultipartUploadCommand({ Bucket: env.S3_BUCKET, Key: key, UploadId: uploadId, }); await s3Client.send(command); return true; }