makearmy-app/app/api/files/download/route.ts

58 lines
1.9 KiB
TypeScript

import { stat } from "fs/promises";
import { createReadStream } from "fs";
import path from "path";
import { NextResponse } from "next/server";
import { getUserBearerFromRequest } from "@/lib/directus";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export const revalidate = 0;
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");
}
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 abs = safeJoin(ROOT, p);
const s = await stat(abs);
if (s.isDirectory()) {
return NextResponse.json({ error: "Is a directory" }, { status: 400 });
}
const stream = createReadStream(abs);
const fileName = path.basename(abs);
return new Response(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",
},
});
} 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" } });
}
}