From 98bce1b0da675715b2e5482c0ad61ba21135f62f Mon Sep 17 00:00:00 2001
From: Server-Bibliothek <server-bibliothek@lokal>
Date: Di, 04 Aug 2026 07:00:04 +0200
Subject: [PATCH] Add a desktop client that knows what is installed locally
---
packages/bibliothek-server/src/bezugsquellen/github.ts | 95 +++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 95 insertions(+), 0 deletions(-)
diff --git a/packages/bibliothek-server/src/bezugsquellen/github.ts b/packages/bibliothek-server/src/bezugsquellen/github.ts
new file mode 100644
index 0000000..de969d7
--- /dev/null
+++ b/packages/bibliothek-server/src/bezugsquellen/github.ts
@@ -0,0 +1,95 @@
+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;
+}
--
Gitblit v1.9.3