/** * The route serving file dumps' uploads. * * @module */ import { Router } from "@oak/oak"; import { APIErrorCode } from "../../shared/api.ts"; import { APIException } from "../lib/api-exception.ts"; import { DUMPS_DIR } from "../lib/upload.ts"; import { authenticateMedia } from "../lib/auth.ts"; import { serveFile } from "../lib/serve-file.ts"; import { dumpFileVersion, getDump } from "../services/dump-service.ts"; /** * The route serving file dumps' uploads: * * - `GET /api/files/:dumpId?token=` — public; a private dump's file needs its * poster's media `token`. Serves the file, inline when that is safe and as a * download otherwise, with byte ranges. */ const router: Router = new Router({ prefix: "/api/files" }); /** * Builds an RFC 6266 `Content-Disposition` value that is always a valid * ByteString. `headers.set` throws a TypeError if the value contains any * character > U+00FF, so a filename with an emoji / accent / CJK character * (e.g. an uploaded file) would otherwise crash the response. We provide an * ASCII-only `filename="…"` fallback plus an RFC 5987 `filename*` with the * full UTF-8 name for modern browsers. */ function contentDisposition( type: "inline" | "attachment", name: string, ): string { const asciiFallback = name .replace(/["\\]/g, "_") .replace(/[^\x20-\x7e]/g, "_"); return `${type}; filename="${asciiFallback}"; filename*=UTF-8''${ encodeURIComponent(name) }`; } /** * The types served inline. The MIME type of an upload is whatever the * uploader's browser claimed, and these responses come from the app's own * origin — where the session token lives. Only types a browser renders without * running the file's own code are served inline; anything else (text/html, * image/svg+xml, XML, …) downloads. ,