Files
remotion_service/server/services/s3.ts
T
2026-05-14 02:23:02 +03:00

153 lines
3.9 KiB
TypeScript

import { serverConfig } from "@/srv/config";
import {
GetObjectCommand,
S3Client,
} from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { createReadStream, statSync } from "node:fs";
import path from "path";
const PRESIGN_EXPIRY_SECONDS = 3600 * 24;
const S3_REGION = "us-east-1";
const KILOBYTE = 1024;
const MEGABYTE = 1024 * KILOBYTE;
const UPLOAD_LOG_INTERVAL_BYTES = 25 * MEGABYTE;
const MULTIPART_PART_SIZE = 5 * MEGABYTE;
export type UploadProgress = {
uploadedBytes: number;
totalBytes: number;
percent: number;
};
function formatBytes(bytes: number): string {
if (bytes >= MEGABYTE) {
return `${(bytes / MEGABYTE).toFixed(1)} MB`;
}
if (bytes >= KILOBYTE) {
return `${(bytes / KILOBYTE).toFixed(1)} KB`;
}
return `${bytes} B`;
}
export class S3Service {
private client: S3Client;
private folder: string;
private folderToStore: string;
constructor(folderToStore: string, folder?: string) {
this.folderToStore = folderToStore;
this.folder = folder || "";
this.client = new S3Client({
credentials: {
accessKeyId: serverConfig.S3_ACCESS_KEY,
secretAccessKey: serverConfig.S3_SECRET_KEY,
},
endpoint: serverConfig.S3_ENDPOINT_URL,
forcePathStyle: true,
region: S3_REGION,
});
}
async getFileURL(s3Path: string): Promise<string> {
return getSignedUrl(
this.client,
new GetObjectCommand({
Bucket: serverConfig.S3_BUCKET_NAME,
Key: s3Path,
}),
{ expiresIn: PRESIGN_EXPIRY_SECONDS },
);
}
getFileName(s3Path: string): string {
return path.basename(s3Path);
}
async uploadFile(
localFilePath: string,
key: string,
onProgress?: (progress: UploadProgress) => void,
abortSignal?: AbortSignal,
): Promise<string> {
const s3Path = path.join(this.folder, this.folderToStore, key);
const totalBytes = statSync(localFilePath).size;
let lastLoggedBytes = 0;
let lastLoggedPercent = -1;
console.log(
`[s3] Upload starting: ${localFilePath} -> s3://${serverConfig.S3_BUCKET_NAME}/${s3Path} (${formatBytes(totalBytes)})`,
);
const fileStream = createReadStream(localFilePath);
const upload = new Upload({
client: this.client,
params: {
Body: fileStream,
Bucket: serverConfig.S3_BUCKET_NAME,
Key: s3Path,
},
partSize: MULTIPART_PART_SIZE,
});
const onAbort = () => upload.abort();
if (abortSignal) {
if (abortSignal.aborted) {
upload.abort();
} else {
abortSignal.addEventListener("abort", onAbort, { once: true });
}
}
upload.on("httpUploadProgress", (progress) => {
const uploadedBytes = progress.loaded ?? 0;
const percent = totalBytes > 0 ? uploadedBytes / totalBytes : 1;
const percentValue = Math.floor(percent * 100);
onProgress?.({
uploadedBytes,
totalBytes,
percent: Math.min(percent, 1),
});
if (
uploadedBytes === totalBytes ||
uploadedBytes - lastLoggedBytes >= UPLOAD_LOG_INTERVAL_BYTES ||
percentValue >= lastLoggedPercent + 10
) {
lastLoggedBytes = uploadedBytes;
lastLoggedPercent = percentValue;
console.log(
`[s3] Upload progress: ${Math.min(percentValue, 100)}% (${formatBytes(uploadedBytes)} / ${formatBytes(totalBytes)})`,
);
}
});
try {
await upload.done();
} catch (error) {
console.error(
`[s3] Upload failed after ${formatBytes(lastLoggedBytes)} sent:`,
error,
);
throw error;
} finally {
abortSignal?.removeEventListener("abort", onAbort);
}
if (totalBytes === 0) {
onProgress?.({ uploadedBytes: 0, totalBytes: 0, percent: 1 });
}
console.log(
`[s3] Upload completed: s3://${serverConfig.S3_BUCKET_NAME}/${s3Path}`,
);
return s3Path;
}
}