/** * Serve files from /Users/guto/ufo/processing/* and /Users/guto/ufo/raw/* via * /api/static/png//, /api/static/crops///, etc. * * Sandboxed to UFO_ROOT to prevent path traversal. */ import fs from "node:fs/promises"; import path from "node:path"; import { NextResponse } from "next/server"; import { UFO_ROOT } from "@/lib/wiki"; const ALLOWED_ROOTS = ["processing", "raw"]; function mimeFor(ext: string): string { switch (ext.toLowerCase()) { case ".png": return "image/png"; case ".jpg": case ".jpeg": return "image/jpeg"; case ".webp": return "image/webp"; case ".gif": return "image/gif"; case ".svg": return "image/svg+xml"; case ".pdf": return "application/pdf"; case ".txt": return "text/plain; charset=utf-8"; case ".json": return "application/json"; case ".mp4": return "video/mp4"; default: return "application/octet-stream"; } } export async function GET(_req: Request, ctx: { params: Promise<{ path: string[] }> }) { const { path: parts } = await ctx.params; if (!parts || parts.length < 2) { return new NextResponse("Bad Request", { status: 400 }); } const root = parts[0]; if (!ALLOWED_ROOTS.includes(root)) { return new NextResponse("Forbidden", { status: 403 }); } const rel = parts.slice(1).join("/"); const abs = path.resolve(UFO_ROOT, root, rel); // Path traversal guard const expectedPrefix = path.resolve(UFO_ROOT, root) + path.sep; if (!abs.startsWith(expectedPrefix) && abs !== path.resolve(UFO_ROOT, root)) { return new NextResponse("Forbidden", { status: 403 }); } try { const buf = await fs.readFile(abs); const ext = path.extname(abs); return new NextResponse(buf, { headers: { "content-type": mimeFor(ext), "cache-control": "public, max-age=3600", }, }); } catch { return new NextResponse("Not Found", { status: 404 }); } }