Reduce Seafile manifest churn when endpoint settings change
Store Seafile references as share tokens and resolve full URLs at sync time, while removing the now-obsolete temporary access notice from the homepage after HTTPS migration. Constraint: Seafile host/domain can change independently from content manifests Rejected: Keeping /f/<token>/?dl=1 in manifest entries | repeats fixed URL boilerplate and increases bulk-edit risk Confidence: high Scope-risk: narrow Directive: Keep Seafile manifest values token-first unless a non-standard external link is explicitly required Tested: npm run build; ./scripts/deploy-homepage.sh --dev-deploy Not-tested: Manual click-through verification for every share/download link on production domain Co-authored-by: OmX <omx@oh-my-codex.dev>
This commit is contained in:
@@ -201,10 +201,11 @@ async function resolveResource(
|
||||
syncedAt: string;
|
||||
},
|
||||
): Promise<ProjectDownload> {
|
||||
const resolvedResourceUrl = resolveResourceUrl(resource.url, input.config.baseUrl);
|
||||
const fallback: ProjectDownload = {
|
||||
name: resource.name,
|
||||
description: resource.description,
|
||||
url: resource.url,
|
||||
url: resolvedResourceUrl,
|
||||
type: resource.type,
|
||||
platform: resource.platform,
|
||||
size: resource.size,
|
||||
@@ -230,7 +231,7 @@ async function resolveResource(
|
||||
return {
|
||||
name: resource.name || detail.name || path.basename(resource.path),
|
||||
description: resource.description,
|
||||
url: downloadUrl || fallback.url,
|
||||
url: resolveResourceUrl(downloadUrl, input.config.baseUrl) || fallback.url,
|
||||
type: resource.type,
|
||||
platform: resource.platform,
|
||||
size: detail.size ?? resource.size,
|
||||
@@ -355,6 +356,48 @@ function canUseSeafileApi(config: SeafileSyncConfig) {
|
||||
return Boolean(config.baseUrl && config.token);
|
||||
}
|
||||
|
||||
function resolveResourceUrl(value: string | undefined, baseUrl: string) {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const normalizedPath = normalizeSeafilePath(value);
|
||||
if (!normalizedPath) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isAbsoluteUrl(normalizedPath)) {
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
if (!baseUrl) {
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
return new URL(normalizedPath, `${trimTrailingSlash(baseUrl)}/`).toString();
|
||||
}
|
||||
|
||||
function isAbsoluteUrl(value: string) {
|
||||
return /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value);
|
||||
}
|
||||
|
||||
function normalizeSeafilePath(value: string) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isAbsoluteUrl(trimmed) || trimmed.startsWith('/')) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
if (/^[A-Za-z0-9_-]+$/.test(trimmed)) {
|
||||
return `/f/${trimmed}/?dl=1`;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function trimTrailingSlash(value: string) {
|
||||
return value.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
type StaticAssetManifestItem = {
|
||||
url: string;
|
||||
source: string;
|
||||
target: string;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ type StaticAssetManifestGroup = {
|
||||
type StaticAssetConfig = {
|
||||
rootDir: string;
|
||||
manifestPath: string;
|
||||
seafileBaseUrl: string;
|
||||
strict: boolean;
|
||||
requestTimeoutMs: number;
|
||||
};
|
||||
@@ -63,6 +64,7 @@ function loadConfig(rootDir: string): StaticAssetConfig {
|
||||
rootDir,
|
||||
process.env.STATIC_ASSET_MANIFEST ?? 'src/content/static-assets/index.json',
|
||||
),
|
||||
seafileBaseUrl: process.env.SEAFILE_BASE_URL?.trim() ?? '',
|
||||
strict: getBooleanEnv('STATIC_ASSET_SYNC_STRICT', false),
|
||||
requestTimeoutMs: getNumberEnv('STATIC_ASSET_REQUEST_TIMEOUT_MS', 15000),
|
||||
};
|
||||
@@ -107,10 +109,10 @@ function normalizeLegacyManifestItem(
|
||||
record: Record<string, unknown>,
|
||||
index: number,
|
||||
): StaticAssetManifestItem {
|
||||
const url = typeof record.url === 'string' ? record.url.trim() : '';
|
||||
const source = typeof record.url === 'string' ? record.url.trim() : '';
|
||||
const target = typeof record.target === 'string' ? record.target.trim() : '';
|
||||
|
||||
if (!url) {
|
||||
if (!source) {
|
||||
throw new Error(`manifest item #${index + 1} is missing url`);
|
||||
}
|
||||
|
||||
@@ -118,7 +120,7 @@ function normalizeLegacyManifestItem(
|
||||
throw new Error(`manifest item #${index + 1} is missing target`);
|
||||
}
|
||||
|
||||
return { url, target };
|
||||
return { source, target };
|
||||
}
|
||||
|
||||
function normalizeManifestGroup(
|
||||
@@ -141,10 +143,10 @@ function normalizeManifestGroup(
|
||||
}
|
||||
|
||||
const record = item as Record<string, unknown>;
|
||||
const url = typeof record.url === 'string' ? record.url.trim() : '';
|
||||
const source = typeof record.url === 'string' ? record.url.trim() : '';
|
||||
const filename = typeof record.filename === 'string' ? record.filename.trim() : '';
|
||||
|
||||
if (!url) {
|
||||
if (!source) {
|
||||
throw new Error(`manifest group #${index + 1} file #${fileIndex + 1} is missing url`);
|
||||
}
|
||||
|
||||
@@ -155,7 +157,7 @@ function normalizeManifestGroup(
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
source,
|
||||
target: path.posix.join(targetDir.replace(/\\/g, '/'), filename),
|
||||
};
|
||||
});
|
||||
@@ -166,7 +168,8 @@ async function syncAsset(item: StaticAssetManifestItem, config: StaticAssetConfi
|
||||
await mkdir(path.dirname(targetPath), { recursive: true });
|
||||
|
||||
const tempPath = `${targetPath}.tmp`;
|
||||
const response = await fetchWithTimeout(item.url, config.requestTimeoutMs);
|
||||
const resolvedUrl = resolveManifestUrl(item.source, config.seafileBaseUrl);
|
||||
const response = await fetchWithTimeout(resolvedUrl, config.requestTimeoutMs);
|
||||
|
||||
try {
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
@@ -176,7 +179,7 @@ async function syncAsset(item: StaticAssetManifestItem, config: StaticAssetConfi
|
||||
await rename(tempPath, targetPath);
|
||||
|
||||
console.log(
|
||||
`[sync-static-assets] wrote ${path.relative(config.rootDir, targetPath)} <- ${item.url}`,
|
||||
`[sync-static-assets] wrote ${path.relative(config.rootDir, targetPath)} <- ${resolvedUrl}`,
|
||||
);
|
||||
} catch (error) {
|
||||
await safeRemove(tempPath);
|
||||
@@ -236,10 +239,54 @@ function getNumberEnv(name: string, fallback: number) {
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
|
||||
function resolveManifestUrl(value: string, baseUrl: string) {
|
||||
const normalizedPath = normalizeSeafilePath(value);
|
||||
if (!normalizedPath) {
|
||||
throw new Error('manifest item url is empty');
|
||||
}
|
||||
|
||||
if (isAbsoluteUrl(normalizedPath)) {
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
if (!baseUrl) {
|
||||
throw new Error(
|
||||
`relative asset url "${normalizedPath}" requires SEAFILE_BASE_URL in .env`,
|
||||
);
|
||||
}
|
||||
|
||||
return new URL(normalizedPath, `${trimTrailingSlash(baseUrl)}/`).toString();
|
||||
}
|
||||
|
||||
function isAbsoluteUrl(value: string) {
|
||||
return /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value);
|
||||
}
|
||||
|
||||
function normalizeSeafilePath(value: string) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isAbsoluteUrl(trimmed) || trimmed.startsWith('/')) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
if (/^[A-Za-z0-9_-]+$/.test(trimmed)) {
|
||||
return `/f/${trimmed}/?dl=1`;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function resolveFromRoot(rootDir: string, targetPath: string) {
|
||||
return path.isAbsolute(targetPath) ? targetPath : path.join(rootDir, targetPath);
|
||||
}
|
||||
|
||||
function trimTrailingSlash(value: string) {
|
||||
return value.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function isMissingFileError(error: unknown) {
|
||||
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user