mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
Refactor NodeCG runtime preparation and update handling
- Updated paths and configurations in doctor.mjs and prepare-nodecg-runtime.mjs to use new build-config.mjs imports. - Enhanced runtime installation checks and permissions validation. - Introduced new update configuration management in update-config.ts, including loading and validating update settings. - Implemented update service for managing update checks and downloads in update-service.ts. - Replaced update-utils.ts with update-schema.ts for better structure and clarity in update handling. - Added comprehensive tests for update download and settings management. - Ensured secure handling of download URLs and improved error handling in update processes.
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
export type GiteaReleaseAsset = {
|
||||
name: string;
|
||||
browserDownloadUrl: string;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
export type GiteaRelease = {
|
||||
tagName: string;
|
||||
title?: string;
|
||||
pageUrl?: string;
|
||||
assets: GiteaReleaseAsset[];
|
||||
};
|
||||
|
||||
export type InstallerAsset = {
|
||||
name: string;
|
||||
downloadUrl: string;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
export type ReleaseUpdate = {
|
||||
version: string;
|
||||
title: string;
|
||||
pageUrl?: string;
|
||||
installer: InstallerAsset;
|
||||
};
|
||||
|
||||
export type UpdateFileConfig = {
|
||||
enabled?: unknown;
|
||||
apiUrl?: unknown;
|
||||
releasePageUrl?: unknown;
|
||||
assetPattern?: unknown;
|
||||
};
|
||||
|
||||
type UrlPolicy = {
|
||||
allowInsecureHttp: boolean;
|
||||
};
|
||||
|
||||
export function parseGiteaRelease(value: unknown): GiteaRelease | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tagName = readRequiredString(value.tag_name);
|
||||
const assets = Array.isArray(value.assets) ? value.assets.map(parseGiteaReleaseAsset).filter(isPresent) : null;
|
||||
|
||||
if (!tagName || !assets) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
tagName,
|
||||
assets,
|
||||
...(readOptionalString(value.name) ? { title: readOptionalString(value.name) } : {}),
|
||||
...(readOptionalUrlString(value.html_url) ? { pageUrl: readOptionalUrlString(value.html_url) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function isVersionNewer(candidateVersion: string, currentVersion: string): boolean {
|
||||
const candidate = normalizeVersion(candidateVersion);
|
||||
const current = normalizeVersion(currentVersion);
|
||||
|
||||
for (let index = 0; index < Math.max(candidate.length, current.length); index += 1) {
|
||||
const candidatePart = candidate[index] ?? 0;
|
||||
const currentPart = current[index] ?? 0;
|
||||
|
||||
if (candidatePart > currentPart) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (candidatePart < currentPart) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function selectInstallerAsset(
|
||||
release: GiteaRelease,
|
||||
assetPattern: string,
|
||||
policy: UrlPolicy = { allowInsecureHttp: true },
|
||||
): InstallerAsset | null {
|
||||
const matcher = new RegExp(assetPattern, "i");
|
||||
|
||||
for (const asset of release.assets) {
|
||||
if (!matcher.test(asset.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const downloadUrl = validateHttpUrl(asset.browserDownloadUrl, policy);
|
||||
if (!downloadUrl) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return {
|
||||
name: asset.name,
|
||||
downloadUrl,
|
||||
...(typeof asset.size === "number" ? { size: asset.size } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function buildReleaseUpdate(
|
||||
release: GiteaRelease,
|
||||
currentVersion: string,
|
||||
assetPattern: string,
|
||||
policy: UrlPolicy = { allowInsecureHttp: true },
|
||||
): ReleaseUpdate | null {
|
||||
const version = release.tagName.replace(/^v/i, "");
|
||||
if (!version || !isVersionNewer(version, currentVersion)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const installer = selectInstallerAsset(release, assetPattern, policy);
|
||||
if (!installer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pageUrl = release.pageUrl ? validateHttpUrl(release.pageUrl, policy) ?? undefined : undefined;
|
||||
|
||||
return {
|
||||
version,
|
||||
title: release.title ?? `Scoreko ${version}`,
|
||||
...(pageUrl ? { pageUrl } : {}),
|
||||
installer,
|
||||
};
|
||||
}
|
||||
|
||||
export function sanitizeFileName(fileName: string): string {
|
||||
const sanitized = fileName.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").trim();
|
||||
return sanitized.length > 0 ? sanitized : "scoreko-update-installer";
|
||||
}
|
||||
|
||||
export function validateHttpUrl(value: string, policy: UrlPolicy): string | null {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
|
||||
if (url.protocol === "https:" || (policy.allowInsecureHttp && url.protocol === "http:")) {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function parseGiteaReleaseAsset(value: unknown): GiteaReleaseAsset | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = readRequiredString(value.name);
|
||||
const browserDownloadUrl = readRequiredString(value.browser_download_url);
|
||||
|
||||
if (!name || !browserDownloadUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
browserDownloadUrl,
|
||||
...(typeof value.size === "number" && value.size >= 0 ? { size: value.size } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeVersion(version: string): number[] {
|
||||
return version
|
||||
.trim()
|
||||
.replace(/^v/i, "")
|
||||
.split(/[+-]/)[0]
|
||||
.split(".")
|
||||
.map((part) => Number.parseInt(part, 10))
|
||||
.map((part) => (Number.isFinite(part) ? part : 0));
|
||||
}
|
||||
|
||||
function readRequiredString(value: unknown): string | null {
|
||||
const text = readOptionalString(value);
|
||||
return text && text.length > 0 ? text : null;
|
||||
}
|
||||
|
||||
function readOptionalString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function readOptionalUrlString(value: unknown): string | undefined {
|
||||
const rawValue = readOptionalString(value);
|
||||
if (!rawValue) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return validateHttpUrl(rawValue, { allowInsecureHttp: true }) ?? undefined;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isPresent<T>(value: T | null): value is T {
|
||||
return value !== null;
|
||||
}
|
||||
Reference in New Issue
Block a user