"""Updates: neuestes Release auf Gitea finden und das Windows-Setup herunterladen. Nur Netzwerk und Dateien – kein Tk (läuft im Hintergrund-Thread). Das Installieren startet die App (gui/app.py). """ from __future__ import annotations import json import re import urllib.request from pathlib import Path from urllib.parse import urlparse HOST = "git.handy-insel.de" API = f"https://{HOST}/api/v1/repos/kerimatilgan/provisionsmanager95/releases/latest" SETUP = re.compile(r"Provisionsmanager-Setup-[\d.]+\.exe") MAX_BYTES = 200 * 1024 * 1024 class UpdateFehler(Exception): pass def version_tupel(v: str) -> tuple[int, ...]: """'v1.10.0' -> (1, 10, 0); Unlesbares -> ()""" m = re.fullmatch(r"v?(\d+(?:\.\d+)*)", (v or "").strip()) return tuple(int(x) for x in m[1].split(".")) if m else () def ist_neuer(neu: str, alt: str) -> bool: return version_tupel(neu) > version_tupel(alt) and bool(version_tupel(alt)) def _eigene_url(url: str) -> bool: """Nur über HTTPS vom eigenen Gitea laden (die Antwort des Servers ist hier die Vertrauensgrenze).""" u = urlparse(url) return u.scheme == "https" and u.hostname == HOST def neueste(timeout: float = 8) -> dict: """Neuestes Release: {"version", "seite", "setup_url", "groesse"} (setup_url None, wenn kein Setup dabei).""" with urllib.request.urlopen(API, timeout=timeout) as r: daten = json.loads(r.read(1024 * 1024).decode("utf-8")) setup = next((a for a in daten.get("assets", []) if SETUP.fullmatch(a.get("name", ""))), None) url = setup.get("browser_download_url") if setup else None if url and not _eigene_url(url): url = None seite = daten.get("html_url") or "" return {"version": str(daten.get("tag_name", "")).lstrip("v"), "seite": seite if _eigene_url(seite) else f"https://{HOST}/kerimatilgan/provisionsmanager95/releases", "setup_url": url, "groesse": int(setup.get("size") or 0) if setup else 0} def herunterladen(info: dict, ordner: Path, timeout: float = 60) -> Path: """Setup nach `ordner` laden und prüfen (Größe wie angekündigt, Windows-Programm). Gibt den Pfad zurück.""" url = info.get("setup_url") if not url or not _eigene_url(url): raise UpdateFehler("Für diese Version gibt es kein Setup zum Herunterladen.") ordner.mkdir(parents=True, exist_ok=True) ziel = ordner / Path(urlparse(url).path).name if not SETUP.fullmatch(ziel.name): raise UpdateFehler(f"Unerwarteter Dateiname: {ziel.name}") teil = ziel.with_suffix(".teil") n = 0 try: with urllib.request.urlopen(url, timeout=timeout) as r, teil.open("wb") as f: if not _eigene_url(r.geturl()): # Weiterleitung auf einen fremden Server raise UpdateFehler("Download wurde auf einen fremden Server umgeleitet.") while block := r.read(1 << 16): n += len(block) if n > MAX_BYTES: raise UpdateFehler("Download ist unerwartet groß.") f.write(block) except BaseException: teil.unlink(missing_ok=True) raise with teil.open("rb") as f: kopf = f.read(2) if kopf != b"MZ" or (info.get("groesse") and n != info["groesse"]): teil.unlink(missing_ok=True) raise UpdateFehler("Download ist unvollständig oder beschädigt – bitte später erneut versuchen.") teil.replace(ziel) return ziel