Proxy local ZXDB/WoS mirror downloads through application API

- Created `src/app/api/zxdb/download/route.ts` to serve local files.
- Updated `resolveLocalLink` in `src/server/repo/zxdb.ts` to return
  API-relative URLs with `source` and `path` parameters.
- Encodes the relative subpath to ensure correct URL construction.
- Includes security checks in the API route to prevent path traversal.
- Updated `docs/ZXDB.md` to reflect the proxy mechanism.

Signed-off: junie@lucy.xalior.com
This commit is contained in:
2026-02-17 12:43:26 +00:00
parent 2e47b598c1
commit 32985c33b9
3 changed files with 65 additions and 7 deletions

View File

@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from "next/server";
import fs from "fs";
import path from "path";
import { env } from "@/env";
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const source = searchParams.get("source");
const filePath = searchParams.get("path");
if (!source || !filePath) {
return new NextResponse("Missing source or path", { status: 400 });
}
let baseDir: string | undefined;
if (source === "zxdb") {
baseDir = env.ZXDB_LOCAL_FILEPATH;
} else if (source === "wos") {
baseDir = env.WOS_LOCAL_FILEPATH;
}
if (!baseDir) {
return new NextResponse("Invalid source or mirroring not enabled", { status: 400 });
}
// Security: Ensure path doesn't escape baseDir
const absolutePath = path.normalize(path.join(baseDir, filePath));
if (!absolutePath.startsWith(path.normalize(baseDir))) {
return new NextResponse("Forbidden", { status: 403 });
}
if (!fs.existsSync(absolutePath)) {
return new NextResponse("File not found", { status: 404 });
}
const stat = fs.statSync(absolutePath);
if (!stat.isFile()) {
return new NextResponse("Not a file", { status: 400 });
}
const fileBuffer = fs.readFileSync(absolutePath);
const fileName = path.basename(absolutePath);
return new NextResponse(fileBuffer, {
headers: {
"Content-Type": "application/octet-stream",
"Content-Disposition": `attachment; filename="${fileName}"`,
"Content-Length": stat.size.toString(),
},
});
}
export const runtime = "nodejs";

View File

@@ -116,20 +116,25 @@ export interface EntryFacets {
*/
function resolveLocalLink(fileLink: string): string | null {
let localPath: string | null = null;
let source: "zxdb" | "wos" | null = null;
let subPath: string | null = null;
const zxdbPrefix = env.ZXDB_FILE_PREFIX || "";
const wosPrefix = env.WOS_FILE_PREFIX || "";
if (fileLink.startsWith(zxdbPrefix) && env.ZXDB_LOCAL_FILEPATH) {
const sub = fileLink.slice(zxdbPrefix.replace(/\/$/, "").length);
localPath = path.join(env.ZXDB_LOCAL_FILEPATH, sub);
subPath = fileLink.slice(zxdbPrefix.replace(/\/$/, "").length);
localPath = path.join(env.ZXDB_LOCAL_FILEPATH, subPath);
source = "zxdb";
} else if (fileLink.startsWith(wosPrefix) && env.WOS_LOCAL_FILEPATH) {
const sub = fileLink.slice(wosPrefix.replace(/\/$/, "").length);
localPath = path.join(env.WOS_LOCAL_FILEPATH, sub);
subPath = fileLink.slice(wosPrefix.replace(/\/$/, "").length);
localPath = path.join(env.WOS_LOCAL_FILEPATH, subPath);
source = "wos";
}
if (localPath && fs.existsSync(localPath)) {
return localPath;
if (localPath && fs.existsSync(localPath) && source && subPath) {
// Return an application-relative URL instead of the absolute filesystem path
return `/api/zxdb/download?source=${source}&path=${encodeURIComponent(subPath)}`;
}
return null;