Files
doktainer-upstream/src/server/services/app-catalog/service.ts
T
2026-06-02 00:17:50 +07:00

123 lines
3.2 KiB
TypeScript

import { APP_TEMPLATES } from "../../config/app-templates";
import {
getCachedCatalog,
getCacheKey,
makeCachedResult,
setCachedCatalog,
} from "./cache";
import { CatalogSyncResult, CatalogSourceMode } from "./types";
import { parseArchiveResponse } from "./archive";
import { dedupeTemplates } from "./normalize";
import { normalizeSingleManifest } from "./manifest-parsers";
import {
fetchRemoteResponse,
getFallbackIdFromUrl,
isZipResponse,
normalizeRemoteUrl,
} from "./remote";
import {
CatalogSourceValidationError,
isLikelyHtmlDocument,
parseStructuredText,
} from "./utils";
function detectTextFormat(text: string): string {
return text.trim().startsWith("{") || text.trim().startsWith("[")
? "json"
: "yaml";
}
async function loadRemoteTextCatalog(
source: Extract<CatalogSourceMode, "auto-detect" | "manifest-url">,
url: string,
label: string,
): Promise<CatalogSyncResult> {
const normalizedUrl = normalizeRemoteUrl(source, url);
const response = await fetchRemoteResponse(normalizedUrl);
if (isZipResponse(response, normalizedUrl)) {
return parseArchiveResponse(response, source, label, normalizedUrl);
}
const text = await response.text();
if (isLikelyHtmlDocument(text, response.headers.get("content-type"))) {
throw new CatalogSourceValidationError();
}
const payload = parseStructuredText(text);
const fallbackId = getFallbackIdFromUrl(normalizedUrl);
return {
templates: dedupeTemplates(
normalizeSingleManifest(payload, fallbackId, normalizedUrl),
),
meta: {
source,
label,
url: normalizedUrl,
fetchedAt: new Date().toISOString(),
isRemote: true,
format: detectTextFormat(text),
},
};
}
async function loadGithubArchiveCatalog(
url: string,
): Promise<CatalogSyncResult> {
const normalizedUrl = normalizeRemoteUrl("github-archive", url);
const response = await fetchRemoteResponse(normalizedUrl);
return parseArchiveResponse(
response,
"github-archive",
"GitHub Archive ZIP",
normalizedUrl,
);
}
export async function getCatalogTemplates(
source: CatalogSourceMode,
url?: string,
): Promise<CatalogSyncResult> {
const fetchedAt = new Date().toISOString();
if (source === "local") {
return {
templates: APP_TEMPLATES,
meta: {
source,
label: "Built-in Catalog",
fetchedAt,
isRemote: false,
cached: false,
},
};
}
if (!url?.trim()) {
throw new Error("Remote catalog URL is required");
}
const trimmedUrl = url.trim();
const cacheKey = getCacheKey(source, trimmedUrl);
const cachedEntry = getCachedCatalog(cacheKey);
if (cachedEntry) {
return makeCachedResult(cachedEntry);
}
const result =
source === "auto-detect"
? await loadRemoteTextCatalog(source, trimmedUrl, "Custom Source")
: source === "manifest-url"
? await loadRemoteTextCatalog(source, trimmedUrl, "Remote Manifest URL")
: await loadGithubArchiveCatalog(trimmedUrl);
if (result.templates.length === 0) {
throw new Error(
"No app templates could be extracted from the remote source",
);
}
return setCachedCatalog(cacheKey, result.templates, result.meta);
}