makearmy-app/app/api/files/list/route.ts

62 lines
2.1 KiB
TypeScript

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 });
}
}