173 lines
4.9 KiB
JavaScript
173 lines
4.9 KiB
JavaScript
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;
|
|
}
|