| 1 | import type { WriteBucketClient } from "@/Cloudflare/R2/WriteBucket.ts"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import type * as HttpServerRequest from "effect/http/HttpServerRequest"; |
| 4 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 5 | |
| 6 | /** |
| 7 | * Shared write-side routes exercised by both the binding and HTTP fixtures so |
| 8 | * every (HTTP-supported) method of {@link WriteBucketClient} is driven over |
| 9 | * `fetch`: |
| 10 | * |
| 11 | * - `PUT /put?key=` — `put(key, body)` (returns the object key to prove the |
| 12 | * call resolved an `R2Object`). |
| 13 | * - `DELETE /del?key=` — `delete(key)` (single). |
| 14 | * - `DELETE /del-many?keys=a,b,c` — `delete(keys)` (batch). Keys travel as a |
| 15 | * comma-separated query param rather than a request body because DELETE |
| 16 | * bodies are unreliable across `fetch`/the CF edge. |
| 17 | * |
| 18 | * Returns `undefined` when the path is not a write route so the caller can |
| 19 | * fall through (used by the read-write fixtures). |
| 20 | */ |
| 21 | export const writeRoutes = ( |
| 22 | r2: WriteBucketClient, |
| 23 | request: HttpServerRequest.HttpServerRequest, |
| 24 | url: URL, |
| 25 | ) => |
| 26 | Effect.gen(function* () { |
| 27 | if (request.method === "PUT" && url.pathname === "/put") { |
| 28 | const key = url.searchParams.get("key") ?? ""; |
| 29 | const body = yield* request.text; |
| 30 | const object = yield* r2.put(key, body).pipe(Effect.orDie); |
| 31 | return yield* HttpServerResponse.json({ |
| 32 | ok: true, |
| 33 | key: object?.key ?? key, |
| 34 | }); |
| 35 | } |
| 36 | if (request.method === "DELETE" && url.pathname === "/del") { |
| 37 | const key = url.searchParams.get("key") ?? ""; |
| 38 | yield* r2.delete(key).pipe(Effect.orDie); |
| 39 | return yield* HttpServerResponse.json({ ok: true }); |
| 40 | } |
| 41 | if (request.method === "DELETE" && url.pathname === "/del-many") { |
| 42 | const keys = (url.searchParams.get("keys") ?? "") |
| 43 | .split(",") |
| 44 | .filter((k) => k.length > 0); |
| 45 | yield* r2.delete(keys).pipe(Effect.orDie); |
| 46 | return yield* HttpServerResponse.json({ ok: true }); |
| 47 | } |
| 48 | // Multipart upload (native binding only — the HTTP client `Effect.die`s |
| 49 | // these). R2 requires every part except the last to be ≥ 5 MiB, so the |
| 50 | // first part is exactly 5 MiB and the tail is a tiny final part. The |
| 51 | // worker generates the bytes internally so the test payload stays small. |
| 52 | if (request.method === "POST" && url.pathname === "/mpu") { |
| 53 | const key = url.searchParams.get("key") ?? ""; |
| 54 | const part = new Uint8Array(5 * 1024 * 1024).fill(65); |
| 55 | const upload = yield* r2.createMultipartUpload(key).pipe(Effect.orDie); |
| 56 | const p1 = yield* upload.uploadPart(1, part).pipe(Effect.orDie); |
| 57 | const p2 = yield* upload.uploadPart(2, "tail").pipe(Effect.orDie); |
| 58 | const object = yield* upload.complete([p1, p2]).pipe(Effect.orDie); |
| 59 | return yield* HttpServerResponse.json({ ok: true, size: object.size }); |
| 60 | } |
| 61 | if (request.method === "POST" && url.pathname === "/mpu-abort") { |
| 62 | const key = url.searchParams.get("key") ?? ""; |
| 63 | const part = new Uint8Array(5 * 1024 * 1024).fill(66); |
| 64 | const upload = yield* r2.createMultipartUpload(key).pipe(Effect.orDie); |
| 65 | yield* upload.uploadPart(1, part).pipe(Effect.orDie); |
| 66 | yield* upload.abort().pipe(Effect.orDie); |
| 67 | return yield* HttpServerResponse.json({ ok: true }); |
| 68 | } |
| 69 | if (request.method === "POST" && url.pathname === "/mpu-resume") { |
| 70 | const key = url.searchParams.get("key") ?? ""; |
| 71 | const part = new Uint8Array(5 * 1024 * 1024).fill(67); |
| 72 | const created = yield* r2.createMultipartUpload(key).pipe(Effect.orDie); |
| 73 | const upload = yield* r2 |
| 74 | .resumeMultipartUpload(key, created.uploadId) |
| 75 | .pipe(Effect.orDie); |
| 76 | const p1 = yield* upload.uploadPart(1, part).pipe(Effect.orDie); |
| 77 | const p2 = yield* upload.uploadPart(2, "tail").pipe(Effect.orDie); |
| 78 | const object = yield* upload.complete([p1, p2]).pipe(Effect.orDie); |
| 79 | return yield* HttpServerResponse.json({ ok: true, size: object.size }); |
| 80 | } |
| 81 | return undefined; |
| 82 | }); |