import { createUser, db, del, get, hasFfmpeg, insertDump, makeImage, PNG, test, TEST_DIR, webpWidth, } from "../helpers.ts"; import { assertEquals } from "@std/assert"; function createdAtOf(id: string): string { return (db.prepare(`SELECT created_at FROM dumps WHERE id = ?;`).get(id) as { created_at: string; }).created_at; } function readDir(path: string): string[] { try { return [...Deno.readDirSync(path)].map((e) => e.name); } catch { return []; } } test("GET /api/dumps/:id/thumbnail: serves a cached video still, with privacy", async () => { const owner = await createUser(); const video = insertDump({ userId: owner.id, fileName: "v.mp4", fileMime: "video/mp4", }); const priv = insertDump({ userId: owner.id, fileName: "p.mp4", fileMime: "video/mp4", isPrivate: true, }); Deno.mkdirSync(`${TEST_DIR}/uploads/thumbnails`, { recursive: true }); for (const { id } of [video, priv]) { Deno.writeFileSync(`${TEST_DIR}/uploads/thumbnails/${id}.jpg`, PNG); } const res = await get(`/api/dumps/${video.id}/thumbnail`); assertEquals(res.status, 200); assertEquals(res.headers.get("content-type"), "image/jpeg"); assertEquals(res.headers.get("cache-control"), "public, no-cache"); const v = (id: string) => `5-${Date.parse(createdAtOf(id))}`; assertEquals( (await get(`/api/dumps/${video.id}/thumbnail?v=${v(video.id)}`)).headers .get( "cache-control", ), "public, max-age=31536000, immutable", ); assertEquals((await get(`/api/dumps/${priv.id}/thumbnail`)).status, 404); const own = await get( `/api/dumps/${priv.id}/thumbnail?v=${v(priv.id)}&token=${owner.mediaToken}`, ); assertEquals(own.status, 200); assertEquals( own.headers.get("cache-control"), "private, max-age=31536000, immutable", ); const text = insertDump({ userId: owner.id, fileName: "t.txt" }); assertEquals((await get(`/api/dumps/${text.id}/thumbnail`)).status, 404); assertEquals((await get(`/api/dumps/nope/thumbnail`)).status, 400); }); test("GET /api/dumps/:id/thumbnail: an image dump gets a scaled copy, or itself when none can be made", async () => { const owner = await createUser(); const scaled = (id: string) => readDir(`${TEST_DIR}/uploads/scaled/dumps`).filter((n) => n.startsWith(id)) .sort(); // Not an image ffmpeg can read (or no ffmpeg at all): the original goes out. const gif = new TextEncoder().encode("GIF89a-not-really"); const broken = insertDump({ userId: owner.id, fileName: "a.gif", fileMime: "image/gif", fileData: gif, }); const fallback = await get(`/api/dumps/${broken.id}/thumbnail`); assertEquals(fallback.status, 200); assertEquals(fallback.headers.get("content-type"), "image/gif"); assertEquals(fallback.bytes, gif); assertEquals(scaled(broken.id), []); if (!await hasFfmpeg()) { const photo = insertDump({ userId: owner.id, fileName: "p.png", fileMime: "image/png", fileData: PNG, }); const itself = await get(`/api/dumps/${photo.id}/thumbnail`); assertEquals(itself.headers.get("content-type"), "image/png"); assertEquals(itself.bytes, PNG); return; } // A real image is scaled to the widest width without `w`, and to the width // asked for with it, each copy kept under the upload's own path. const photo = insertDump({ userId: owner.id, fileName: "p.png", fileMime: "image/png", fileData: await makeImage(1400, 300), }); const widest = await get(`/api/dumps/${photo.id}/thumbnail`); assertEquals(widest.status, 200); assertEquals(widest.headers.get("content-type"), "image/webp"); assertEquals(webpWidth(widest.bytes), 960); const small = await get(`/api/dumps/${photo.id}/thumbnail?w=128`); assertEquals(small.headers.get("content-type"), "image/webp"); assertEquals(webpWidth(small.bytes), 128); // A width between two of the list's takes the one above. const between = await get(`/api/dumps/${photo.id}/thumbnail?w=200`); assertEquals(webpWidth(between.bytes), 320); assertEquals(scaled(photo.id), [ `${photo.id}.w128.webp`, `${photo.id}.w320.webp`, `${photo.id}.w960.webp`, ]); // A reader without WebP, as a mail client, asks for a PNG or a JPEG. const compat = await get(`/api/dumps/${photo.id}/thumbnail?w=640&f=compat`); assertEquals(compat.status, 200); assertEquals(compat.headers.get("content-type"), "image/png"); // Its header states the width, on four bytes from the sixteenth. assertEquals(new DataView(compat.bytes.buffer).getUint32(16), 640); assertEquals(scaled(photo.id).includes(`${photo.id}.w640.png`), true); // An animation keeps its frames. const animated = insertDump({ userId: owner.id, fileName: "anim.gif", fileMime: "image/gif", fileData: await makeImage(400, 300, "blue", "gif"), }); const moving = await get(`/api/dumps/${animated.id}/thumbnail?w=128`); assertEquals(moving.headers.get("content-type"), "image/webp"); assertEquals(webpWidth(moving.bytes), 128); assertEquals(new TextDecoder().decode(moving.bytes).includes("ANMF"), true); // The copies go with the dump. assertEquals( (await del(`/api/dumps/${photo.id}`, { auth: owner.auth })).status, 200, ); assertEquals(scaled(photo.id), []); });