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/svn.ts |   53 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 53 insertions(+), 0 deletions(-)

diff --git a/packages/bibliothek-server/src/bezugsquellen/svn.ts b/packages/bibliothek-server/src/bezugsquellen/svn.ts
new file mode 100644
index 0000000..0be2f6f
--- /dev/null
+++ b/packages/bibliothek-server/src/bezugsquellen/svn.ts
@@ -0,0 +1,53 @@
+import { execFile } from 'child_process';
+import { promisify } from 'util';
+import * as path from 'path';
+import { abgleichMisserfolg, alsIsoDatum, dateiVorhanden, mitTemporaeremOrdner, type Abgleichauftrag, type Bezugsquelle, type QuellenAbgleich } from './basis';
+
+const execFileAsync = promisify(execFile);
+
+/**
+ * SVN: exportiert eine Datei aus einem Repository, erkennt Änderungen über die Revision.
+ * Anmeldedaten kommen aus dem Windows-Anmeldespeicher des Dienstbenutzers —
+ * die Erstanmeldung muss einmal interaktiv als dieser Benutzer erfolgt sein.
+ */
+export class SvnQuelle implements Bezugsquelle {
+  async gleicheAb(auftrag: Abgleichauftrag): Promise<QuellenAbgleich> {
+    const dateiname = decodeURIComponent(auftrag.wert.split('/').pop() ?? 'datei');
+    let revision = '';
+    let veroeffentlichtAm = '';
+    try {
+      const svnInfo = (element: string) =>
+        execFileAsync('svn', ['info', '--show-item', element, '--non-interactive', auftrag.wert], { timeout: 60000, windowsHide: true });
+      revision = (await svnInfo('last-changed-revision')).stdout.trim();
+      veroeffentlichtAm = alsIsoDatum((await svnInfo('last-changed-date')).stdout.trim());
+    } catch (fehler) {
+      return abgleichMisserfolg(auftrag.aktuelleVersion, svnFehlertext(fehler));
+    }
+
+    if (revision && revision === auftrag.aktuelleVersion && dateiVorhanden(auftrag.zielordner, dateiname)) {
+      return { erfolg: true, geaendert: false, version: revision, dateiname, meldung: 'Bereits aktuell.', veroeffentlichtAm };
+    }
+
+    const ablage = mitTemporaeremOrdner(auftrag.zielordner);
+    try {
+      await execFileAsync('svn', ['export', '--force', '--non-interactive', auftrag.wert, path.join(ablage.ordner, dateiname)], {
+        timeout: 300000,
+        windowsHide: true,
+      });
+      ablage.uebernehmen();
+      return { erfolg: true, geaendert: true, version: revision || new Date().toISOString(), dateiname, meldung: `SVN-Revision ${revision} exportiert.`, veroeffentlichtAm };
+    } catch (fehler) {
+      ablage.verwerfen();
+      return abgleichMisserfolg(auftrag.aktuelleVersion, svnFehlertext(fehler));
+    }
+  }
+}
+
+/** Macht fehlende Anmeldedaten als häufigste Ursache kenntlich. */
+function svnFehlertext(fehler: unknown): string {
+  const text = fehler instanceof Error ? fehler.message : String(fehler);
+  if (/authoriz|authentic|E170001|credentials/i.test(text)) {
+    return `SVN-Anmeldung fehlt oder ist ungültig. Einmalig als Dienstbenutzer anmelden (die Anmeldung wird dann im Windows-Speicher hinterlegt). Ursprüngliche Meldung: ${text}`;
+  }
+  return `SVN fehlgeschlagen: ${text}`;
+}

--
Gitblit v1.9.3