import { createUser, db, get, insertDump, PNG, test, TEST_DIR, } from "../helpers.ts"; import { assertEquals } from "@std/assert"; const BYTES = new TextEncoder().encode("0123456789"); function createdAtOf(id: string): string { return (db.prepare(`SELECT created_at FROM dumps WHERE id = ?;`).get(id) as { created_at: string; }).created_at; } async function hasFfmpeg(): Promise { try { return (await new Deno.Command("ffmpeg", { args: ["-version"], stdout: "null", stderr: "null", }).output()).success; } catch { return false; } } 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 preview, a GIF itself", async () => { const owner = await createUser(); const gif = new TextEncoder().encode("GIF89a-not-really"); const animated = insertDump({ userId: owner.id, fileName: "a.gif", fileMime: "image/gif", fileData: gif, }); const res = await get(`/api/dumps/${animated.id}/thumbnail`); assertEquals(res.status, 200); assertEquals(res.headers.get("content-type"), "image/gif"); assertEquals(res.bytes, gif); // A real PNG is scaled into a preview, kept beside the video stills. const photo = insertDump({ userId: owner.id, fileName: "p.png", fileMime: "image/png", fileData: PNG, }); const preview = await get(`/api/dumps/${photo.id}/thumbnail`); assertEquals(preview.status, 200); assertEquals(preview.headers.get("content-type"), "image/png"); if (await hasFfmpeg()) { assertEquals( readDir(`${TEST_DIR}/uploads/thumbnails`).filter((n) => n.startsWith(photo.id) ), [`${photo.id}.preview.png`], ); } // Not an image ffmpeg can read (or no ffmpeg at all): the original goes out. const broken = insertDump({ userId: owner.id, fileName: "b.png", fileMime: "image/png", fileData: BYTES, }); const fallback = await get(`/api/dumps/${broken.id}/thumbnail`); assertEquals(fallback.status, 200); assertEquals(fallback.headers.get("content-type"), "image/png"); assertEquals(fallback.bytes, BYTES); assertEquals( readDir(`${TEST_DIR}/uploads/thumbnails`).filter((n) => n.startsWith(broken.id) ), [], ); });