Files
2026-07-13 13:08:41 +08:00

83 lines
2.7 KiB
TypeScript

import fs from "fs";
import fsPromises from "fs/promises";
import path from "path";
import { Readable } from "stream";
import { NextRequest, NextResponse } from "next/server";
const CONTENT_TYPES: Record<string, string> = {
".pdf": "application/pdf",
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
".zip": "application/zip",
};
function getExportsDirectory(): string {
const appDataDirectory = process.env.APP_DATA_DIRECTORY?.trim();
if (!appDataDirectory) {
throw new Error("APP_DATA_DIRECTORY is required to download exported files.");
}
return path.join(appDataDirectory, "exports");
}
function getSafeExportName(request: NextRequest): string | null {
const decodedName = request.nextUrl.searchParams.get("name");
if (
!decodedName ||
decodedName.includes("/") ||
decodedName.includes("\\") ||
decodedName !== path.basename(decodedName)
) {
return null;
}
return decodedName;
}
function contentDisposition(filename: string): string {
const fallback = filename.replace(/[^a-zA-Z0-9._-]/g, "_");
return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
}
export async function GET(request: NextRequest) {
const filename = getSafeExportName(request);
if (!filename) {
return NextResponse.json({ error: "Invalid export file name" }, { status: 400 });
}
try {
const exportsDirectory = getExportsDirectory();
const resolvedExportsDirectory = await fsPromises.realpath(exportsDirectory);
const filePath = path.join(exportsDirectory, filename);
const resolvedFilePath = await fsPromises.realpath(filePath);
if (
resolvedFilePath !== resolvedExportsDirectory &&
!resolvedFilePath.startsWith(resolvedExportsDirectory + path.sep)
) {
return NextResponse.json({ error: "Access denied" }, { status: 403 });
}
const ext = path.extname(filename).toLowerCase();
const stats = await fsPromises.stat(resolvedFilePath);
const stream = Readable.toWeb(fs.createReadStream(resolvedFilePath));
return new NextResponse(stream as unknown as BodyInit, {
headers: {
"Content-Type": CONTENT_TYPES[ext] ?? "application/octet-stream",
"Content-Disposition": contentDisposition(filename),
"Content-Length": String(stats.size),
"Cache-Control": "no-store",
},
});
} catch (error) {
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
return NextResponse.json({ error: "Export file not found" }, { status: 404 });
}
console.error("[export-presentation:file]", error);
return NextResponse.json(
{ error: "Failed to download export file" },
{ status: 500 }
);
}
}