Server-Bibliothek
vor 18 Std. 98bce1b0da675715b2e5482c0ad61ba21135f62f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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;
}