From 2c3ca4acc1f87e7b9ca3af863b79da8f00cf6442 Mon Sep 17 00:00:00 2001 From: Tim Eertmoed Date: Wed, 9 Sep 2026 21:48:05 +0200 Subject: [PATCH] Version 2.2.0: Vorschau mit Zoom und PDF-Formularen, eine Uploadgrenze --- apps/tesm/pyproject.toml | 2 +- apps/tesm/src/tesm/blueprints/fileshare.py | 50 +- apps/tesm/src/tesm/services/fileshare.py | 37 + apps/tesm/src/tesm/settings_spec.py | 9 - .../src/tesm/templates/tesm/fileshare.html | 36 +- deploy/install.sh | 5 +- packages/tesm-core/pyproject.toml | 2 +- .../tesm-core/src/tesm_core/app_factory.py | 20 +- .../static/tesm_core/css/components.css | 73 +- .../src/tesm_core/static/tesm_core/js/app.js | 10 + .../tesm_core/static/tesm_core/js/viewer.js | 388 + .../tesm_core/vendor/pdfjs/HERKUNFT.txt | 6 + .../static/tesm_core/vendor/pdfjs/LICENSE | 177 + .../static/tesm_core/vendor/pdfjs/VERSION | 1 + .../vendor/pdfjs/images/altText_add.svg | 3 + .../pdfjs/images/altText_disclaimer.svg | 3 + .../vendor/pdfjs/images/altText_done.svg | 3 + .../vendor/pdfjs/images/altText_spinner.svg | 30 + .../vendor/pdfjs/images/altText_warning.svg | 3 + .../vendor/pdfjs/images/annotation-check.svg | 11 + .../pdfjs/images/annotation-comment.svg | 16 + .../vendor/pdfjs/images/annotation-help.svg | 26 + .../vendor/pdfjs/images/annotation-insert.svg | 10 + .../vendor/pdfjs/images/annotation-key.svg | 11 + .../pdfjs/images/annotation-newparagraph.svg | 11 + .../vendor/pdfjs/images/annotation-noicon.svg | 7 + .../vendor/pdfjs/images/annotation-note.svg | 42 + .../pdfjs/images/annotation-paperclip.svg | 6 + .../pdfjs/images/annotation-paragraph.svg | 16 + .../pdfjs/images/annotation-pushpin.svg | 7 + .../images/cursor-editorFreeHighlight.svg | 6 + .../pdfjs/images/cursor-editorFreeText.svg | 3 + .../vendor/pdfjs/images/cursor-editorInk.svg | 4 + .../images/cursor-editorTextHighlight.svg | 8 + .../pdfjs/images/editor-toolbar-delete.svg | 5 + .../vendor/pdfjs/images/loading-icon.gif | Bin 0 -> 2545 bytes .../pdfjs/images/messageBar_closingButton.svg | 3 + .../pdfjs/images/messageBar_warning.svg | 3 + ...ondaryToolbarButton-documentProperties.svg | 3 + .../images/toolbarButton-editorHighlight.svg | 6 + .../pdfjs/images/toolbarButton-menuArrow.svg | 3 + .../static/tesm_core/vendor/pdfjs/pdf.min.mjs | 21 + .../tesm_core/vendor/pdfjs/pdf.worker.min.mjs | 21 + .../tesm_core/vendor/pdfjs/pdf_viewer.css | 3274 +++++++ .../tesm_core/vendor/pdfjs/pdf_viewer.mjs | 8435 +++++++++++++++++ .../pdfjs/standard_fonts/FoxitDingbats.pfb | Bin 0 -> 29513 bytes .../pdfjs/standard_fonts/FoxitFixed.pfb | Bin 0 -> 17597 bytes .../pdfjs/standard_fonts/FoxitFixedBold.pfb | Bin 0 -> 18055 bytes .../standard_fonts/FoxitFixedBoldItalic.pfb | Bin 0 -> 19151 bytes .../pdfjs/standard_fonts/FoxitFixedItalic.pfb | Bin 0 -> 18746 bytes .../pdfjs/standard_fonts/FoxitSerif.pfb | Bin 0 -> 19469 bytes .../pdfjs/standard_fonts/FoxitSerifBold.pfb | Bin 0 -> 19395 bytes .../standard_fonts/FoxitSerifBoldItalic.pfb | Bin 0 -> 20733 bytes .../pdfjs/standard_fonts/FoxitSerifItalic.pfb | Bin 0 -> 21227 bytes .../pdfjs/standard_fonts/FoxitSymbol.pfb | Bin 0 -> 16729 bytes .../vendor/pdfjs/standard_fonts/LICENSE_FOXIT | 27 + .../pdfjs/standard_fonts/LICENSE_LIBERATION | 102 + .../standard_fonts/LiberationSans-Bold.ttf | Bin 0 -> 137052 bytes .../LiberationSans-BoldItalic.ttf | Bin 0 -> 135124 bytes .../standard_fonts/LiberationSans-Italic.ttf | Bin 0 -> 162036 bytes .../standard_fonts/LiberationSans-Regular.ttf | Bin 0 -> 139512 bytes .../src/tesm_core/system/webserver.py | 11 +- .../templates/tesm_core/settings_web.html | 9 + packages/tesm-core/src/tesm_core/ui.py | 3 + packages/tesm-core/src/tesm_core/uploads.py | 80 + .../src/tesm_core/views/hostsettings.py | 27 +- packages/tesm-licensing/pyproject.toml | 2 +- 67 files changed, 13051 insertions(+), 26 deletions(-) create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/js/viewer.js create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/HERKUNFT.txt create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/LICENSE create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/VERSION create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/altText_add.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/altText_disclaimer.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/altText_done.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/altText_spinner.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/altText_warning.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-check.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-comment.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-help.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-insert.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-key.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-newparagraph.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-noicon.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-note.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-paperclip.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-paragraph.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/annotation-pushpin.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/cursor-editorFreeHighlight.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/cursor-editorFreeText.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/cursor-editorInk.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/cursor-editorTextHighlight.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/editor-toolbar-delete.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/loading-icon.gif create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/messageBar_closingButton.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/messageBar_warning.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/secondaryToolbarButton-documentProperties.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/toolbarButton-editorHighlight.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/images/toolbarButton-menuArrow.svg create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/pdf.min.mjs create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/pdf.worker.min.mjs create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/pdf_viewer.css create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/pdf_viewer.mjs create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitDingbats.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitFixed.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitFixedBold.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitFixedBoldItalic.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitFixedItalic.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitSerif.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitSerifBold.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitSerifBoldItalic.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitSerifItalic.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/FoxitSymbol.pfb create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/LICENSE_FOXIT create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/LICENSE_LIBERATION create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/LiberationSans-Bold.ttf create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/LiberationSans-BoldItalic.ttf create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/LiberationSans-Italic.ttf create mode 100644 packages/tesm-core/src/tesm_core/static/tesm_core/vendor/pdfjs/standard_fonts/LiberationSans-Regular.ttf create mode 100644 packages/tesm-core/src/tesm_core/uploads.py diff --git a/apps/tesm/pyproject.toml b/apps/tesm/pyproject.toml index 82a9bfc..997ac34 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.1.2" +version = "2.2.0" description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen" requires-python = ">=3.11" dependencies = [ diff --git a/apps/tesm/src/tesm/blueprints/fileshare.py b/apps/tesm/src/tesm/blueprints/fileshare.py index 076da6e..4d86727 100644 --- a/apps/tesm/src/tesm/blueprints/fileshare.py +++ b/apps/tesm/src/tesm/blueprints/fileshare.py @@ -18,6 +18,7 @@ from flask import ( from tesm_core import audit from tesm_core.auth.models import current_user, permission_required from tesm_core.auth.service import client_ip +from tesm_core import uploads from tesm_core.extension import core, db, transaction from ..services import fileshare as service @@ -75,7 +76,10 @@ def browse(): token=token, listing=listing, error=error, - max_upload_mb=int(core().settings.get(conn, "fileshare_upload_max_mb") or 512), + # Dieselbe Zahl, die auch die Anfrage begrenzt und in nginx steht -- + # vorher versprach diese Seite 512 MB, waehrend bei 64 MB abgebrochen + # wurde. + max_upload_mb=uploads.limit_mb(conn), ) @@ -95,7 +99,12 @@ def download(token: str): path, as_attachment=True, download_name=path.name, - mimetype=service.download_mime(path.name), + # Bewusst der neutrale Typ und nicht der erkannte: iOS Safari + # entscheidet nach dem MIME-Typ und zeigt eine "application/pdf" im + # Browser an, auch wenn sie als Anhang ausgeliefert wird. Wer hier + # klickt, will die Datei aber ablegen -- angesehen wird sie in der + # Vorschau. Fuer alle anderen Browser aendert sich damit nichts. + mimetype="application/octet-stream", ) @@ -148,7 +157,7 @@ def upload(token: str): flash("Keine Datei ausgewaehlt.", "danger") return redirect(url_for("fileshare.browse", freigabe=token, pfad=relative)) - limit = int(core().settings.get(db(), "fileshare_upload_max_mb") or 512) * 1024 * 1024 + limit = uploads.limit_bytes(db()) stored = 0 try: target_dir = service.resolve_path(mount, relative) @@ -172,6 +181,41 @@ def upload(token: str): return redirect(url_for("fileshare.browse", freigabe=token, pfad=relative)) +@bp.post("//formular") +@permission_required("fileshare.create") +def save_form(token: str): + """Nimmt ein in der Vorschau ausgefuelltes PDF und legt es daneben ab. + + Als **neue** Datei: auf einer Freigabe liegt in der Regel die leere + Vorlage, und die soll leer bleiben. Wer das Original ersetzen will, kann + die alte Datei danach loeschen -- das ist eine bewusste Handlung. + """ + mount = _mount_or_404(token) + relative = request.form.get("pfad") or "" + upload_file = request.files.get("datei") + if upload_file is None or not upload_file.filename: + return {"error": "Es kam keine Datei an."}, 400 + + grenze = uploads.limit_bytes(db()) + daten = upload_file.stream.read(grenze + 1) + if len(daten) > grenze: + return {"error": f"Zu gross (Grenze {grenze // 1024 // 1024} MB)."}, 413 + if not daten.startswith(b"%PDF-"): + # Diese Route nimmt nur, was pdf.js hier ablegt. Ein anderer Inhalt + # waere ein Umweg, um beliebige Dateien in die Freigabe zu bekommen. + return {"error": "Das ist keine PDF-Datei."}, 415 + + try: + original = service.resolve_path(mount, relative) + 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."} + + @bp.post("//ordner") @permission_required("fileshare.create") def make_directory(token: str): diff --git a/apps/tesm/src/tesm/services/fileshare.py b/apps/tesm/src/tesm/services/fileshare.py index 79b1834..ff0ca43 100644 --- a/apps/tesm/src/tesm/services/fileshare.py +++ b/apps/tesm/src/tesm/services/fileshare.py @@ -57,6 +57,10 @@ PREVIEW_MIME: dict[str, str] = { ".gif": "image/gif", ".webp": "image/webp", ".bmp": "image/bmp", + # Das Format, in dem ein iPad fotografiert. Safari zeigt es an, andere + # Browser nicht -- die Vorschau sagt es dann und bietet den Download an. + ".heic": "image/heic", + ".heif": "image/heif", ".txt": "text/plain; charset=utf-8", ".csv": "text/plain; charset=utf-8", ".log": "text/plain; charset=utf-8", @@ -68,6 +72,21 @@ PREVIEW_MIME: dict[str, str] = { #: im Ursprung der Anwendung ausgefuehrt. Download bleibt moeglich. +def preview_kind(name: str) -> str: + """Welche Anzeige passt: ``bild``, ``pdf`` oder ``text``? + + Die Unterscheidung gehoert hierher und nicht in die Vorlage: die + Zuordnung Endung -> MIME-Typ steht schon hier, und eine zweite Liste in + Jinja liefe frueher oder spaeter auseinander. + """ + mime = PREVIEW_MIME.get(Path(name).suffix.lower(), "") + if mime.startswith("image/"): + return "bild" + if mime == "application/pdf": + return "pdf" + return "text" + + class FileshareError(Exception): """Fehler mit Klartextmeldung fuer die Oberflaeche.""" @@ -528,6 +547,7 @@ def list_directory(mount: Mount, relative: str = "") -> dict[str, Any]: directories.append(record) else: record["previewable"] = entry.suffix.lower() in PREVIEW_MIME + record["preview_kind"] = preview_kind(entry.name) files.append(record) except OSError as exc: raise FileshareError(f"Verzeichnis nicht lesbar ({exc}).") from exc @@ -568,6 +588,23 @@ def safe_filename(name: str) -> str: return cleaned +def free_name(parent: Path, stem: str, zusatz: str, suffix: str) -> Path: + """Ein freier Name neben dem Original: ``vorlage-ausgefuellt.pdf``. + + Nichts ueberschreiben: auf einer Freigabe liegt meist die leere Vorlage, + und zwei Leute fuellen sie nacheinander aus. Ab dem zweiten Mal wird + durchnummeriert. + """ + kandidat = parent / f"{stem}{zusatz}{suffix}" + zaehler = 2 + while kandidat.exists(): + kandidat = parent / f"{stem}{zusatz}-{zaehler}{suffix}" + zaehler += 1 + if zaehler > 200: + raise FileshareError("Zu viele Dateien dieses Namens -- bitte aufraeumen.") + return kandidat + + def delete_entry(mount: Mount, relative: str) -> str: path = resolve_path(mount, relative) if path == mount.path.resolve(): diff --git a/apps/tesm/src/tesm/settings_spec.py b/apps/tesm/src/tesm/settings_spec.py index 28983a3..61b244f 100644 --- a/apps/tesm/src/tesm/settings_spec.py +++ b/apps/tesm/src/tesm/settings_spec.py @@ -79,15 +79,6 @@ TESM_SETTINGS: tuple[SettingSpec, ...] = ( minimum=1, maximum=168, ), - SettingSpec( - "fileshare_upload_max_mb", - 512, - "int", - "Maximale Dateigroesse beim Hochladen (MB)", - section="fileshare", - minimum=1, - maximum=8192, - ), # -- Wartung ------------------------------------------------------------ # SettingSpec( "maintenance_command_timeout", diff --git a/apps/tesm/src/tesm/templates/tesm/fileshare.html b/apps/tesm/src/tesm/templates/tesm/fileshare.html index faf9463..da851e1 100644 --- a/apps/tesm/src/tesm/templates/tesm/fileshare.html +++ b/apps/tesm/src/tesm/templates/tesm/fileshare.html @@ -103,6 +103,8 @@ @@ -201,14 +205,40 @@ Dialog für alle Dateien statt einer je Zeile. #} {% call modal('file-preview', 'Vorschau', '', 'wide') %} -