From c0a6dd057c6e65da29b59782e7928e2776f3d72d Mon Sep 17 00:00:00 2001 From: Tim Eertmoed Date: Wed, 9 Sep 2026 22:41:07 +0200 Subject: [PATCH] Version 2.3.0: Speichern mit Ordner und Namen --- apps/tesm/pyproject.toml | 2 +- apps/tesm/src/tesm/__init__.py | 31 +++++-- apps/tesm/src/tesm/blueprints/fileshare.py | 29 +++++- apps/tesm/src/tesm/services/fileshare.py | 30 ++++++ .../src/tesm/templates/tesm/fileshare.html | 32 ++++++- packages/tesm-core/pyproject.toml | 2 +- .../tesm-core/src/tesm_core/app_factory.py | 2 +- .../static/tesm_core/css/components.css | 32 ++++++- .../src/tesm_core/static/tesm_core/js/app.js | 6 +- .../tesm_core/static/tesm_core/js/viewer.js | 93 +++++++++++++++---- packages/tesm-licensing/pyproject.toml | 2 +- 11 files changed, 224 insertions(+), 37 deletions(-) diff --git a/apps/tesm/pyproject.toml b/apps/tesm/pyproject.toml index f337fdf..004d23c 100644 --- a/apps/tesm/pyproject.toml +++ b/apps/tesm/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm" -version = "2.2.1" +version = "2.3.0" description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen" requires-python = ">=3.11" dependencies = [ diff --git a/apps/tesm/src/tesm/__init__.py b/apps/tesm/src/tesm/__init__.py index d5e6bf1..089072c 100644 --- a/apps/tesm/src/tesm/__init__.py +++ b/apps/tesm/src/tesm/__init__.py @@ -353,19 +353,32 @@ def _start_fileshare_sweep(app: Flask) -> None: weiter eingebunden. """ import threading + import time from .services import fileshare + def einmal() -> None: + with app.app_context(): + geloest = fileshare.sweep_expired(core(app).database) + if geloest: + logger.info("%s abgelaufene Freigabe-Einbindungen geloest", geloest) + def schleife() -> None: - while True: - time.sleep(FILESHARE_SWEEP_SECONDS) - try: - with app.app_context(): - geloest = fileshare.sweep_expired(core(app).database) - if geloest: - logger.info("%s abgelaufene Freigabe-Einbindungen geloest", geloest) - except Exception: # noqa: BLE001 - der Webprozess darf nie sterben - logger.exception("Aufraeumen der Dateifreigaben fehlgeschlagen") + # Zwei Netze: das innere haelt den Faden am Leben, wenn ein einzelner + # Durchlauf scheitert; das aeussere sorgt dafuer, dass sein Ende + # ueberhaupt auffaellt. Vorher fehlte der Import von "time" -- der + # NameError kam beim ersten sleep, also vor jedem try, und der Faden + # war weg, ohne eine Zeile zu hinterlassen. + try: + while True: + time.sleep(FILESHARE_SWEEP_SECONDS) + try: + einmal() + except Exception: # noqa: BLE001 - der Webprozess darf nie sterben + logger.exception("Aufraeumen der Dateifreigaben fehlgeschlagen") + except BaseException: # noqa: BLE001 - auch ein Abbruch gehoert protokolliert + logger.exception("Der Aufraeumfaden der Dateifreigaben ist beendet") + raise threading.Thread(target=schleife, name="fileshare-sweep", daemon=True).start() diff --git a/apps/tesm/src/tesm/blueprints/fileshare.py b/apps/tesm/src/tesm/blueprints/fileshare.py index 4d86727..d8adbe0 100644 --- a/apps/tesm/src/tesm/blueprints/fileshare.py +++ b/apps/tesm/src/tesm/blueprints/fileshare.py @@ -80,6 +80,8 @@ def browse(): # vorher versprach diese Seite 512 MB, waehrend bei 64 MB abgebrochen # wurde. max_upload_mb=uploads.limit_mb(conn), + # Vorschlagsliste fuer "Ausgefuellt speichern": wohin darf es gehen? + verzeichnisse=(service.all_directories(mount) if listing else []), ) @@ -205,15 +207,36 @@ def save_form(token: str): # waere ein Umweg, um beliebige Dateien in die Freigabe zu bekommen. return {"error": "Das ist keine PDF-Datei."}, 415 + # Wohin und unter welchem Namen -- beides darf der Benutzer bestimmen. + ordner = (request.form.get("ordner") or "").strip().strip("/") + wunschname = (request.form.get("name") or "").strip() + ersetzen = request.form.get("ersetzen") == "1" + try: original = service.resolve_path(mount, relative) - ziel = service.free_name(original.parent, original.stem, "-ausgefuellt", ".pdf") + zielordner = service.resolve_path(mount, ordner) if ordner else original.parent + if not zielordner.is_dir(): + return {"error": f"Den Ordner '{ordner}' gibt es in dieser Freigabe nicht."}, 400 + if wunschname: + name = service.safe_filename(wunschname) + if not name.lower().endswith(".pdf"): + name += ".pdf" + ziel = zielordner / name + if ziel.exists() and not ersetzen: + return { + "error": f"'{name}' gibt es dort schon. Anderen Namen waehlen oder " + "'Vorhandene ersetzen' ankreuzen." + }, 409 + else: + # Ohne Angabe wie bisher: neben das Original, ohne etwas zu ueberschreiben. + ziel = service.free_name(original.parent, original.stem, "-ausgefuellt", ".pdf") ziel.write_bytes(daten) except service.FileshareError as exc: return {"error": str(exc)}, 400 - _audit("fileshare.form_saved", f"{mount.label}:{relative}", ziel.name, severity="notice") - return {"message": f"Als {ziel.name} gespeichert."} + wohin = ziel.relative_to(mount.path.resolve()).as_posix() + _audit("fileshare.form_saved", f"{mount.label}:{relative}", wohin, severity="notice") + return {"message": f"Gespeichert als {wohin}.", "pfad": wohin} @bp.post("//ordner") diff --git a/apps/tesm/src/tesm/services/fileshare.py b/apps/tesm/src/tesm/services/fileshare.py index ff0ca43..0f241eb 100644 --- a/apps/tesm/src/tesm/services/fileshare.py +++ b/apps/tesm/src/tesm/services/fileshare.py @@ -588,6 +588,36 @@ def safe_filename(name: str) -> str: return cleaned +def all_directories(mount: Mount, *, tiefe: int = 4, grenze: int = 400) -> list[str]: + """Die Ordner der Freigabe, relativ und alphabetisch -- fuer eine Auswahlliste. + + Begrenzt in Tiefe und Anzahl: eine Netzwerkfreigabe kann zehntausende + Verzeichnisse haben, und eine Liste, die den Browser lahmlegt, hilft + niemandem. Wer tiefer speichern will, tippt den Pfad -- das Feld nimmt + beides. + """ + wurzel = mount.path.resolve() + gefunden: list[str] = [] + + def hinein(verzeichnis: Path, ebene: int) -> None: + if ebene > tiefe or len(gefunden) >= grenze: + return + try: + eintraege = sorted(verzeichnis.iterdir(), key=lambda e: e.name.lower()) + except OSError: + return + for eintrag in eintraege: + if len(gefunden) >= grenze: + return + if not eintrag.is_dir() or eintrag.name.startswith("."): + continue + gefunden.append(eintrag.relative_to(wurzel).as_posix()) + hinein(eintrag, ebene + 1) + + hinein(wurzel, 1) + return gefunden + + def free_name(parent: Path, stem: str, zusatz: str, suffix: str) -> Path: """Ein freier Name neben dem Original: ``vorlage-ausgefuellt.pdf``. diff --git a/apps/tesm/src/tesm/templates/tesm/fileshare.html b/apps/tesm/src/tesm/templates/tesm/fileshare.html index da851e1..1f3147b 100644 --- a/apps/tesm/src/tesm/templates/tesm/fileshare.html +++ b/apps/tesm/src/tesm/templates/tesm/fileshare.html @@ -209,16 +209,40 @@ iPad in Originalgrösse (also einen Ausschnitt) und ein PDF auf iPadOS nur als Standbild -- Formularfelder liessen sich nicht anfassen. #}