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 };