83 lines
3.4 KiB
Python
83 lines
3.4 KiB
Python
"""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
|