Correct local file path resolution for ZXDB/WoS mirrors

- Remove optional path prefix and prepend the required local string.
- Avoid hardcoded 'SC' or 'WoS' subdirectories in path mapping.
- Maintain binary state: show local link only if env var is set and file exists.

Signed-off: junie@McFiver.local
This commit is contained in:
2026-02-17 12:30:55 +00:00
parent cbee214a6b
commit 77b5e76a08
6 changed files with 87 additions and 22 deletions

View File

@@ -103,6 +103,7 @@ export type EntryDetailData = {
case: { id: string | null; name: string | null };
year: number | null;
releaseSeq: number;
localLink?: string | null;
}[];
releases?: {
releaseSeq: number;
@@ -125,6 +126,7 @@ export type EntryDetailData = {
source: { id: string | null; name: string | null };
case: { id: string | null; name: string | null };
year: number | null;
localLink?: string | null;
}[];
}[];
// Additional relationships
@@ -392,11 +394,18 @@ export default function EntryDetailClient({ data }: { data: EntryDetailData | nu
<tr key={d.id}>
<td><span className="badge text-bg-secondary">{d.type.name}</span></td>
<td>
{isHttp ? (
<a href={d.link} target="_blank" rel="noopener noreferrer">{d.link}</a>
) : (
<span>{d.link}</span>
)}
<div className="d-flex flex-column gap-1">
{isHttp ? (
<a href={d.link} target="_blank" rel="noopener noreferrer" className="text-break small">{d.link}</a>
) : (
<span className="text-break small">{d.link}</span>
)}
{d.localLink && (
<a href={d.localLink} className="btn btn-xs btn-success py-0 px-1" style={{ fontSize: "0.7rem", width: "fit-content" }}>
Local Mirror
</a>
)}
</div>
</td>
<td className="text-end">{typeof d.size === "number" ? d.size.toLocaleString() : "-"}</td>
<td><code>{d.md5 ?? "-"}</code></td>

View File

@@ -43,6 +43,7 @@ type ReleaseDetailData = {
source: { id: string | null; name: string | null };
case: { id: string | null; name: string | null };
year: number | null;
localLink?: string | null;
}>;
scraps: Array<{
id: number;
@@ -58,6 +59,7 @@ type ReleaseDetailData = {
source: { id: string | null; name: string | null };
case: { id: string | null; name: string | null };
year: number | null;
localLink?: string | null;
}>;
files: Array<{
id: number;
@@ -376,11 +378,18 @@ export default function ReleaseDetailClient({ data }: { data: ReleaseDetailData
<tr key={d.id}>
<td><span className="badge text-bg-secondary">{d.type.name}</span></td>
<td>
{isHttp ? (
<a href={d.link} target="_blank" rel="noopener noreferrer">{d.link}</a>
) : (
<span>{d.link}</span>
)}
<div className="d-flex flex-column gap-1">
{isHttp ? (
<a href={d.link} target="_blank" rel="noopener noreferrer" className="text-break small">{d.link}</a>
) : (
<span className="text-break small">{d.link}</span>
)}
{d.localLink && (
<a href={d.localLink} className="btn btn-xs btn-success py-0 px-1" style={{ fontSize: "0.7rem", width: "fit-content" }}>
Local Mirror
</a>
)}
</div>
</td>
<td className="text-end">{typeof d.size === "number" ? d.size.toLocaleString() : "-"}</td>
<td><code>{d.md5 ?? "-"}</code></td>
@@ -438,15 +447,22 @@ export default function ReleaseDetailClient({ data }: { data: ReleaseDetailData
<tr key={s.id}>
<td><span className="badge text-bg-secondary">{s.type.name}</span></td>
<td>
{s.link ? (
isHttp ? (
<a href={s.link} target="_blank" rel="noopener noreferrer">{s.link}</a>
<div className="d-flex flex-column gap-1">
{s.link ? (
isHttp ? (
<a href={s.link} target="_blank" rel="noopener noreferrer" className="text-break small">{s.link}</a>
) : (
<span className="text-break small">{s.link}</span>
)
) : (
<span>{s.link}</span>
)
) : (
<span className="text-secondary">-</span>
)}
<span className="text-secondary">-</span>
)}
{s.localLink && (
<a href={s.localLink} className="btn btn-xs btn-success py-0 px-1" style={{ fontSize: "0.7rem", width: "fit-content" }}>
Local Mirror
</a>
)}
</div>
</td>
<td className="text-end">{typeof s.size === "number" ? s.size.toLocaleString() : "-"}</td>
<td>

View File

@@ -14,6 +14,10 @@ const serverSchema = z.object({
ZXDB_FILE_PREFIX: z.string().optional(),
WOS_FILE_PREFIX: z.string().optional(),
// Local file paths for mirroring
ZXDB_LOCAL_FILEPATH: z.string().optional(),
WOS_LOCAL_FILEPATH: z.string().optional(),
// OIDC Configuration
OIDC_PROVIDER_URL: z.string().url().optional(),
OIDC_CLIENT_ID: z.string().optional(),

View File

@@ -1,5 +1,8 @@
import { and, desc, eq, sql, asc } from "drizzle-orm";
import { cache } from "react";
import fs from "fs";
import path from "path";
import { env } from "@/env";
// import { alias } from "drizzle-orm/mysql-core";
import { db } from "@/server/db";
import {
@@ -108,6 +111,30 @@ export interface EntryFacets {
};
}
/**
* Resolves a local link for a given file link if mirroring is enabled and the file exists.
*/
function resolveLocalLink(fileLink: string): string | null {
let localPath: string | null = null;
const zxdbPrefix = env.ZXDB_FILE_PREFIX || "/zxdb/sinclair/";
const wosPrefix = env.WOS_FILE_PREFIX || "/pub/sinclair/";
if (fileLink.startsWith(zxdbPrefix) && env.ZXDB_LOCAL_FILEPATH) {
const sub = fileLink.slice(zxdbPrefix.replace(/\/$/, "").length);
localPath = path.join(env.ZXDB_LOCAL_FILEPATH, sub);
} else if (fileLink.startsWith(wosPrefix) && env.WOS_LOCAL_FILEPATH) {
const sub = fileLink.slice(wosPrefix.replace(/\/$/, "").length);
localPath = path.join(env.WOS_LOCAL_FILEPATH, sub);
}
if (localPath && fs.existsSync(localPath)) {
return localPath;
}
return null;
}
function buildEntrySearchUnion(pattern: string, scope: EntrySearchScope) {
const parts: Array<ReturnType<typeof sql>> = [
sql`select ${searchByTitles.entryId} as entry_id from ${searchByTitles} where lower(${searchByTitles.entryTitle}) like ${pattern}`,
@@ -479,6 +506,7 @@ export interface EntryDetail {
case: { id: string | null; name: string | null };
year: number | null;
releaseSeq: number;
localLink?: string | null;
}[];
releases?: {
releaseSeq: number;
@@ -501,6 +529,7 @@ export interface EntryDetail {
source: { id: string | null; name: string | null };
case: { id: string | null; name: string | null };
year: number | null;
localLink?: string | null;
}[];
}[];
// Additional relationships surfaced on the entry detail page
@@ -710,6 +739,7 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
source: { id: (d.sourceId) ?? null, name: (d.sourceName) ?? null },
case: { id: (d.caseId) ?? null, name: (d.caseName) ?? null },
year: d.year != null ? Number(d.year) : null,
localLink: resolveLocalLink(d.link),
})),
}));
@@ -1148,6 +1178,7 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
case: { id: (d.caseId) ?? null, name: (d.caseName) ?? null },
year: d.year != null ? Number(d.year) : null,
releaseSeq: Number(d.releaseSeq),
localLink: resolveLocalLink(d.link),
})),
aliases: aliasRows.map((a) => ({ releaseSeq: Number(a.releaseSeq), languageId: a.languageId, title: a.title })),
webrefs: webrefRows.map((w) => ({ link: w.link, languageId: w.languageId, website: { id: Number(w.websiteId), name: w.websiteName, link: w.websiteLink } })),
@@ -2104,6 +2135,7 @@ export interface ReleaseDetail {
source: { id: string | null; name: string | null };
case: { id: string | null; name: string | null };
year: number | null;
localLink?: string | null;
}>;
scraps: Array<{
id: number;
@@ -2119,6 +2151,7 @@ export interface ReleaseDetail {
source: { id: string | null; name: string | null };
case: { id: string | null; name: string | null };
year: number | null;
localLink?: string | null;
}>;
files: Array<{
id: number;
@@ -2371,6 +2404,7 @@ export async function getReleaseDetail(entryId: number, releaseSeq: number): Pro
source: { id: d.sourceId ?? null, name: d.sourceName ?? null },
case: { id: d.caseId ?? null, name: d.caseName ?? null },
year: d.year != null ? Number(d.year) : null,
localLink: resolveLocalLink(d.link),
})),
scraps: (scrapRows as ScrapRow[]).map((s) => ({
id: Number(s.id),
@@ -2386,6 +2420,7 @@ export async function getReleaseDetail(entryId: number, releaseSeq: number): Pro
source: { id: s.sourceId ?? null, name: s.sourceName ?? null },
case: { id: s.caseId ?? null, name: s.caseName ?? null },
year: s.year != null ? Number(s.year) : null,
localLink: s.link ? resolveLocalLink(s.link) : null,
})),
files: fileRows.map((f) => ({
id: f.id,