initial commit
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user