"""Bruecke zwischen Oberflaeche (pywebview) und Backend.
|
|
Jede oeffentliche Methode ist aus dem Frontend als
|
`window.pywebview.api.<methode>(...)` erreichbar und liefert JSON-taugliche Werte.
|
Fehler werden nicht geworfen, sondern als `{ok: false, meldung: ...}` gemeldet —
|
ein Ausnahmefehler ueber die Bruecke kaeme in der Oberflaeche nur als
|
unverstaendlicher Text an.
|
"""
|
|
from __future__ import annotations
|
|
import subprocess
|
import tempfile
|
from dataclasses import asdict
|
from pathlib import Path
|
from typing import Any
|
|
from . import bibliothek, konfiguration
|
from .werkzeuge import Werkzeugordner, Zustand
|
|
# Installationsprogramme kommen nicht zwischen die eigenstaendigen Werkzeuge —
|
# sie werden einmal ausgefuehrt und sind danach entbehrlich.
|
UNTERORDNER_INSTALLER = "Installationsprogramme"
|
|
# Vorerst melden nur unsere eigenen Werkzeuge Updates. Fremdprogramme kuemmern
|
# sich teils selbst darum, und wo nicht, fehlt uns ein verlaesslicher Weg, ihre
|
# installierte Fassung zu erkennen. Bewusst eng gehalten, leicht zu weiten.
|
EINSTUFUNG_MIT_UPDATEPRUEFUNG = "intern"
|
|
|
class Api:
|
def __init__(self) -> None:
|
self._einstellungen = konfiguration.laden()
|
self._ordner = Werkzeugordner(self._einstellungen.werkzeugOrdner)
|
self._ordner.altlasten_aufraeumen()
|
|
# --- Einstellungen ---
|
|
def einstellungen(self) -> dict[str, Any]:
|
return {
|
"einstellungen": asdict(self._einstellungen),
|
"vorgaben": {
|
"serverAdresse": konfiguration.STANDARD_SERVER,
|
"werkzeugOrdner": konfiguration.STANDARD_WERKZEUG_ORDNER,
|
},
|
}
|
|
def einstellungen_speichern(self, serverAdresse: str, werkzeugOrdner: str) -> dict[str, Any]:
|
self._einstellungen = konfiguration.speichern(
|
konfiguration.Einstellungen(serverAdresse=serverAdresse, werkzeugOrdner=werkzeugOrdner)
|
)
|
self._ordner = Werkzeugordner(self._einstellungen.werkzeugOrdner)
|
return {"ok": True, **self.einstellungen()}
|
|
# --- Katalog ---
|
|
def katalog(self) -> dict[str, Any]:
|
"""Katalog der Bibliothek, angereichert um den Befund auf diesem Rechner."""
|
try:
|
eintraege = bibliothek.katalog(self._einstellungen.serverAdresse)
|
except bibliothek.BibliothekNichtErreichbar as fehler:
|
return {"ok": False, "meldung": str(fehler), "eintraege": []}
|
|
angereichert = []
|
for eintrag in eintraege:
|
eigenstaendig = eintrag.get("programmart") == "eigenstaendig"
|
befund = (
|
self._ordner.befund(int(eintrag["id"]), str(eintrag.get("version") or ""))
|
if eigenstaendig
|
else None
|
)
|
angereichert.append(
|
{
|
**eintrag,
|
"lokal": asdict(befund) if befund else None,
|
"ueberwacht": eigenstaendig
|
and eintrag.get("einstufung") == EINSTUFUNG_MIT_UPDATEPRUEFUNG,
|
}
|
)
|
return {"ok": True, "eintraege": angereichert, "ordner": str(self._ordner.ordner)}
|
|
def veraltete(self) -> list[dict[str, str]]:
|
"""Kurzfassung fuer die Meldung beim Start: was ist ueberwacht und nicht aktuell."""
|
stand = self.katalog()
|
if not stand["ok"]:
|
return []
|
return [
|
{"name": eintrag["name"], "zustand": eintrag["lokal"]["zustand"]}
|
for eintrag in stand["eintraege"]
|
if eintrag["ueberwacht"]
|
and eintrag["lokal"]
|
and eintrag["lokal"]["zustand"] in (Zustand.VERALTET.value, Zustand.FREMD.value)
|
]
|
|
# --- Holen ---
|
|
def herunterladen(self, software_id: int, dateiname: str, version: str, programmart: str) -> dict[str, Any]:
|
"""Holt eine Datei und legt sie am passenden Ort ab.
|
|
Eigenstaendige Programme kommen in den Werkzeug-Ordner und werden im Stand
|
vermerkt; Installationsprogramme daneben in einen eigenen Unterordner, ohne
|
Vermerk — was installiert ist, sagt der Ordner ohnehin nicht.
|
"""
|
if not dateiname:
|
return {"ok": False, "meldung": "Für diesen Eintrag liegt keine Datei bereit."}
|
|
eigenstaendig = programmart == "eigenstaendig"
|
with tempfile.TemporaryDirectory(prefix="bibliothek-") as zwischenlager:
|
zwischenziel = Path(zwischenlager) / dateiname
|
try:
|
bibliothek.datei_holen(self._einstellungen.serverAdresse, int(software_id), zwischenziel)
|
except bibliothek.BibliothekNichtErreichbar as fehler:
|
return {"ok": False, "meldung": str(fehler)}
|
|
try:
|
if eigenstaendig:
|
abgelegt = self._ordner.ablegen(int(software_id), zwischenziel, dateiname, version)
|
else:
|
ziel = self._ordner.ordner / UNTERORDNER_INSTALLER / dateiname
|
ziel.parent.mkdir(parents=True, exist_ok=True)
|
ziel.unlink(missing_ok=True)
|
zwischenziel.replace(ziel)
|
abgelegt = ziel
|
except OSError as fehler:
|
return {"ok": False, "meldung": f"Ablegen fehlgeschlagen: {fehler}"}
|
|
return {
|
"ok": True,
|
"pfad": str(abgelegt),
|
"meldung": (
|
f"Abgelegt unter {abgelegt}. Lief das Programm gerade, wirkt die neue "
|
"Fassung nach dem nächsten Start."
|
if eigenstaendig
|
else f"Abgelegt unter {abgelegt}. Nach der Installation kann die Datei gelöscht werden."
|
),
|
}
|
|
# --- Windows-Handreichungen ---
|
|
def ordner_oeffnen(self, unterordner: str = "") -> dict[str, Any]:
|
ziel = self._ordner.ordner / unterordner if unterordner else self._ordner.ordner
|
ziel.mkdir(parents=True, exist_ok=True)
|
subprocess.Popen(["explorer", str(ziel)]) # noqa: S603,S607 - fester Befehl
|
return {"ok": True}
|
|
def verknuepfung_anlegen(self, pfad: str, name: str) -> dict[str, Any]:
|
"""Legt eine Verknuepfung auf dem Desktop an — das kann kein Browser."""
|
ziel = Path(pfad)
|
if not ziel.exists():
|
return {"ok": False, "meldung": "Die Datei liegt nicht (mehr) an diesem Ort."}
|
skript = (
|
"$verknuepfung = (New-Object -ComObject WScript.Shell)."
|
"CreateShortcut([Environment]::GetFolderPath('Desktop') + '\\' + $env:VERKNUEPFUNG_NAME + '.lnk');"
|
"$verknuepfung.TargetPath = $env:VERKNUEPFUNG_ZIEL;"
|
"$verknuepfung.WorkingDirectory = Split-Path $env:VERKNUEPFUNG_ZIEL;"
|
"$verknuepfung.Save()"
|
)
|
try:
|
subprocess.run( # noqa: S603
|
["powershell", "-NoProfile", "-NonInteractive", "-Command", skript],
|
check=True,
|
capture_output=True,
|
timeout=30,
|
env={**_umgebung(), "VERKNUEPFUNG_ZIEL": str(ziel), "VERKNUEPFUNG_NAME": _dateiname_saeubern(name)},
|
)
|
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as fehler:
|
return {"ok": False, "meldung": f"Verknüpfung fehlgeschlagen: {fehler}"}
|
return {"ok": True, "meldung": f"Verknüpfung „{name}“ liegt auf dem Desktop."}
|
|
|
def _umgebung() -> dict[str, str]:
|
import os
|
|
return dict(os.environ)
|
|
|
def _dateiname_saeubern(name: str) -> str:
|
verboten = '\\/:*?"<>|'
|
gesaeubert = "".join(zeichen for zeichen in name if zeichen not in verboten).strip()
|
return gesaeubert or "Programm"
|