import {
|
abgleichMisserfolg,
|
alsIsoDatum,
|
dateiVorhanden,
|
ersetzeDateiSicher,
|
inhaltBeanstanden,
|
type Abgleichauftrag,
|
type Bezugsquelle,
|
type QuellenAbgleich,
|
} from './basis';
|
|
interface GithubAsset {
|
name: string;
|
browser_download_url: string;
|
}
|
|
interface GithubRelease {
|
tag_name: string;
|
/** Veröffentlichungszeitpunkt des Releases beim Anbieter. */
|
published_at: string;
|
assets: GithubAsset[];
|
}
|
|
/** GitHub-Releases: holt das neueste Release und lädt ein passendes Installer-Asset. */
|
export class GithubQuelle implements Bezugsquelle {
|
async gleicheAb(auftrag: Abgleichauftrag): Promise<QuellenAbgleich> {
|
try {
|
const antwort = await fetch(`https://api.github.com/repos/${auftrag.wert}/releases/latest`, {
|
headers: { 'User-Agent': 'software-bibliothek', Accept: 'application/vnd.github+json' },
|
});
|
if (!antwort.ok) {
|
return abgleichMisserfolg(auftrag.aktuelleVersion, `GitHub nicht erreichbar (HTTP ${antwort.status}).`);
|
}
|
const release = (await antwort.json()) as GithubRelease;
|
const asset = waehlePassendesAsset(release.assets);
|
if (!asset) {
|
return abgleichMisserfolg(auftrag.aktuelleVersion, `Im Release ${release.tag_name} liegt keine herunterladbare Datei.`);
|
}
|
const veroeffentlichtAm = alsIsoDatum(release.published_at);
|
if (release.tag_name === auftrag.aktuelleVersion && dateiVorhanden(auftrag.zielordner, asset.name)) {
|
return { erfolg: true, geaendert: false, version: release.tag_name, dateiname: asset.name, meldung: 'Bereits aktuell.', veroeffentlichtAm };
|
}
|
|
const dateiAntwort = await fetch(asset.browser_download_url, { headers: { 'User-Agent': 'software-bibliothek' }, redirect: 'follow' });
|
if (!dateiAntwort.ok) {
|
return abgleichMisserfolg(auftrag.aktuelleVersion, `Asset-Download fehlgeschlagen (HTTP ${dateiAntwort.status}).`);
|
}
|
const inhalt = Buffer.from(await dateiAntwort.arrayBuffer());
|
const beanstandung = inhaltBeanstanden(inhalt, asset.name, dateiAntwort.headers.get('content-type'), auftrag.vorherigeGroesse);
|
if (beanstandung) {
|
return abgleichMisserfolg(auftrag.aktuelleVersion, beanstandung);
|
}
|
|
ersetzeDateiSicher(auftrag.zielordner, asset.name, inhalt);
|
return { erfolg: true, geaendert: true, version: release.tag_name, dateiname: asset.name, meldung: `Auf ${release.tag_name} aktualisiert.`, veroeffentlichtAm };
|
} catch (fehler) {
|
return abgleichMisserfolg(auftrag.aktuelleVersion, `GitHub-Abgleich fehlgeschlagen: ${fehler instanceof Error ? fehler.message : String(fehler)}`);
|
}
|
}
|
}
|
|
/**
|
* Wählt aus den Release-Dateien den passenden Windows-Installer aus.
|
*
|
* Bewertet statt „erster Treffer gewinnt", damit die Reihenfolge im Release
|
* das Ergebnis nicht beeinflusst: 64-Bit vor allem anderen, ARM und 32-Bit
|
* ausgeschlossen, `.exe` vor `.msi`, portable Varianten nur als Notnagel.
|
*/
|
export function waehlePassendesAsset(assets: GithubAsset[]): GithubAsset | undefined {
|
const kandidaten = assets
|
.filter((eintrag) => /\.(exe|msi|msix|zip)$/i.test(eintrag.name))
|
.filter((eintrag) => !/arm|aarch|x86|ia32|32.?bit/i.test(eintrag.name));
|
if (kandidaten.length === 0) {
|
return undefined;
|
}
|
const bewertet = kandidaten.map((eintrag) => ({ eintrag, punkte: bewerteAsset(eintrag.name) }));
|
bewertet.sort((links, rechts) => rechts.punkte - links.punkte);
|
return bewertet[0].eintrag;
|
}
|
|
function bewerteAsset(name: string): number {
|
let punkte = 0;
|
if (/x64|amd64|win64|64.?bit/i.test(name)) {
|
punkte += 100; // ausdrücklich 64-Bit
|
}
|
if (/\.exe$/i.test(name)) {
|
punkte += 20; // Installer bevorzugt gegenüber .msi/.zip
|
} else if (/\.msi$/i.test(name)) {
|
punkte += 10;
|
}
|
if (/portable/i.test(name)) {
|
punkte -= 50; // nur nehmen, wenn es nichts anderes gibt
|
}
|
return punkte;
|
}
|