62 lines
2.1 KiB
TypeScript
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 });
|
|
}
|
|
}
|