86 lines
2.7 KiB
TypeScript
86 lines
2.7 KiB
TypeScript
import { S3Client } from "bun";
|
|
import config from "@/../config.toml";
|
|
|
|
// Bun.S3Client 단일화
|
|
const client = new S3Client({
|
|
accessKeyId: config.s3.access_key,
|
|
secretAccessKey: config.s3.secret_key,
|
|
bucket: config.s3.bucket,
|
|
endpoint: config.s3.endpoint,
|
|
});
|
|
|
|
async function warmupS3() {
|
|
try {
|
|
await client.exists(`__warmup_${Date.now()}__`);
|
|
} catch (e) {
|
|
}
|
|
}
|
|
|
|
function makeS3FileName(authorId: string, tweetId: string, mediaUrl: string, index: number) {
|
|
const rawName = mediaUrl.split("/").pop() || `media_${Date.now()}_${index}`;
|
|
const withoutQuery = rawName.split("?")[0]?.split("#")[0] || `media_${Date.now()}_${index}`;
|
|
const safeName = withoutQuery.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
return `twitter/${authorId}/${tweetId}/${safeName}`;
|
|
}
|
|
|
|
async function uploadToS3(fileName: string, mediaUrl: string, maxRetry = 3) {
|
|
let lastError: unknown;
|
|
|
|
// 1. 요청 시작 전 예열 수행
|
|
await warmupS3();
|
|
|
|
for (let attempt = 1; attempt <= maxRetry; attempt++) {
|
|
try {
|
|
const parsedUrl = new URL(mediaUrl);
|
|
const isPixivImage = parsedUrl.hostname === "i.pximg.net";
|
|
const response = await fetch(mediaUrl, {
|
|
headers: isPixivImage
|
|
? {
|
|
"Referer": "https://www.pixiv.net/",
|
|
"User-Agent": "Mozilla/5.0 (compatible; akiyama.mizuki.guru/1.0)",
|
|
}
|
|
: undefined,
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Fetch failed: ${response.status}`);
|
|
}
|
|
|
|
const buffer = await response.arrayBuffer();
|
|
|
|
// 2. Bun S3 Client만 사용하여 쓰기
|
|
// client.write는 내부적으로 효율적인 스트리밍/버퍼 처리를 수행합니다.
|
|
await client.write(fileName, buffer, {
|
|
type: response.headers.get("content-type") || "application/octet-stream",
|
|
});
|
|
|
|
return; // 성공 시 리턴
|
|
} catch (error) {
|
|
lastError = error;
|
|
console.error(`[S3 upload attempt ${attempt}/${maxRetry}] key=${fileName}`, error);
|
|
|
|
// UnknownError 발생 시 잠시 대기 후 재시도 (지수 백오프)
|
|
if (attempt < maxRetry) {
|
|
await Bun.sleep(attempt * 1000);
|
|
// 재시도 전 다시 한번 예열 시도 가능
|
|
await warmupS3();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 최종 실패 전 마지막 확인 (이미 올라갔을 수도 있음)
|
|
if (await client.exists(fileName)) {
|
|
console.warn(`[S3 upload recovered] key=${fileName} was found after error.`);
|
|
return;
|
|
}
|
|
|
|
throw lastError;
|
|
}
|
|
|
|
async function writeToS3(fileName: string, data: ArrayBuffer | Blob, contentType: string) {
|
|
await warmupS3();
|
|
await client.write(fileName, data, {
|
|
type: contentType,
|
|
});
|
|
}
|
|
|
|
export { makeS3FileName, uploadToS3, writeToS3, client as s3Client };
|