zero-knowledge file sharing
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111import { Hono } from "hono";
import { config } from "./config.ts";import { createFile, getFile, peekFile, unlinkFile } from "./db.ts";
const DURATION_UNITS: Record<string, number> = { s: 1, m: 60, h: 3600, d: 86400,};
function parseDuration(s: string): number | undefined { const n = parseInt(s); const mult = DURATION_UNITS[s.slice(-1)]; if (isNaN(n) || mult === undefined) return undefined; return n * mult;}
const FILES_DIR = `${config.dataDir}/files`;const MAX_FILE_SIZE = config.maxFileSize;const MAX_TTL = parseDuration(config.maxTtl)!;
const file = new Hono();
file.post("/", async (c) => { const formData = await c.req.formData(); const fileField = formData.get("file"); const expiresIn = formData.get("expiresIn"); const burnAfterRead = formData.get("burnAfterRead") === "true";
if (!fileField || !(fileField instanceof File)) { return c.json({ error: "file field is required" }, 400); }
if (fileField.size > MAX_FILE_SIZE) { return c.json({ error: "File too large" }, 413); }
const expiresInStr = typeof expiresIn === "string" ? expiresIn.trim() : ""; const expiresInSec = expiresInStr ? parseDuration(expiresInStr) : undefined; if (!expiresInSec) { return c.json( { error: "Invalid lifetime. Use a duration like 30m, 24h, 7d" }, 400, ); } if (expiresInSec > MAX_TTL) { return c.json({ error: "expiresIn exceeds maximum allowed TTL" }, 400); }
const id = crypto.randomUUID(); const expiresAt = Math.floor(Date.now() / 1000) + expiresInSec; const filePath = `${FILES_DIR}/${id}`;
const buffer = await fileField.arrayBuffer(); await Bun.write(filePath, buffer);
try { createFile(id, expiresAt, burnAfterRead); } catch (err) { unlinkFile(id); throw err; }
return c.json({ id });});
file.get("/:id/info", (c) => { const id = c.req.param("id"); const row = peekFile(id);
if (!row) { return c.json({ error: "File not found or expired" }, 404); }
const bunFile = Bun.file(`${FILES_DIR}/${id}`);
return c.json({ id, expiresAt: row.expires_at, burnAfterRead: row.burn_after_read === 1, size: bunFile.size, });});
file.get("/:id", (c) => { const id = c.req.param("id"); const row = getFile(id);
if (!row) { return c.json({ error: "File not found or expired" }, 404); }
const filePath = `${FILES_DIR}/${id}`; const bunFile = Bun.file(filePath);
const headers = new Headers({ "Content-Type": "application/octet-stream", "Content-Length": String(bunFile.size), });
if (row.burn_after_read) { setTimeout(() => unlinkFile(id), 0); }
return new Response(bunFile, { headers });});
export default file;