// FILMS OFF THE DISK: /api/file serves a film in ranges without reading it // into memory, with its own type for every container; the upload takes a // film past a picture's cap and streams it to disk; a published film's poster // is a visitor's to fetch; the pocket answers ranges from one read; the // shell's frame policy opens only on the switch. // // The large file is SPARSE — `truncate` to 600 MB costs no disk — and the // memory check reads a range from its far end: a server that loaded the file // to slice it would grow by 600 MB, one that streams grows by a chunk. import assert from "node:assert/strict"; import { openSync, closeSync, ftruncateSync, readFileSync, statSync, existsSync } from "node:fs"; import path from "node:path"; import { after, before, describe, it } from "node:test"; import { initAuth } from "../server/auth.ts"; import { initIndexer, isAllowedAttachment } from "../server/indexer.ts"; import { initSite } from "../server/site.ts"; import { initVault } from "../server/vault.ts"; import { api } from "../server/api.ts"; import { Hono } from "hono"; import { patchSettings } from "../server/settings.ts"; import { makeDir, makeVault, removeVault } from "./helpers/vault.ts"; import { makeMemoryFs } from "./helpers/memoryFs.ts"; import { PocketIndex } from "../mobile/src/pocket/index.ts"; import { createPocketServer } from "../mobile/src/pocket/server.ts"; import { createVaultIo } from "../mobile/src/pocket/vaultIo.ts"; const BIG = 600 * 1024 * 1024; // Mounted where the shell mounts it, so the body cap (keyed on `/api/upload`) // is the one a real upload meets. const app = new Hono().route("/api", api); describe("a film on the server", () => { const data = makeDir(); const root = makeVault({ "Media/tiny.webm": "\x1a\x45\xdf\xa3 not really a film", "Media/a.mp4": "x", "Media/a.m4v": "x", "Media/a.mov": "x", "Media/a.mkv": "x", "Media/a.ogv": "x", "Media/frame.jpg": "x", "Media/secret.jpg": "x", "Films.md": "---\npublish: true\n---\n# Films\n\n![[tiny.webm|480|poster=frame.jpg]]\n", "Draft.md": "# Draft\n\n![[a.mp4|poster=secret.jpg]]\n", }); const bigPath = path.join(root, "Media/big.mp4"); before(async () => { const fd = openSync(bigPath, "w"); ftruncateSync(fd, BIG); closeSync(fd); initAuth({}); // local mode: no password, the owner's session initSite({ ASTROLABE_DATA: data }); initVault(root); await initIndexer(); }); after(() => { removeVault(root); removeVault(data); }); it("answers a range at the far end of a 600 MB film without reading it into memory", async () => { global.gc?.(); const before = process.memoryUsage(); const res = await api.request("/file?path=Media/big.mp4", { headers: { range: `bytes=${BIG - 1024}-` } }); assert.equal(res.status, 206); assert.equal(res.headers.get("content-range"), `bytes ${BIG - 1024}-${BIG - 1}/${BIG}`); assert.equal(res.headers.get("content-type"), "video/mp4"); assert.equal(res.headers.get("accept-ranges"), "bytes"); assert.equal((await res.arrayBuffer()).byteLength, 1024); const grown = process.memoryUsage().rss - before.rss; assert.ok(grown < 100 * 1024 * 1024, `rss grew ${Math.round(grown / 1048576)} MB`); }); it("a whole-file answer is a stream: the headers come before the bytes", async () => { global.gc?.(); const before = process.memoryUsage().rss; const res = await api.request("/file?path=Media/big.mp4"); assert.equal(res.status, 200); assert.equal(res.headers.get("content-length"), String(BIG)); const reader = res.body?.getReader(); assert.ok(reader); const first = await reader.read(); assert.ok(first.value && first.value.byteLength > 0 && first.value.byteLength < BIG); await reader.cancel(); const grown = process.memoryUsage().rss - before; assert.ok(grown < 100 * 1024 * 1024, `rss grew ${Math.round(grown / 1048576)} MB`); }); it("names every container's type", async () => { const want: Record = { mp4: "video/mp4", m4v: "video/mp4", mov: "video/quicktime", mkv: "video/x-matroska", ogv: "video/ogg" }; for (const [ext, type] of Object.entries(want)) { const res = await api.request(`/file?path=Media/a.${ext}`, { headers: { range: "bytes=0-0" } }); assert.equal(res.status, 206, ext); assert.equal(res.headers.get("content-type"), type, ext); await res.arrayBuffer(); } }); it("a published film's poster is a visitor's; a draft's is not", () => { assert.equal(isAllowedAttachment("Media/tiny.webm"), true); assert.equal(isAllowedAttachment("Media/frame.jpg"), true); assert.equal(isAllowedAttachment("Media/secret.jpg"), false); }); it("takes a film past a picture's cap, streamed to disk under its own name", async () => { const bytes = new Uint8Array(12 * 1024 * 1024); bytes.set([0x1a, 0x45, 0xdf, 0xa3], 0); bytes[bytes.length - 1] = 7; const form = new FormData(); form.append("file", new File([bytes], "holiday.mkv", { type: "video/x-matroska" })); const res = await app.request("/api/upload", { method: "POST", body: form }); assert.equal(res.status, 200, await res.clone().text()); const { path: rel } = (await res.json()) as { path: string }; assert.match(rel, /holiday\.mkv$/, "the uploader's .mkv is kept"); const abs = path.join(root, rel); assert.ok(existsSync(abs)); assert.equal(statSync(abs).size, bytes.length); assert.equal(readFileSync(abs)[bytes.length - 1], 7, "every byte written"); }); it("still holds a picture to a picture's cap", async () => { const bytes = new Uint8Array(11 * 1024 * 1024); bytes.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], 0); const form = new FormData(); form.append("file", new File([bytes], "huge.png", { type: "image/png" })); const res = await app.request("/api/upload", { method: "POST", body: form }); assert.equal(res.status, 413); }); }); describe("a film on the pocket", () => { it("is served in ranges, with its type, again and again from one read", async () => { // A megabyte and a bit: large enough to be the file the pocket keeps. const film = "0123456789".repeat(110_000); const fs = makeMemoryFs({ "/vault/Media/clip.webm": film, "/vault/Welcome.md": "# Welcome\n" }); const io = createVaultIo(fs, "/vault"); let reads = 0; const counted = { ...io, readBytes: async (p: string) => { reads++; return io.readBytes(p); }, }; const held = new Map(); const server = createPocketServer({ io: counted, git: { commit: async () => "sha", history: async () => [], blobAt: async () => "" }, store: { get: async (k) => held.get(k) ?? null, set: async (k, v) => void held.set(k, v) }, index: new PocketIndex(), repo: { name: "owner/vault", branch: "main" }, now: () => 1_700_000_000_000, }); for (const [range, body] of [["bytes=0-3", "0123"], ["bytes=10-12", "012"], ["bytes=-2", "89"]] as const) { const res = await server.handle({ method: "GET", url: "/api/file?path=Media/clip.webm", headers: { range } }); assert.equal(res.status, 206, range); assert.equal(res.headers["Content-Type"], "video/webm"); assert.equal(new TextDecoder().decode(res.body as Uint8Array), body, range); } assert.equal(reads, 1, "one read of the film, three answers"); }); }); describe("the shell's frame policy", () => { it("is 'none' until Embed external video is on, and https frames while it is", async () => { const data = makeDir(); const root = makeVault({ "Welcome.md": "# Hi\n" }); try { initSite({ ASTROLABE_DATA: data }); initVault(root); patchSettings({ externalVideo: null }); const src = readFileSync(new URL("../server/index.ts", import.meta.url), "utf8"); // The switch's door is the only change to the policy: read it as written. assert.match(src, /"frame-src 'none'"/); assert.match(src, /getSettings\(\)\.externalVideo === true \? SHELL_CSP\.replace\("frame-src 'none'", "frame-src https:"\) : SHELL_CSP/); patchSettings({ externalVideo: true }); const { getSettings } = await import("../server/settings.ts"); assert.equal(getSettings().externalVideo, true); patchSettings({ externalVideo: null }); assert.equal(getSettings().externalVideo, undefined); assert.throws(() => patchSettings({ externalVideo: "yes" as unknown as boolean }), /must be a boolean/); } finally { removeVault(root); removeVault(data); } }); });