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/umzug.ts | 131 +++++++++++++++++++++++++++++++++++++++++++
1 files changed, 131 insertions(+), 0 deletions(-)
diff --git a/packages/bibliothek-server/src/umzug.ts b/packages/bibliothek-server/src/umzug.ts
new file mode 100644
index 0000000..dc7accb
--- /dev/null
+++ b/packages/bibliothek-server/src/umzug.ts
@@ -0,0 +1,131 @@
+import type { Einstellungen, Software } from 'bibliothek-types';
+import { alleSoftware, softwareAnlegen, softwareLoeschen, umzugsAngabenSetzen } from './datenbank';
+import { aktuelleEinstellungen, speichereEinstellungen } from './konfiguration';
+import { ladeGeheimnisse, loescheGeheimnisse, speichereGeheimnisse, type Geheimniswert } from './geheimnisse';
+
+/**
+ * Umzug auf einen anderen Rechner.
+ *
+ * Bewusst als lesbares JSON und nicht als Ordner-Kopie:
+ * - Die geholten Dateien (mehrere hundert MB) und die Git-Arbeitskopien (GB)
+ * sind jederzeit reproduzierbar — ein Abgleich holt sie neu. Nur Katalog,
+ * Einstellungen und Geheimnisse sind unersetzlich, und das sind wenige KB.
+ * - Die Datenbank im laufenden Betrieb zu kopieren wäre unsicher: die jüngsten
+ * Änderungen stehen im WAL und fehlten in der Kopie.
+ * - JSON übersteht Schema-Änderungen: beim Einlesen werden Felder zugeordnet,
+ * statt eine Binärdatei einer bestimmten Version vorauszusetzen.
+ */
+
+/** Aktuelle Fassung des Austauschformats — erlaubt später sauberes Nachziehen. */
+const FORMAT_FASSUNG = 1;
+
+export interface UmzugsPaket {
+ fassung: number;
+ erstelltAm: string;
+ einstellungen: Einstellungen;
+ software: Software[];
+ /** Nur enthalten, wenn ausdrücklich mit exportiert (enthält Klartext-Schlüssel!). */
+ geheimnisse?: Record<string, Geheimniswert[]>;
+}
+
+export function erstelleUmzugsPaket(mitGeheimnissen: boolean): UmzugsPaket {
+ const eintraege = alleSoftware();
+ const paket: UmzugsPaket = {
+ fassung: FORMAT_FASSUNG,
+ erstelltAm: new Date().toISOString(),
+ einstellungen: aktuelleEinstellungen(),
+ software: eintraege,
+ };
+ if (mitGeheimnissen) {
+ paket.geheimnisse = {};
+ for (const eintrag of eintraege) {
+ const werte = ladeGeheimnisse(eintrag.id);
+ if (werte.length > 0) {
+ paket.geheimnisse[String(eintrag.id)] = werte;
+ }
+ }
+ }
+ return paket;
+}
+
+export interface UmzugsErgebnis {
+ uebernommen: number;
+ mitGeheimnissen: number;
+ hinweis: string;
+}
+
+/**
+ * Liest ein Paket ein und ersetzt den bisherigen Katalog vollständig.
+ * Die eigentlichen Programm-Dateien kommen nicht mit — sie werden beim
+ * nächsten Abgleich neu geholt bzw. gebaut.
+ */
+export function uebernehmeUmzugsPaket(paket: UmzugsPaket): UmzugsErgebnis {
+ if (!paket || typeof paket !== 'object' || !Array.isArray(paket.software)) {
+ throw new Error('Die Datei ist kein gültiges Umzugs-Paket.');
+ }
+ if (paket.fassung > FORMAT_FASSUNG) {
+ throw new Error(`Das Paket stammt aus einer neueren Fassung (${paket.fassung}) — bitte die Anwendung aktualisieren.`);
+ }
+
+ // Bisherigen Bestand räumen, damit keine Mischung aus altem und neuem Stand entsteht.
+ for (const vorhandene of alleSoftware()) {
+ loescheGeheimnisse(vorhandene.id);
+ softwareLoeschen(vorhandene.id);
+ }
+
+ // Die Ids ändern sich beim Neuanlegen — für die Eltern-Zuordnung mitführen.
+ const idZuordnung = new Map<number, number>();
+ for (const eintrag of paket.software.filter((eintrag) => !eintrag.elternId)) {
+ idZuordnung.set(eintrag.id, uebernehmeEintrag(eintrag, 0).id);
+ }
+ for (const eintrag of paket.software.filter((eintrag) => eintrag.elternId)) {
+ idZuordnung.set(eintrag.id, uebernehmeEintrag(eintrag, idZuordnung.get(eintrag.elternId) ?? 0).id);
+ }
+
+ let mitGeheimnissen = 0;
+ for (const [alteId, werte] of Object.entries(paket.geheimnisse ?? {})) {
+ const neueId = idZuordnung.get(Number(alteId));
+ if (neueId) {
+ speichereGeheimnisse(neueId, werte);
+ mitGeheimnissen++;
+ }
+ }
+
+ if (paket.einstellungen) {
+ // Datenordner und Port gehören zum Zielrechner und werden bewusst nicht überschrieben.
+ speichereEinstellungen({
+ gitBenutzer: paket.einstellungen.gitBenutzer ?? '',
+ gitSchluessel: paket.einstellungen.gitSchluessel ?? '',
+ });
+ }
+
+ return {
+ uebernommen: idZuordnung.size,
+ mitGeheimnissen,
+ hinweis: 'Die Programm-Dateien wurden nicht mit übertragen — sie werden beim nächsten Abgleich neu geholt bzw. gebaut.',
+ };
+}
+
+/** Legt einen Eintrag neu an und stellt den bekannten Abgleich-Stand wieder her. */
+function uebernehmeEintrag(eintrag: Software, elternId: number): Software {
+ const angelegt = softwareAnlegen({
+ name: eintrag.name ?? '',
+ beschreibung: eintrag.beschreibung ?? '',
+ elternId,
+ quelleTyp: eintrag.quelleTyp ?? '',
+ quelleWert: eintrag.quelleWert ?? '',
+ bauBefehl: eintrag.bauBefehl ?? '',
+ bauErgebnis: eintrag.bauErgebnis ?? '',
+ einstufung: eintrag.einstufung ?? 'lizenzfrei',
+ programmart: eintrag.programmart ?? 'unbestimmt',
+ hinweis: eintrag.hinweis ?? '',
+ anleitungUrl: eintrag.anleitungUrl ?? '',
+ erwarteterHerausgeber: eintrag.erwarteterHerausgeber ?? '',
+ });
+ // Version und Datei bleiben leer (Standard beim Anlegen): Die Datei fehlt auf dem
+ // neuen Rechner noch, der nächste Abgleich soll sie wirklich holen. Bewusst KEIN
+ // fehlgeschlagener Abgleich — „noch nicht geholt" ist kein Problemfall und soll
+ // nicht in der Warnleiste auftauchen.
+ umzugsAngabenSetzen(angelegt.id, eintrag.veroeffentlichtAm ?? '', eintrag.benoetigteGeheimnisse ?? []);
+ return angelegt;
+}
--
Gitblit v1.9.3