40 lines
1.4 KiB
TypeScript
40 lines
1.4 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import fs from "fs";
|
|
import fsp from "fs/promises";
|
|
import path from "path";
|
|
import mime from "mime";
|
|
|
|
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") || "";
|
|
if (!reqPath) return new NextResponse("Missing path", { 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 ctype = mime.getType(abs) || "application/octet-stream";
|
|
const stream = fs.createReadStream(abs);
|
|
return new NextResponse(stream as any, {
|
|
headers: {
|
|
"Content-Type": ctype,
|
|
"Cache-Control": "public, max-age=3600",
|
|
},
|
|
});
|
|
} catch (e: any) {
|
|
return new NextResponse(String(e?.message || e), { status: 400 });
|
|
}
|
|
}
|