Public: anyone can read this; the crew can change it.

Keep media pages (/media) for strangers at a full commit id (#9405) #9640

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/9505-keep-media-pages-media-for-strangers-at → main 2 commits fork

  • Merged. mason/9505-keep-media-pages-media-for-strangers-at was merged into main as ebd2899.

Review

Sign in to review this change.
3 files changed +53 −5
src/web/pages/media.ts +20 −5
@@ -23,6 +23,7 @@ import type { Ctx } from "../../http/context.ts";
2323 import { holds } from "../../http/etag.ts";
2424 import { parseRange } from "../../http/range.ts";
2525 import { loadRepo, notFoundPage } from "../chrome.ts";
26 import { cachedMedia, keepMedia } from "./tree_cache.ts";
2627 import { HEAD_BYTES, mediaType } from "../render/kinds.ts";
2728
2829 /** The policy every media answer leaves with. */
@@ -57,10 +58,20 @@ export async function mediaBlob(ctx: Ctx): Promise<Response> {
5758
5859 const dir = repoDir(ctx.app.reposRoot, repo.ownerName, repo.name);
5960 const reader = readerFor(dir);
60 const stat = await reader.stat(`${ref}:${path}`).catch(() => undefined);
61 if (stat === undefined || !stat.ok || stat.value.type !== "blob")
62 return notFoundPage(ctx);
63 const { oid, size } = stat.value;
61 // At a full commit id a small file never changes: a repeat read spawns no git (#9505).
62 const pinned = /^[0-9a-f]{40}$/.test(ref);
63 const held = pinned ? cachedMedia(repo.id, ref, path) : undefined;
64 let oid: string;
65 let size: number;
66 if (held !== undefined) {
67 oid = held.oid;
68 size = held.body.length;
69 } else {
70 const stat = await reader.stat(`${ref}:${path}`).catch(() => undefined);
71 if (stat === undefined || !stat.ok || stat.value.type !== "blob")
72 return notFoundPage(ctx);
73 ({ oid, size } = stat.value);
74 }
6475
6576 const download = ctx.url.searchParams.get("download") === "1";
6677 const etag = `"${oid}"`;
@@ -90,11 +101,15 @@ export async function mediaBlob(ctx: Ctx): Promise<Response> {
90101 // its first bytes and streamed, so its size never becomes memory.
91102 let body: Uint8Array | undefined;
92103 let head: Uint8Array;
93 if (size <= SMALL_BYTES) {
104 if (held !== undefined) {
105 body = held.body;
106 head = body.subarray(0, HEAD_BYTES);
107 } else if (size <= SMALL_BYTES) {
94108 const object = await reader.object(oid).catch(() => undefined);
95109 if (object === undefined || !object.ok) return notFoundPage(ctx);
96110 body = object.value.body;
97111 head = body.subarray(0, HEAD_BYTES);
112 if (pinned) keepMedia(repo.id, ref, path, { oid, body });
98113 } else {
99114 const sniff = await blobStream(dir, oid, 0, HEAD_BYTES - 1, { signal: ctx.request.signal });
100115 if (sniff === "busy" || sniff === "gone") return busy();
src/web/pages/tree_cache.ts +16
@@ -54,6 +54,7 @@ export const treeCacheStats = () => cache.stats();
5454 export const clearTreeCache = (): void => {
5555 cache.clear();
5656 rawCache.clear();
57 mediaCache.clear();
5758 parts.clear();
5859 clearKeptPages();
5960 };
@@ -136,3 +137,18 @@ export function keepRaw(repoId: string, commit: string, path: string, blob: Blob
136137 if (blob.kind === "missing" || blob.kind === "too_large") return;
137138 if (blob.size <= RAW_MAX) rawCache.set(rawKey(repoId, commit, path), blob, TTL_MS, blob.size);
138139 }
140
141 /** A small media file at a full commit id (#9505): its blob id and bytes, so a repeat read spawns no git. */
142 export interface KeptMedia {
143 readonly oid: string;
144 readonly body: Uint8Array;
145 }
146 const MEDIA_MAX = 1024 * 1024;
147 const mediaCache = new ViewCache({ maxEntries: 512, maxBytes: 32 * 1024 * 1024 });
148 const mediaKey = (repoId: string, commit: string, path: string): string => ["media", repoId, commit, path].join("\0");
149
150 export const cachedMedia = (repoId: string, commit: string, path: string) => mediaCache.get<KeptMedia>(mediaKey(repoId, commit, path));
151
152 export function keepMedia(repoId: string, commit: string, path: string, media: KeptMedia): void {
153 if (media.body.length <= MEDIA_MAX) mediaCache.set(mediaKey(repoId, commit, path), media, TTL_MS, media.body.length + 128);
154 }
test/media.test.ts +17
@@ -17,6 +17,7 @@ import { mediaGate } from "../src/git/env.ts";
1717 import { blobStream, MEDIA_QUEUE_MAX, refTip, readerFor } from "../src/git/read.ts";
1818 import { repoDir } from "../src/git/repos.ts";
1919 import { serve, type Servers } from "../src/http/server.ts";
20 import { cachedMedia } from "../src/web/pages/tree_cache.ts";
2021 import { MEDIA_POLICY, PDF_POLICY } from "../src/web/pages/media.ts";
2122 import { seedAdmin } from "./helpers/admin.ts";
2223 import { quietLogs } from "./helpers/globals.ts";
@@ -89,6 +90,22 @@ afterAll(async () => {
8990 });
9091
9192 describe("the media route", () => {
93 test("a small file at a full commit id is kept, a branch's and a large one are not", async () => {
94 const repoId = app.db.query<{ id: string }, []>("select id from repo where name = 'gallery'").get()!.id;
95 expect(cachedMedia(repoId, sha, "art/logo.png")).toBeUndefined();
96 const first = await get(`/alice/gallery/media/${sha}/art/logo.png`);
97 const bytes = new Uint8Array(await first.arrayBuffer());
98 const held = cachedMedia(repoId, sha, "art/logo.png");
99 expect(held?.body).toEqual(PNG);
100 const again = await get(`/alice/gallery/media/${sha}/art/logo.png`);
101 expect(again.headers.get("etag")).toBe(first.headers.get("etag"));
102 expect(new Uint8Array(await again.arrayBuffer())).toEqual(bytes);
103 await get("/alice/gallery/media/main/art/logo.png");
104 expect(cachedMedia(repoId, "main", "art/logo.png")).toBeUndefined();
105 await (await get(`/alice/gallery/media/${sha}/clip.mp4`)).arrayBuffer();
106 expect(cachedMedia(repoId, sha, "clip.mp4")).toBeUndefined();
107 });
108
92109 test("a confirmed PNG is served as image/png with its bytes", async () => {
93110 const res = await get("/alice/gallery/media/main/art/logo.png");
94111 expect(res.status).toBe(200);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs