fake-s3.ts

2.4 KB · raw

1/**2 * A bucket in memory, served over HTTP for the joint public-steal test: koh3's own S3 backend and3 * Bun's S3Client both talk to it, so Kepr and the two koh3 machines share one bucket.4 *5 * It runs as its own process because the test drives koh3 with `Bun.spawnSync`, which would block6 * the event loop this server needs. It prints its endpoint on the first line of stdout.7 *8 * Only what the two clients use is implemented: GET, HEAD, PUT (with `If-None-Match: *` for the9 * one conditional write that matters, `head`), DELETE, and ListObjectsV2. Signatures are ignored10 * rather than verified — the one way this is easier than a real bucket.11 */12const BUCKET = "kohbucket";13const objects = new Map<string, Uint8Array>();1415async function handle(request: Request): Promise<Response> {16	const url = new URL(request.url);17	const [, bucket = "", ...rest] = url.pathname.split("/");18	if (bucket !== BUCKET) return new Response("", { status: 404 });19	if (url.searchParams.get("list-type") === "2") {20		const prefix = url.searchParams.get("prefix") ?? "";21		const keys = [...objects.keys()].filter((key) => key.startsWith(prefix)).sort();22		const contents = keys23			.map(24				(key) =>25					`<Contents><Key>${key}</Key><Size>${objects.get(key)?.length ?? 0}</Size></Contents>`,26			)27			.join("");28		return new Response(29			`<?xml version="1.0" encoding="UTF-8"?><ListBucketResult><IsTruncated>false</IsTruncated>${contents}</ListBucketResult>`,30			{ headers: { "content-type": "application/xml" } },31		);32	}33	const key = rest.join("/");34	if (request.method === "PUT") {35		const body = new Uint8Array(await request.arrayBuffer());36		if (request.headers.get("if-none-match") === "*" && objects.has(key)) {37			return new Response("", { status: 412 });38		}39		objects.set(key, body);40		return new Response("", { status: 200, headers: { etag: `"${body.length}"` } });41	}42	if (request.method === "GET" || request.method === "HEAD") {43		const body = objects.get(key);44		if (!body) return new Response("", { status: 404 });45		return new Response(request.method === "HEAD" ? null : body, {46			status: 200,47			headers: { etag: `"${body.length}"`, "content-length": String(body.length) },48		});49	}50	if (request.method === "DELETE") {51		objects.delete(key);52		return new Response("", { status: 204 });53	}54	return new Response("", { status: 404 });55}5657const server = Bun.serve({ port: 0, fetch: handle });58console.log(`http://127.0.0.1:${server.port}`);