file server fix + middleware updates
This commit is contained in:
parent
130db9725a
commit
4aebd80a5d
5 changed files with 108 additions and 210 deletions
|
|
@ -1,58 +1,42 @@
|
|||
import { stat } from "fs/promises";
|
||||
import { createReadStream } from "fs";
|
||||
import path from "path";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getUserBearerFromRequest } from "@/lib/directus";
|
||||
import fs from "fs";
|
||||
import fsp from "fs/promises";
|
||||
import path from "path";
|
||||
import mime from "mime";
|
||||
|
||||
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 || "/"));
|
||||
const abs = path.resolve(rootResolved, "." + raw);
|
||||
if (abs === rootResolved) return abs;
|
||||
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.isDirectory()) {
|
||||
return NextResponse.json({ error: "Is a directory" }, { status: 400 });
|
||||
}
|
||||
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 fileName = path.basename(abs);
|
||||
|
||||
return new Response(stream as any, {
|
||||
const filename = path.basename(abs);
|
||||
const ctype = mime.getType(abs) || "application/octet-stream";
|
||||
const stream = fs.createReadStream(abs);
|
||||
return new NextResponse(stream as any, {
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Length": String(s.size),
|
||||
"Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`,
|
||||
"Cache-Control": "no-store",
|
||||
"Content-Type": ctype,
|
||||
"Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`,
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
});
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || "Not found";
|
||||
const code = msg === "Invalid path" ? 400 : 404;
|
||||
return NextResponse.json({ error: msg }, { status: code, headers: { "Cache-Control": "no-store" } });
|
||||
return new NextResponse(String(e?.message || e), { status: 400 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue