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/winget.ts | 81 ++++++++++++++++++++++++++++++++++++++++
1 files changed, 81 insertions(+), 0 deletions(-)
diff --git a/packages/bibliothek-server/src/bezugsquellen/winget.ts b/packages/bibliothek-server/src/bezugsquellen/winget.ts
new file mode 100644
index 0000000..0c9264a
--- /dev/null
+++ b/packages/bibliothek-server/src/bezugsquellen/winget.ts
@@ -0,0 +1,81 @@
+import { execFile } from 'child_process';
+import { promisify } from 'util';
+import * as fs from 'fs';
+import * as path from 'path';
+import { abgleichMisserfolg, alsIsoDatum, mitTemporaeremOrdner, type Abgleichauftrag, type Bezugsquelle, type QuellenAbgleich } from './basis';
+
+const execFileAsync = promisify(execFile);
+
+/** winget: lädt das Paket per `winget download` (nur unter Windows verfügbar). */
+export class WingetQuelle implements Bezugsquelle {
+ async gleicheAb(auftrag: Abgleichauftrag): Promise<QuellenAbgleich> {
+ // In einen temporären Ordner laden — der bisherige Stand bleibt erhalten, falls etwas schiefgeht.
+ const ablage = mitTemporaeremOrdner(auftrag.zielordner);
+ try {
+ const { stdout } = await execFileAsync(
+ 'winget',
+ [
+ 'download',
+ '--id',
+ auftrag.wert,
+ '--exact',
+ '--download-directory',
+ ablage.ordner,
+ '--accept-package-agreements',
+ '--accept-source-agreements',
+ '--disable-interactivity',
+ ],
+ { timeout: 300000, windowsHide: true }
+ );
+ const geladeneDateien = fs.readdirSync(ablage.ordner);
+ const installer = geladeneDateien.find((name) => /\.(exe|msi|msix|appx|appxbundle|zip)$/i.test(name));
+ if (!installer) {
+ ablage.verwerfen();
+ return abgleichMisserfolg(auftrag.aktuelleVersion, 'winget hat keine Installer-Datei geliefert.');
+ }
+ // winget legt zusätzlich sein Manifest (.yaml) ab — im Ablageordner soll nur der Installer liegen.
+ for (const name of geladeneDateien) {
+ if (name !== installer) {
+ fs.rmSync(path.join(ablage.ordner, name), { recursive: true, force: true });
+ }
+ }
+ const version = versionAusAusgabe(stdout) || new Date().toISOString();
+ ablage.uebernehmen();
+ return {
+ erfolg: true,
+ geaendert: true,
+ version,
+ dateiname: installer,
+ meldung: `Über winget geladen (${version}).`,
+ // Das Veröffentlichungsdatum steht nur in der Paket-Auskunft, nicht in der Download-Ausgabe.
+ veroeffentlichtAm: alsIsoDatum(await veroeffentlichungsdatum(auftrag.wert)),
+ };
+ } catch (fehler) {
+ ablage.verwerfen();
+ return abgleichMisserfolg(auftrag.aktuelleVersion, `winget fehlgeschlagen: ${fehler instanceof Error ? fehler.message : String(fehler)}`);
+ }
+ }
+}
+
+function versionAusAusgabe(ausgabe: string): string {
+ const treffer = ausgabe.match(/[Vv]ersion[:\s]+([0-9][0-9.]*)/);
+ return treffer ? treffer[1] : '';
+}
+
+/**
+ * Fragt das Veröffentlichungsdatum beim Paket-Verzeichnis ab. `winget download`
+ * gibt es nicht aus, `winget show` schon — dafür ein zweiter, kurzer Aufruf.
+ */
+async function veroeffentlichungsdatum(paketId: string): Promise<string> {
+ try {
+ const { stdout } = await execFileAsync(
+ 'winget',
+ ['show', '--id', paketId, '--exact', '--accept-source-agreements', '--disable-interactivity'],
+ { timeout: 120000, windowsHide: true }
+ );
+ const treffer = stdout.match(/(?:Release\s*Date|Ver[öo]ffentlich\w*)[:\s]+([0-9]{4}-[0-9]{2}-[0-9]{2})/i);
+ return treffer ? treffer[1] : '';
+ } catch {
+ return ''; // ohne Datum ist die Zeile nur weniger aussagekräftig, kein Grund zu scheitern
+ }
+}
--
Gitblit v1.9.3