1import { readdir } from "node:fs/promises";2import { join, relative } from "node:path";3import type { BucketSource } from "../server/config.ts";456export type BucketReader = {7 get(key: string): Promise<Uint8Array | null>;8 list(prefix: string): Promise<string[]>;9};1011export function openBucket(source: BucketSource): BucketReader {12 return source.kind === "s3" ? s3Reader(source) : folderReader(source.path);13}1415function s3Reader(source: Extract<BucketSource, { kind: "s3" }>): BucketReader {16 const client = new Bun.S3Client({17 endpoint: source.endpoint,18 bucket: source.name,19 region: source.region,20 accessKeyId: source.accessKeyId,21 secretAccessKey: source.secretAccessKey,22 });23 return {24 async get(key) {25 try {26 return await client.file(key).bytes();27 } catch (error) {28 if ((error as { code?: string }).code === "NoSuchKey") return null;29 throw error;30 }31 },32 async list(prefix) {33 const keys: string[] = [];34 let continuationToken: string | undefined;35 do {36 const page = await client.list({ prefix, continuationToken });37 for (const object of page.contents ?? []) keys.push(object.key);38 continuationToken = page.isTruncated ? page.nextContinuationToken : undefined;39 } while (continuationToken);40 return keys;41 },42 };43}4445function folderReader(root: string): BucketReader {46 return {47 async get(key) {48 if (key.split("/").includes("..")) throw new Error(`bucket key escapes the folder: ${key}`);49 const file = Bun.file(join(root, key));50 return (await file.exists()) ? await file.bytes() : null;51 },52 async list(prefix) {53 const entries = await readdir(root, { recursive: true, withFileTypes: true });54 return entries55 .filter((entry) => entry.isFile())56 .map((entry) => relative(root, join(entry.parentPath, entry.name)))57 .filter((key) => key.startsWith(prefix))58 .sort();59 },60 };61}