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

GET /api/v1/repos/:owner/:name/builds/:label/shots carries an ETag and answers a repeat with 304 until a shot is added, removed or reordered #9288

Merged opened this

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

Merged

scout/forge:scout/9276-get-api-v1-repos-owner-name-builds-label → main 2 commits fork

  • Merged. scout/9276-get-api-v1-repos-owner-name-builds-label was merged into main as bbb82b4.

Review

Sign in to review this change.
2 files changed +34 −1
src/http/api/build_shots.ts +8 −1
@@ -16,6 +16,7 @@ import { readShots, type Shots } from "../../git/shots.ts";
1616 import { mayFetchLook } from "../../web/pages/looks.ts";
1717 import { loadRepo } from "../api.ts";
1818 import type { Handler } from "../context.ts";
19 import { holds, weakEtag } from "../etag.ts";
1920 import { readBounded } from "../limits.ts";
2021 import { apiError, body, json, notFound, requireScope, respondAccess } from "../respond.ts";
2122 import type { Router } from "../router.ts";
@@ -48,7 +49,13 @@ export function buildShotRoutes(router: Router<Handler>): void {
4849 const label = ctx.params.label ?? "";
4950 const dir = dirOf(repo, ctx.app.reposRoot);
5051 if ((await readBuildPin(dir, label)) === undefined) return notFound();
51 return json(items(repo, label, await readShots(dir, label)));
52 // The tag is of the exact bytes sent, so an upload, delete, pick, caption or reorder moves it;
53 // private because a private repository's pictures are for its readers only.
54 const text = JSON.stringify(items(repo, label, await readShots(dir, label)));
55 const etag = weakEtag(text);
56 const headers = { etag, "cache-control": "private, no-cache", vary: "authorization, cookie" };
57 if (holds(ctx.request, etag)) return new Response(null, { status: 304, headers });
58 return new Response(text, { headers: { "content-type": "application/json", ...headers } });
5259 });
5360
5461 // Upload one: multipart, the picture as `shot`, an optional `caption` and the `tree` it was read from as `at`.
test/build-shots.test.ts +26
@@ -243,6 +243,32 @@ describe("pin screenshots", () => {
243243 expect((await shots()).tree).toBe(before);
244244 });
245245
246 test("the gallery carries an ETag: a repeat is 304 until a shot is added, moved or removed", async () => {
247 const first = await api("GET", "/alice/karts/builds/v1/shots", aliceToken);
248 const tag = first.headers.get("etag")!;
249 expect(tag).toStartWith('W/"');
250 expect(first.headers.get("cache-control")).toBe("private, no-cache");
251 const repeat = await fetch(`${base}/api/v1/repos/alice/karts/builds/v1/shots`, { headers: { authorization: `Bearer ${aliceToken}`, "if-none-match": tag } });
252 expect(repeat.status).toBe(304);
253 expect(repeat.headers.get("etag")).toBe(tag);
254 expect(repeat.headers.get("x-content-type-options")).toBe("nosniff");
255 expect(await repeat.text()).toBe("");
256 const tagged = async () => (await api("GET", "/alice/karts/builds/v1/shots", aliceToken)).headers.get("etag")!;
257 const room = (await shots()).items.length < 12;
258 expect(room).toBe(true);
259 expect((await api("POST", "/alice/karts/builds/v1/shots", aliceToken, upload(PNG(77), "tag"))).status).toBe(201);
260 const added = await tagged();
261 expect(added).not.toBe(tag);
262 const stale = await fetch(`${base}/api/v1/repos/alice/karts/builds/v1/shots`, { headers: { authorization: `Bearer ${aliceToken}`, "if-none-match": tag } });
263 expect(stale.status).toBe(200);
264 const last = (await shots()).items.at(-1)!.file;
265 expect((await api("POST", `/alice/karts/builds/v1/shots/${last}`, aliceToken, JSON.stringify({ move: "up" }), "application/json")).status).toBe(200);
266 const moved = await tagged();
267 expect(moved).not.toBe(added);
268 expect((await api("DELETE", `/alice/karts/builds/v1/shots/${last}`, aliceToken)).status).toBe(204);
269 expect(await tagged()).not.toBe(moved);
270 });
271
246272 test("a stranger to a private repository gets 404 for its pictures and its writes", async () => {
247273 expect((await get("/alice/secret/builds/v1/shots/01.png", "alice")).status).toBe(200);
248274 expect((await get("/alice/secret/builds/v1/shots/01.png", "bob")).status).toBe(404);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs