import { Hono } from "hono"; import { config } from "./config.ts"; import { createFile, getFile, peekFile, unlinkFile } from "./db.ts"; const DURATION_UNITS: Record = { 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;