/** * Serves and replaces file dumps' uploads, a sub-resource of their dump. The * players, viewers and download links of the frontend build the URL with * `dumpFileUrl`, and its edit page replaces the file. * * - `GET /api/dumps/:dumpId/file?v=&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. * - `PUT /api/dumps/:dumpId/file` — signed in, as its poster or with * `dump:moderate`. Replaces a file dump's upload and comment, and any of its * title, slug, categories and visibility (multipart). * * @module */ import { Router } from "@oak/oak"; import { APIException } from "../lib/api-exception.ts"; import type { APIResponse } from "../../shared/api.ts"; import { type Dump, dumpFileVersion } from "../../shared/dump.ts"; import { DUMPS_DIR } from "../config.ts"; import { UUID_RE } from "../../shared/validation.ts"; import { authenticateMedia } from "../lib/auth.ts"; import { can } from "../lib/permissions.ts"; import { serveFile } from "../lib/serve-file.ts"; import { authMiddleware } from "../middleware/auth.ts"; import { parseCategoryIds } from "../validators/dump.ts"; import { getDump, replaceFileDump } from "../services/dump-service.ts"; /** * Mounted by `api/main.ts` behind the error, Open Graph and CORS middleware. * An `APIException` thrown by a handler becomes the JSON error response there. */ const router: Router = new Router({ prefix: "/api/dumps" }); /** * Builds a `Content-Disposition` value for a file called `name`. `Headers.set` * throws on any character above U+00FF, so `filename` gets an ASCII copy of the * name and `filename*` (RFC 5987) the full one, percent-encoded. */ 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) }`; } /** * Matches the types served inline. An upload's type is whatever the uploader's * browser claimed, and the file comes from the app's own origin, where the * session token lives. So only types a browser shows without running code in * them are inline, and anything else (HTML, SVG, XML…) downloads. ``, * `