import * as Effect from "effect/Effect"; import { bodyBytes } from "../server/http"; import { MAX_ATTACHMENT_BYTES, type Attachment } from "../shared/attachments"; import { privateResponse } from "./session"; interface AttachmentHost { upload(name: string, bytes: Uint8Array): Promise; download(id: string): Promise<{ file: Attachment; bytes: Uint8Array } | null>; remove(id: string): Promise; } export async function handleAttachmentRequest( request: Request, host: AttachmentHost, ) { const url = new URL(request.url); const id = url.pathname.split("/")[5]; try { if (request.method === "POST" && !id) { request.signal.throwIfAborted(); const bytes = await Effect.runPromise( bodyBytes( new Response(request.body, { headers: request.headers }), MAX_ATTACHMENT_BYTES, new Error("File is too large or unreadable"), ), { signal: request.signal }, ); const name = decodeURIComponent( request.headers.get("X-Filename") ?? "Attachment", ); const file = await host.upload(name, bytes); return Response.json(file, { status: 201, headers: { "Cache-Control": "no-store" }, }); } if (request.method === "GET" && id) { const result = await host.download(id); if (!result) return privateResponse("Not found", 404); return new Response(result.bytes, { headers: { "Content-Type": result.file.mediaType, "Content-Length": String(result.bytes.length), "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", "Content-Security-Policy": "sandbox; default-src 'none'", "Content-Disposition": `${url.searchParams.has("download") ? "attachment" : "inline"}; filename*=UTF-8''${encodeURIComponent(result.file.name)}`, }, }); } if (request.method === "DELETE" && id) { await host.remove(id); return new Response(null, { status: 204, headers: { "Cache-Control": "no-store" }, }); } return privateResponse("Method not allowed", 405); } catch { return privateResponse( "Could not process the attachment. Use PNG, JPEG or WebP up to 3.5 MiB, or PDF up to 20 MiB. Saved attachments cannot be removed individually.", 400, ); } }