From 32985c33b990c71c74a086e98dc9df28fc00001b Mon Sep 17 00:00:00 2001 From: "D. Rimron-Soutter" Date: Tue, 17 Feb 2026 12:43:26 +0000 Subject: [PATCH] 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 --- docs/ZXDB.md | 2 +- src/app/api/zxdb/download/route.ts | 53 ++++++++++++++++++++++++++++++ src/server/repo/zxdb.ts | 17 ++++++---- 3 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 src/app/api/zxdb/download/route.ts diff --git a/docs/ZXDB.md b/docs/ZXDB.md index 94a7785..f420acc 100644 --- a/docs/ZXDB.md +++ b/docs/ZXDB.md @@ -63,7 +63,7 @@ WOS_FILE_PREFIX=/pub/sinclair/ 2. It strips the prefix from the database link. 3. It joins the remaining relative path to the corresponding `*_LOCAL_FILEPATH`. 4. It checks if the file exists on the local disk. -5. If the file exists and the environment variable is set, a "Local Mirror" link is displayed in the UI. +5. If the file exists and the environment variable is set, a "Local Mirror" link is displayed in the UI, pointing to a proxy download API (`/api/zxdb/download`). Note: Obtaining these mirrors is left as an exercise to the host. The paths do not need to share a common parent directory. Both mirrors are optional and independent; you can configure one, both, or neither. diff --git a/src/app/api/zxdb/download/route.ts b/src/app/api/zxdb/download/route.ts new file mode 100644 index 0000000..5951cb2 --- /dev/null +++ b/src/app/api/zxdb/download/route.ts @@ -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"; diff --git a/src/server/repo/zxdb.ts b/src/server/repo/zxdb.ts index e26a4ef..72b1c40 100644 --- a/src/server/repo/zxdb.ts +++ b/src/server/repo/zxdb.ts @@ -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;