mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
194 lines
4.5 KiB
TypeScript
194 lines
4.5 KiB
TypeScript
import { isRecord, readNonEmptyString } from "../utils/unknown-values";
|
|
|
|
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;
|
|
};
|
|
|
|
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;
|
|
}
|
|
|
|
const title = readNonEmptyString(value.name);
|
|
const pageUrl = readOptionalUrlString(value.html_url);
|
|
|
|
return {
|
|
tagName,
|
|
assets,
|
|
...(title ? { title } : {}),
|
|
...(pageUrl ? { pageUrl } : {}),
|
|
};
|
|
}
|
|
|
|
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 = readNonEmptyString(value);
|
|
return text && text.length > 0 ? text : null;
|
|
}
|
|
|
|
function readOptionalUrlString(value: unknown): string | undefined {
|
|
const rawValue = readNonEmptyString(value);
|
|
if (!rawValue) {
|
|
return undefined;
|
|
}
|
|
|
|
return validateHttpUrl(rawValue, { allowInsecureHttp: true }) ?? undefined;
|
|
}
|
|
|
|
function isPresent<T>(value: T | null): value is T {
|
|
return value !== null;
|
|
}
|