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:
@@ -12,12 +12,12 @@ PROTO=http
|
||||
ZXDB_URL=mysql://zxdb_readonly:password@hostname:3306/zxdb
|
||||
|
||||
# Base HTTP locations for CDN sources used by downloads.file_link
|
||||
# When file_link starts with /zxdb, it will be fetched from ZXDB_FILEPATH
|
||||
ZXDB_FILEPATH=https://zxdbfiles.com/
|
||||
# When file_link starts with /zxdb, it will be fetched from ZXDB_REMOTE_FILEPATH
|
||||
ZXDB_REMOTE_FILEPATH=https://zxdbfiles.com/
|
||||
|
||||
# When file_link starts with /public, it will be fetched from WOS_FILEPATH
|
||||
# When file_link starts with /public, it will be fetched from WOS_REMOTE_FILEPATH
|
||||
# Note: Example uses the Internet Archive WoS mirror; keep the trailing slash
|
||||
WOS_FILEPATH=https://archive.org/download/World_of_Spectrum_June_2017_Mirror/World%20of%20Spectrum%20June%202017%20Mirror.zip/World%20of%20Spectrum%20June%202017%20Mirror/
|
||||
WOS_REMOTE_FILEPATH=https://archive.org/download/World_of_Spectrum_June_2017_Mirror/World%20of%20Spectrum%20June%202017%20Mirror.zip/World%20of%20Spectrum%20June%202017%20Mirror/
|
||||
|
||||
# Local cache root where files will be mirrored (without the leading slash)
|
||||
CDN_CACHE=/mnt/files/zxfiles
|
||||
|
||||
@@ -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>
|
||||
<div className="d-flex flex-column gap-1">
|
||||
{isHttp ? (
|
||||
<a href={d.link} target="_blank" rel="noopener noreferrer">{d.link}</a>
|
||||
<a href={d.link} target="_blank" rel="noopener noreferrer" className="text-break small">{d.link}</a>
|
||||
) : (
|
||||
<span>{d.link}</span>
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
<div className="d-flex flex-column gap-1">
|
||||
{isHttp ? (
|
||||
<a href={d.link} target="_blank" rel="noopener noreferrer">{d.link}</a>
|
||||
<a href={d.link} target="_blank" rel="noopener noreferrer" className="text-break small">{d.link}</a>
|
||||
) : (
|
||||
<span>{d.link}</span>
|
||||
<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>
|
||||
<div className="d-flex flex-column gap-1">
|
||||
{s.link ? (
|
||||
isHttp ? (
|
||||
<a href={s.link} target="_blank" rel="noopener noreferrer">{s.link}</a>
|
||||
<a href={s.link} target="_blank" rel="noopener noreferrer" className="text-break small">{s.link}</a>
|
||||
) : (
|
||||
<span>{s.link}</span>
|
||||
<span className="text-break small">{s.link}</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>
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user