Version 2.3.0: Speichern mit Ordner und Namen
This commit is contained in:
1 parent
8c2cd10ee4
commit
c0a6dd057c
11 files changed
+224
-37
No files matched your search
@@ -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 = [
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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("/<token>/ordner")
|
||||
|
||||
@@ -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``.
|
||||
|
||||
|
||||
@@ -209,16 +209,40 @@
|
||||
iPad in Originalgrösse (also einen Ausschnitt) und ein PDF auf iPadOS
|
||||
nur als Standbild -- Formularfelder liessen sich nicht anfassen. #}
|
||||
<div class="modal__body modal__body--flush" data-viewer
|
||||
data-viewer-pdfjs="{{ asset_url('vendor/pdfjs/') }}">
|
||||
data-viewer-pdfjs="{{ asset_url('vendor/pdfjs/pdf.min.js') }}">
|
||||
<div class="viewer">
|
||||
<div class="viewer__stage" data-viewer-bildbuehne hidden>
|
||||
<img class="viewer__img" alt="" data-viewer-bild>
|
||||
</div>
|
||||
<div class="viewer__pdf" data-viewer-pdf hidden>
|
||||
<div class="pdfViewer" data-viewer-pdfseiten></div>
|
||||
{# Zwei Ebenen: aussen ein normaler Kasten, der sich die Hoehe mit
|
||||
Leiste und Speicherzeile teilt, innen der absolute Scrollbereich,
|
||||
den pdf.js verlangt. #}
|
||||
<div class="viewer__pdfscroll" data-viewer-pdfscroll>
|
||||
<div class="pdfViewer" data-viewer-pdfseiten></div>
|
||||
</div>
|
||||
</div>
|
||||
<iframe class="file-preview" title="Vorschau" data-viewer-rahmen
|
||||
src="about:blank" referrerpolicy="same-origin" hidden></iframe>
|
||||
{# Gefragt wird erst, wenn gespeichert werden soll -- vorher waere es
|
||||
eine Zeile Formular ueber jeder Bildvorschau. #}
|
||||
<div class="viewer__save" data-viewer-speichernzeile hidden>
|
||||
<label class="viewer__feld">
|
||||
<span class="field__label">Ordner</span>
|
||||
<input class="input" list="freigabe-ordner" data-viewer-ordner
|
||||
placeholder="Wurzelverzeichnis" autocomplete="off">
|
||||
</label>
|
||||
<label class="viewer__feld">
|
||||
<span class="field__label">Dateiname</span>
|
||||
<input class="input" data-viewer-name autocomplete="off">
|
||||
</label>
|
||||
<label class="check">
|
||||
<input type="checkbox" data-viewer-ersetzen>
|
||||
<span>Vorhandene ersetzen</span>
|
||||
</label>
|
||||
<button class="btn btn--sm btn--primary" type="button" data-viewer-jetzt>Speichern</button>
|
||||
<button class="btn btn--sm btn--ghost" type="button" data-viewer-abbrechen>Abbrechen</button>
|
||||
</div>
|
||||
<div class="viewer__bar" data-viewer-leiste hidden>
|
||||
<button class="btn btn--sm btn--ghost" type="button" data-viewer-zoom="aus"
|
||||
aria-label="Verkleinern">{{ icon('minus', size=15) }}</button>
|
||||
@@ -242,6 +266,10 @@
|
||||
<button class="btn btn--primary" type="button" data-dialog-close>Schliessen</button>
|
||||
</div>
|
||||
{% endcall %}
|
||||
{# Die Ordner der Freigabe als Vorschlag; getippt werden darf trotzdem. #}
|
||||
<datalist id="freigabe-ordner">
|
||||
{% for eintrag in verzeichnisse %}<option value="{{ eintrag }}"></option>{% endfor %}
|
||||
</datalist>
|
||||
{% endif %}
|
||||
|
||||
{% if listing and current_user.has('fileshare.edit') %}
|
||||
|
||||
Reference in new issue
Block a user