import { NextResponse } from "next/server"; import fs from "node:fs/promises"; import fssync from "node:fs"; import path from "node:path"; const BASE = "/app/files"; // this is /var/www/makearmy.io/app/files on the host function safeJoin(base: string, reqPath: string) { const decoded = decodeURIComponent(reqPath || "/"); // normalize, strip traversal, and join under BASE const normalized = path.posix.normalize("/" + decoded).replace(/^(\.\.(\/|\\|$))+/g, ""); const full = path.join(base, normalized); if (!full.startsWith(base)) throw new Error("Invalid path"); return full; } export async function GET(req: Request) { try { const { searchParams } = new URL(req.url); const p = searchParams.get("path") || "/"; const abs = safeJoin(BASE, p); const entries = await fs.readdir(abs, { withFileTypes: true }); const rows = await Promise.all(entries.map(async (ent) => { const full = path.join(abs, ent.name); const stat = await fs.stat(full); const isDir = ent.isDirectory(); return { name: ent.name, type: isDir ? "dir" : "file", size: isDir ? null : stat.size, mtime: stat.mtime.toISOString(), path: path.posix.join(p.endsWith("/") ? p : p + "/", ent.name), // raw download/view URL (served by /api/files/raw) url: isDir ? null : `/api/files/raw?path=${encodeURIComponent(path.posix.join(p, ent.name))}`, }; })); // Sort: directories first, then files alphabetically rows.sort((a, b) => { if (a.type !== b.type) return a.type === "dir" ? -1 : 1; return a.name.localeCompare(b.name, undefined, { sensitivity: "base" }); }); return NextResponse.json({ ok: true, path: p, items: rows }); } catch (err: any) { return NextResponse.json({ ok: false, error: err?.message || "Error" }, { status: 400 }); } }