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,59 +1,40 @@
import { NextResponse } from "next/server";
import { stat } from "fs/promises";
import { createReadStream } from "fs";
import fs from "fs";
import fsp from "fs/promises";
import path from "path";
import mime from "mime";
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 || "/")); // always absolute-ish
const abs = path.resolve(rootResolved, "." + raw); // stays under root
if (abs === rootResolved) return abs; // allow root
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 {
// Auth gate: require ma_at
const bearer = getUserBearerFromRequest(req);
if (!bearer) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { searchParams } = new URL(req.url);
const p = searchParams.get("path");
if (!p) {
return NextResponse.json({ error: "Missing path" }, { status: 400 });
}
const reqPath = searchParams.get("path") || "";
if (!reqPath) return new NextResponse("Missing path", { status: 400 });
const abs = safeJoin(ROOT, p);
const s = await stat(abs);
if (!s.isFile()) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
const abs = safeJoin(ROOT, reqPath);
const stat = await fsp.stat(abs).catch(() => null);
if (!stat || !stat.isFile()) return new NextResponse("Not found", { status: 404 });
const stream = createReadStream(abs);
const type = mime.getType(abs) || "application/octet-stream";
return new Response(stream as any, {
const ctype = mime.getType(abs) || "application/octet-stream";
const stream = fs.createReadStream(abs);
return new NextResponse(stream as any, {
headers: {
"Content-Type": type,
"Content-Length": String(s.size),
// cache only for the requesting user/agent
"Cache-Control": "private, max-age=3600",
"Content-Type": ctype,
"Cache-Control": "public, max-age=3600",
},
});
} catch (err: any) {
const msg = err?.message || "Error";
const code = msg === "Invalid path" ? 400 : 404;
return NextResponse.json({ error: msg }, { status: code });
} catch (e: any) {
return new NextResponse(String(e?.message || e), { status: 400 });
}
}