file server fix + middleware updates

This commit is contained in:
makearmy 2025-10-15 21:10:28 -04:00
parent 130db9725a
commit 4aebd80a5d
5 changed files with 108 additions and 210 deletions

View file

@ -1,69 +1,62 @@
// /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 || "/files").trim();
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");
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 {
// 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 reqPath = searchParams.get("path") || "/";
const abs = safeJoin(ROOT, reqPath);
const abs = safeJoin(ROOT, input);
const s = await fs.stat(abs);
if (!s.isDirectory()) {
return NextResponse.json({ error: "Not a directory" }, { status: 400 });
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 (d) => {
const p = path.join(abs, d.name);
const st = await fs.stat(p);
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: d.name,
isDir: d.isDirectory(),
size: st.size,
mtime: st.mtimeMs,
name: ent.name,
type: ent.isDirectory() ? ("dir" as const) : ("file" as const),
size: ent.isDirectory() ? 0 : s.size,
mtimeMs: s.mtimeMs,
};
})
);
} catch {
return null;
}
}));
// Optional: directories first, then alphabetical
items.sort((a, b) =>
a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1
);
// Hide dotfiles by default; remove filter if you want to show them
const visible = (items.filter(Boolean) as any[]).filter(i => !i.name.startsWith("."));
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 });
return NextResponse.json({ path: reqPath, items: visible });
} catch (e: any) {
return NextResponse.json({ error: String(e?.message || e) }, { status: 400 });
}
}