import { Router } from "@oak/oak"; import { APIErrorCode, APIException } from "../model/interfaces.ts"; import { getDump } from "../services/dump-service.ts"; import { DUMPS_DIR } from "../utils/upload.ts"; const router = new Router({ prefix: "/api/files" }); router.get("/:dumpId", async (ctx) => { const { dumpId } = ctx.params; // Guard against path traversal (UUIDs are safe, but be explicit) if (!/^[0-9a-f-]{36}$/.test(dumpId)) { throw new APIException(APIErrorCode.BAD_REQUEST, 400, "Invalid dump ID"); } const dump = getDump(dumpId); if (dump.kind !== "file" || !dump.fileMime || !dump.fileName) { throw new APIException( APIErrorCode.NOT_FOUND, 404, "No file for this dump", ); } const path = `${DUMPS_DIR}/${dumpId}`; let data: Uint8Array; try { data = await Deno.readFile(path); } catch { throw new APIException(APIErrorCode.NOT_FOUND, 404, "File not found"); } const total = data.byteLength; ctx.response.headers.set("Content-Type", dump.fileMime); ctx.response.headers.set( "Content-Disposition", `inline; filename="${dump.fileName}"`, ); ctx.response.headers.set("Accept-Ranges", "bytes"); const rangeHeader = ctx.request.headers.get("Range"); if (rangeHeader) { const match = rangeHeader.match(/^bytes=(\d*)-(\d*)$/); if (!match) { ctx.response.status = 416; ctx.response.headers.set("Content-Range", `bytes */${total}`); return; } const start = match[1] ? parseInt(match[1], 10) : total - parseInt(match[2], 10); const end = match[2] ? Math.min(parseInt(match[2], 10), total - 1) : total - 1; if (start > end || start >= total) { ctx.response.status = 416; ctx.response.headers.set("Content-Range", `bytes */${total}`); return; } const chunk = data.subarray(start, end + 1); ctx.response.status = 206; ctx.response.headers.set("Content-Range", `bytes ${start}-${end}/${total}`); ctx.response.headers.set("Content-Length", String(chunk.byteLength)); ctx.response.body = chunk; } else { ctx.response.headers.set("Content-Length", String(total)); ctx.response.body = data; } }); export default router;