// /app/api/files/list/route.ts import { NextResponse } from "next/server"; import { promises as fs } from "fs"; import path from "path"; import { getUserBearerFromRequest } from "@/lib/directus"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; export const revalidate = 0; const ROOT = process.env.FILES_ROOT || "/app/files"; function safeJoin(root: string, p: string) { const rootResolved = path.resolve(root); const raw = path.normalize("/" + (p || "/")); // normalize input const abs = path.resolve(rootResolved, "." + raw); // resolve under root if (abs === rootResolved) return abs; // allow root itself if (abs.startsWith(rootResolved + path.sep)) return abs; throw new Error("Invalid path"); } export async function GET(req: Request) { try { // Require auth (no anonymous browsing) const bearer = getUserBearerFromRequest(req); if (!bearer) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { searchParams } = new URL(req.url); const input = searchParams.get("path") || "/"; const abs = safeJoin(ROOT, input); const s = await fs.stat(abs); if (!s.isDirectory()) { return NextResponse.json({ error: "Not a directory" }, { status: 400 }); } const entries = await fs.readdir(abs, { withFileTypes: true }); const items = await Promise.all( entries.map(async (d) => { const p = path.join(abs, d.name); const st = await fs.stat(p); return { name: d.name, isDir: d.isDirectory(), size: st.size, mtime: st.mtimeMs, }; }) ); // Optional: directories first, then alphabetical items.sort((a, b) => a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1 ); const safePath = input.startsWith("/") ? input : `/${input}`; return NextResponse.json( { path: safePath, items }, { headers: { "Cache-Control": "no-store" } } ); } catch (err: any) { const msg = err?.message === "Invalid path" ? "Invalid path" : "Not found"; const code = msg === "Invalid path" ? 400 : 404; return NextResponse.json({ error: msg }, { status: code }); } }