import { NextResponse } from "next/server"; import { promises as fs } from "fs"; import path from "path"; const ROOT = (process.env.FILES_ROOT || "/files").trim(); function safeJoin(root: string, reqPath: string) { const clean = (reqPath || "/").replace(/^\/+/, ""); const joined = path.normalize(path.join(root, clean)); const rootNorm = path.normalize(root.endsWith(path.sep) ? root : root + path.sep); if (!joined.startsWith(rootNorm) && path.normalize(joined) !== path.normalize(root)) { throw new Error("Path traversal blocked"); } return joined; } export async function GET(req: Request) { try { const { searchParams } = new URL(req.url); const reqPath = searchParams.get("path") || "/"; const abs = safeJoin(ROOT, reqPath); const stat = await fs.stat(abs).catch(() => null); if (!stat) return NextResponse.json({ path: reqPath, items: [] }); if (!stat.isDirectory()) { const s = await fs.stat(abs); return NextResponse.json({ path: reqPath, items: [{ name: path.basename(abs), type: "file" as const, size: s.size, mtimeMs: s.mtimeMs, }], }); } const entries = await fs.readdir(abs, { withFileTypes: true }); const items = await Promise.all(entries.map(async (ent) => { const p = path.join(abs, ent.name); try { const s = await fs.stat(p); return { name: ent.name, type: ent.isDirectory() ? ("dir" as const) : ("file" as const), size: ent.isDirectory() ? 0 : s.size, mtimeMs: s.mtimeMs, }; } catch { return null; } })); // Hide dotfiles by default; remove filter if you want to show them const visible = (items.filter(Boolean) as any[]).filter(i => !i.name.startsWith(".")); return NextResponse.json({ path: reqPath, items: visible }); } catch (e: any) { return NextResponse.json({ error: String(e?.message || e) }, { status: 400 }); } }