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
---
desktop/backend/bibliothek.py | 85 ++++++++++++++++++++++++++++++++++++++++++
1 files changed, 85 insertions(+), 0 deletions(-)
diff --git a/desktop/backend/bibliothek.py b/desktop/backend/bibliothek.py
new file mode 100644
index 0000000..6274a70
--- /dev/null
+++ b/desktop/backend/bibliothek.py
@@ -0,0 +1,85 @@
+"""Zugriff auf den Server der Software-Bibliothek.
+
+Nur die beiden Routen, die aus dem Netzwerk erreichbar sind: Katalog lesen und
+Datei holen. Die Verwaltung bleibt der Weboberflaeche am Server vorbehalten.
+
+Bewusst nur mit der Standardbibliothek — jede Abhaengigkeit mehr muss beim Bau
+der exe mitgeschleppt werden, und fuer zwei GET-Aufrufe lohnt das nicht.
+"""
+
+from __future__ import annotations
+
+import json
+import urllib.error
+import urllib.request
+from pathlib import Path
+from typing import Any, Callable
+
+ZEITGRENZE_KATALOG_S = 15
+ZEITGRENZE_DOWNLOAD_S = 600
+
+
+class BibliothekNichtErreichbar(Exception):
+ """Der Server antwortet nicht oder liefert etwas Unerwartetes."""
+
+
+def _lesbarer_fehler(adresse: str, fehler: Exception) -> BibliothekNichtErreichbar:
+ if isinstance(fehler, urllib.error.HTTPError):
+ return BibliothekNichtErreichbar(f"{adresse} antwortet mit HTTP {fehler.code}.")
+ if isinstance(fehler, urllib.error.URLError):
+ return BibliothekNichtErreichbar(f"{adresse} ist nicht erreichbar: {fehler.reason}")
+ return BibliothekNichtErreichbar(f"{adresse}: {fehler}")
+
+
+def katalog(server_adresse: str) -> list[dict[str, Any]]:
+ """Holt alle Katalogeintraege."""
+ adresse = f"{server_adresse.rstrip('/')}/api/software"
+ try:
+ with urllib.request.urlopen(adresse, timeout=ZEITGRENZE_KATALOG_S) as antwort:
+ gelesen = json.loads(antwort.read().decode("utf-8"))
+ except Exception as fehler: # noqa: BLE001 - jede Ursache wird zur selben Meldung
+ raise _lesbarer_fehler(server_adresse, fehler) from fehler
+ if not isinstance(gelesen, list):
+ raise BibliothekNichtErreichbar(f"{server_adresse} liefert keinen Katalog.")
+ return gelesen
+
+
+def datei_holen(
+ server_adresse: str,
+ software_id: int,
+ ziel: Path,
+ fortschritt: Callable[[int, int], None] | None = None,
+) -> Path:
+ """Laedt die bereitgestellte Datei nach `ziel`.
+
+ Geschrieben wird zuerst nach `<ziel>.teil` und erst nach vollstaendigem
+ Empfang umbenannt. Ein Abbruch mitten im Download darf keine halbe Datei
+ hinterlassen, die spaeter fuer ein fertiges Programm gehalten wird.
+ """
+ adresse = f"{server_adresse.rstrip('/')}/api/software/{software_id}/download"
+ unfertig = ziel.with_name(ziel.name + ".teil")
+ ziel.parent.mkdir(parents=True, exist_ok=True)
+ try:
+ with urllib.request.urlopen(adresse, timeout=ZEITGRENZE_DOWNLOAD_S) as antwort:
+ gesamt = int(antwort.headers.get("Content-Length") or 0)
+ geladen = 0
+ with unfertig.open("wb") as datei:
+ while True:
+ brocken = antwort.read(256 * 1024)
+ if not brocken:
+ break
+ datei.write(brocken)
+ geladen += len(brocken)
+ if fortschritt:
+ fortschritt(geladen, gesamt)
+ except Exception as fehler: # noqa: BLE001
+ unfertig.unlink(missing_ok=True)
+ raise _lesbarer_fehler(server_adresse, fehler) from fehler
+
+ if unfertig.stat().st_size == 0:
+ unfertig.unlink(missing_ok=True)
+ raise BibliothekNichtErreichbar("Der Server hat eine leere Datei geliefert.")
+
+ ziel.unlink(missing_ok=True)
+ unfertig.replace(ziel)
+ return ziel
--
Gitblit v1.9.3