zero-knowledge file sharing
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

/info endpoint

Juliet 55291b15 5387b24a

+33 -17
+5 -7
public/view.html
··· 19 19 > 20 20 </h1> 21 21 22 - <!-- Metadata card (shown after HEAD) --> 22 + <!-- Metadata card (shown after /info) --> 23 23 <div id="meta" class="hidden"> 24 24 <div class="font-mono text-base mb-1" id="meta-name"></div> 25 25 <div class="flex gap-3 text-muted text-xs mb-1"> ··· 262 262 263 263 const key = await importKey(keyEncoded); 264 264 265 - // HEAD — get metadata without fetching or burning the file 266 - const headRes = await fetch(`/api/file/${id}`, { method: "HEAD" }); 267 - if (!headRes.ok) { 265 + // /info — get metadata without fetching or burning the file 266 + const infoRes = await fetch(`/api/file/${id}/info`); 267 + if (!infoRes.ok) { 268 268 errorEl.textContent = "File not found or expired."; 269 269 return; 270 270 } 271 271 272 - const burnAfterRead = headRes.headers.get("X-Burn-After-Read") === "1"; 273 - const expiresAt = parseInt(headRes.headers.get("X-Expires-At") || "0"); 274 - const size = parseInt(headRes.headers.get("Content-Length") || "0"); 272 + const { burnAfterRead, expiresAt, size } = await infoRes.json(); 275 273 276 274 metaName.textContent = "Encrypted file"; 277 275 metaSize.textContent = formatBytes(size);
+28 -10
src/routes/file.ts
··· 2 2 import { createFile, getFile, peekFile, unlinkFile } from "../db.ts"; 3 3 import { config } from "../config.ts"; 4 4 5 - const DURATION_UNITS: Record<string, number> = { s: 1, m: 60, h: 3600, d: 86400 }; 5 + const DURATION_UNITS: Record<string, number> = { 6 + s: 1, 7 + m: 60, 8 + h: 3600, 9 + d: 86400, 10 + }; 6 11 7 12 function parseDuration(s: string): number | undefined { 8 13 const n = parseInt(s); ··· 61 66 return c.json({ id, deleteToken }); 62 67 }); 63 68 64 - file.on(["HEAD", "GET"], "/:id", (c) => { 69 + file.get("/:id/info", (c) => { 70 + const id = c.req.param("id"); 71 + const row = peekFile(id); 72 + 73 + if (!row) { 74 + return c.json({ error: "File not found or expired" }, 404); 75 + } 76 + 77 + const bunFile = Bun.file(`${FILES_DIR}/${id}`); 78 + 79 + return c.json({ 80 + id, 81 + expiresAt: row.expires_at, 82 + burnAfterRead: row.burn_after_read === 1, 83 + size: bunFile.size, 84 + }); 85 + }); 86 + 87 + file.get("/:id", (c) => { 65 88 const id = c.req.param("id"); 66 - const isHead = c.req.method === "HEAD"; 67 - const row = isHead ? peekFile(id) : getFile(id); 89 + const row = getFile(id); 68 90 69 91 if (!row) { 70 92 return c.json({ error: "File not found or expired" }, 404); ··· 76 98 const headers = new Headers({ 77 99 "Content-Type": "application/octet-stream", 78 100 "Content-Length": String(bunFile.size), 79 - "X-Expires-At": String(row.expires_at), 80 - "X-Burn-After-Read": row.burn_after_read ? "1" : "0", 81 101 }); 82 102 83 - const response = new Response(bunFile, { headers }); 84 - 85 - if (!isHead && row.burn_after_read) { 103 + if (row.burn_after_read) { 86 104 setTimeout(() => unlinkFile(id), 0); 87 105 } 88 106 89 - return response; 107 + return new Response(bunFile, { headers }); 90 108 }); 91 109 92 110 export default file;