v3: comments say what the code can't — the documentation pass wanted JSDoc on every export and every field of an exported interface, which filled the code with restatements ("The dump.", "Whether it is loading.", "The props of X.", "Renders the X.") and module docs that told a reader nothing ("The hot and new feeds, and the props every main feed of the index takes."); some were wrong as well as vague. Every comment in api/, shared/ and src/ is rewritten under one rule, now stated in the README's "Comments" section: every module and export still has a JSDoc, and it gives context (who uses it and where), usage (what a caller must know: a precondition, what it leaves to the caller, a side effect) or its place in the chain (where its input comes from, where its output goes); a field, parameter, private function or local gets one only when it adds to its parent's, and context about a whole type goes on the type; a type's doc is phrased from the value's side ("Returned to Y by X when Z", "Passed to Y by X"); full sentences, as short as they can be, no label-colon-list; // goes wherever the code isn't obvious and says why, and the section banners and narration go. Every claim was checked against the code, which turned up some forty comments that were wrong: DumpCard's claimed it was only used by the hot and new feeds, the WebSocket welcome claimed to be a socket's first message (presence comes first), UpdateUserRequest said null removes a description (it keeps it), deletedDumpIds leaves out that it also holds dumps made private, THEME_COLOR claimed to mirror index.html's, and the like. The routers' JSDoc now says where they are mounted and what becomes of a thrown APIException. doc-lint.ts lets a field without a doc through; a module or export without one still fails. No code changes; 204 backend and 628 frontend tests pass, and doc:lint, deno check, deno lint and tsc are clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-09-29 14:31:14 +00:00
parent 42e0356d21
commit 0d4e344d77
287 changed files with 5679 additions and 5135 deletions

View File

@@ -1,7 +1,9 @@
/**
* The route serving dumps' thumbnails, generated with ffmpeg where needed.
* Serves dumps' thumbnails to the cards and pages that show a file dump, which
* build the URL with `dumpThumbnailUrl`. Thumbnails made from an upload are
* generated with ffmpeg on first request and kept in `THUMBNAILS_DIR`.
*
* - `GET /api/thumbnails/:dumpId?token=&v=` — public; a private dump's
* - `GET /api/thumbnails/:dumpId?v=&token=` — public; a private dump's
* thumbnail needs its poster's media `token`. Serves the dump's custom
* thumbnail, else one made from its upload: a still of a video, or a
* scaled-down copy of an image (the image itself when none can be made); 404
@@ -23,16 +25,18 @@ import { serveUploadedFile } from "../lib/upload.ts";
import { DUMPS_DIR, THUMBNAILS_DIR } from "../config.ts";
import { authenticateMedia } from "../lib/auth.ts";
/** The router serving the endpoints the module doc lists. */
/**
* 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/thumbnails" });
/**
* Whether ffmpeg runs, probed once per process: spawning `ffmpeg -version` on
* every uncached thumbnail request cost a process per card.
* Holds whether ffmpeg runs, probed once per process: probing on every uncached
* thumbnail request cost a process per card.
*/
let ffmpegProbe: Promise<boolean> | undefined;
/** Checks whether ffmpeg is available. */
function ffmpegAvailable(): Promise<boolean> {
ffmpegProbe ??= (async () => {
try {
@@ -50,21 +54,23 @@ function ffmpegAvailable(): Promise<boolean> {
}
/**
* The container formats ffmpeg may read a video as. The upload's type is only
* what the uploader claimed, and ffmpeg probes the real format from the bytes —
* a "video" could be an HLS or concat playlist pointing ffmpeg at local files
* or internal URLs. Read nothing but the file itself, and only as the kind of
* container it's meant to be.
* Lists the container formats ffmpeg may read a video as. The upload's type is
* only what the uploader claimed, and ffmpeg guesses the format from the bytes,
* so a "video" could be an HLS or concat playlist pointing it at local files or
* internal URLs. With this list, and `-protocol_whitelist file`, ffmpeg reads
* nothing but the file, and only as a video.
*/
const VIDEO_FORMATS =
"mov,mp4,m4a,3gp,3g2,mj2,matroska,webm,avi,flv,mpegts,mpeg,ogg,asf,h264,hevc";
/** The formats ffmpeg may read an image as (see {@link VIDEO_FORMATS}). */
/**
* Lists the formats ffmpeg may read an image as, for the reason given at
* {@link VIDEO_FORMATS}.
*/
const IMAGE_FORMATS = "jpeg_pipe,png_pipe,webp_pipe,bmp_pipe";
/**
* The image types the feed shows through a scaled-down preview, and the
* preview's format. GIFs keep their animation (served as they are); anything
* else has no preview to make.
* Maps the image types that get a scaled-down preview to the preview's format.
* A GIF is served as it is, to keep its animation.
*/
const PREVIEWABLE_IMAGES: Record<string, "jpg" | "png"> = {
"image/jpeg": "jpg",
@@ -75,13 +81,13 @@ const PREVIEWABLE_IMAGES: Record<string, "jpg" | "png"> = {
};
/**
* The width of an image preview: comfortably covers the widest feed card on a
* high-density screen, a fraction of a phone photo's bytes.
* Caps an image preview's width. It covers the widest feed card on a
* high-density screen, at a fraction of a phone photo's bytes.
*/
const PREVIEW_WIDTH = 960;
/**
* Runs ffmpeg with `args` into `outPath`.
* Runs ffmpeg with `args`, writing to `outPath`.
*
* @throws {Error} When ffmpeg fails.
*/
@@ -121,7 +127,7 @@ function videoStill(srcPath: string, outPath: string): Promise<void> {
], outPath);
}
/** Writes a copy of an image scaled down to {@link PREVIEW_WIDTH}. */
/** Writes a copy of an image at most {@link PREVIEW_WIDTH} wide. */
function imagePreview(srcPath: string, outPath: string): Promise<void> {
return runFfmpeg([
"-format_whitelist",
@@ -141,15 +147,14 @@ function imagePreview(srcPath: string, outPath: string): Promise<void> {
}
/**
* The generations under way, by output path: one ffmpeg run per output, so a
* feed full of cards asking for the same new still at once waits on a single
* generation.
* Holds the generations under way, by output path. A feed full of cards asking
* for the same new still at once then waits on a single ffmpeg run.
*/
const inFlight = new Map<string, Promise<void>>();
/**
* Makes `outPath` with `generate` unless it exists; false when it can't be
* made (no ffmpeg, or ffmpeg failed).
* Makes `outPath` with `generate` unless it exists. Resolves false when it
* can't be made, because ffmpeg is missing or failed.
*/
async function ensureGenerated(
outPath: string,
@@ -168,20 +173,22 @@ async function ensureGenerated(
router.get("/:dumpId", async (ctx) => {
const { dumpId } = ctx.params;
// The id names files on disk: nothing but an id's characters gets through.
if (!/^[0-9a-f-]{36}$/.test(dumpId)) {
throw new APIException(APIErrorCode.BAD_REQUEST, 400, "Invalid dump ID");
}
// Token via query param (media elements can't send headers) — authorizes the
// owner's private dumps and gates the custom-thumbnail branch below too.
// Media elements can't send an Authorization header, so the viewer's media
// token comes in the query string. This privacy check covers the custom
// thumbnail too, since getDumpThumbnailInfo makes none.
const token = ctx.request.url.searchParams.get("token");
const viewerId = token ? await authenticateMedia(token) : null;
const dump = getDump(dumpId, viewerId ?? undefined);
// The URL may carry a media token; keep it out of anyone else's logs.
// The URL may carry a media token, which must not leak through a Referer.
ctx.response.headers.set("Referrer-Policy", "no-referrer");
// A private dump's thumbnail must not sit in a shared cache. The app's URLs
// carry the file's version (see dumpFileVersion): what's generated from the
// file is cached for good under the current one, and revalidated otherwise.
// What is generated from the file is cached for good under a URL naming its
// current version (dumpFileVersion), and revalidated under any other. A
// private dump's thumbnail must not sit in a shared cache.
const scope = dump.isPrivate ? "private" : "public";
const generatedCache =
ctx.request.url.searchParams.get("v") === dumpFileVersion(dump)
@@ -190,8 +197,8 @@ router.get("/:dumpId", async (ctx) => {
const customThumb = getDumpThumbnailInfo(dumpId);
if (customThumb) {
// Replaceable without the URL changing: always revalidated (a 304 when
// unchanged).
// Replacing it doesn't change the file's version, so it is always
// revalidated.
const served = await serveUploadedFile(
ctx,
`${THUMBNAILS_DIR}/${customThumb.id}-custom`,