tests/videoServe.test.ts
Rama main · réplica de lectura del repositorio original.
// 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<string, string> = { 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<string, unknown>();
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);
}
});
});