import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client, } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; // S3 API abstraction — provider-agnostic (Garage, Cloudflare R2, AWS S3). const s3 = new S3Client({ endpoint: process.env.S3_ENDPOINT, region: process.env.S3_REGION ?? "us-east-1", credentials: { accessKeyId: process.env.S3_ACCESS_KEY!, secretAccessKey: process.env.S3_SECRET_KEY!, }, forcePathStyle: true, // REQUIRED for Garage and most S3-compatible services }); const bucket = process.env.S3_BUCKET ?? "gearbox-images"; const presignExpiry = Number.parseInt( process.env.S3_PRESIGN_EXPIRY ?? "3600", 10, ); export async function uploadImage( buffer: Buffer | ArrayBuffer, filename: string, contentType: string, ): Promise { await s3.send( new PutObjectCommand({ Bucket: bucket, Key: filename, Body: Buffer.from(buffer), ContentType: contentType, }), ); } export async function deleteImage(filename: string): Promise { await s3.send( new DeleteObjectCommand({ Bucket: bucket, Key: filename, }), ); } export async function getImageUrl(filename: string): Promise { const command = new GetObjectCommand({ Bucket: bucket, Key: filename, }); return getSignedUrl(s3, command, { expiresIn: presignExpiry }); } /** * Enrich a record that has an imageFilename with a presigned imageUrl. * Returns null imageUrl when imageFilename is null. */ export async function withImageUrl( record: T, ): Promise { return { ...record, imageUrl: record.imageFilename ? await getImageUrl(record.imageFilename) : null, }; } /** * Batch version of withImageUrl. Uses Promise.all for parallelism. */ export async function withImageUrls( records: T[], ): Promise<(T & { imageUrl: string | null })[]> { return Promise.all(records.map((record) => withImageUrl(record))); }