25 KiB
Devlog
Arbeitsprotokoll mit Zeiterfassung. Ein Eintrag pro Arbeitsblock, neueste oben. Zeiten in Ortszeit (Europe/Berlin).
Zeitübersicht
| Datum | Von | Bis | Dauer | Schritt | Thema |
|---|---|---|---|---|---|
| 2026-10-10 | 17:21 | 17:27 | 0:06 | 1 | Gerüst, Datenmodell, Migrationen |
| 2026-10-10 | 18:35 | 18:48 | 0:13 | 2 | Mount-Schicht, Validierung, SFTP |
| 2026-10-10 | 18:53 | 19:07 | 0:14 | 3 | Mover-Logik |
| 2026-10-10 | 19:10 | 19:17 | 0:07 | 4 | Scheduler, Protokoll, Benachrichtigung |
| 2026-10-10 | 19:18 | 19:34 | 0:16 | 5 | Weboberfläche und Anmeldung |
| 2026-10-10 | 19:40 | 19:50 | 0:10 | 6 | Deployment, README, Release 0.1.0 |
| 2026-10-10 | 21:36 | 22:05 | 0:29 | – | Praxistest auf VM, Release 0.1.1 |
| 2026-10-10 | 22:45 | 23:00 | 0:15 | – | Echtes NAS/Paperless anbinden |
| 2026-10-10 | 23:31 | 23:58 | 0:27 | – | Schlüsselverwaltung, Update-Skript, 0.2.x |
| 2026-10-11 | 00:13 | 00:32 | 0:19 | – | Menü, Anleitung, Roadmap, Uptime Kuma (0.3.0) |
| 2026-10-11 | 00:31 | 00:50 | 0:19 | – | Pausieren, Wartungsmodus, E-Mail (0.4.0) |
| 2026-10-11 | 00:54 | 01:16 | 0:22 | – | SFTP-Quelle, Zugangsdaten, Anmeldung (0.5.0) |
| 2026-10-11 | 08:43 | 08:55 | 0:12 | – | Anmeldung je Netz (0.6.0) |
| 2026-10-12 | 00:14 | 00:34 | 0:20 | – | Archiv je Regel (0.7.0) |
| 2026-10-12 | 00:46 | 00:55 | 0:09 | – | Anbindungen kopieren (0.8.0) |
| 2026-10-15 | 09:17 | 09:26 | 0:09 | – | Regeln kopieren (0.9.0) |
| Summe | 4:07 |
2026-10-15 · 09:17–09:26 · Regeln kopieren (0.9.0)
Erledigt – Knopf „Kopieren“, Route /rules/{id}/copy (GET Formular, POST
speichern), rule_copy_name. 5 Tests (362 gesamt); Gegenprobe „Kopie aktiv“ schlägt
an (2 Tests).
Entscheidung – Kopie startet deaktiviert: Mit identischer Quelle würden Original
und Kopie sonst sofort parallel dieselben Dateien bearbeiten (doppelte Lieferung bzw.
Kollisionen). Protokoll und Buchführung (copied_files, archived_files) werden
bewusst nicht mitkopiert – die Kopie ist eine neue Regel.
2026-10-12 · 00:46–00:55 · Anbindungen kopieren (0.8.0)
Erledigt – Knopf „Kopieren“, Route /mounts/{id}/copy (GET Formular, POST
speichern), save_mount(copy_from=…), Formular-Test mit Vorlage. 7 Tests (357 gesamt);
Gegenprobe ohne Übernahme der Zugangsdaten: 3 Tests schlagen an.
Entscheidungen
- Eigene Zugangsdaten werden als eigenes Exemplar kopiert (nicht geteilt) – eigene Zugangsdaten verschwinden mit ihrer Anbindung, eine geteilte Nutzung wäre dafür der Fall „Zugangsdaten“.
- Eigenes Schlüsselpaar bleibt gleich (schon in
authorized_keys). - Hostschlüssel nur bei gleichem Server/Port übernehmen, sonst würde der Test an einem anderen Server mit „Hostschlüssel geändert“ scheitern.
2026-10-12 · 00:14–00:34 · Archiv je Regel (0.7.0)
Erledigt
- Archivfelder an der Regel, Tabelle
archived_files(Migration 0004). - Engine: Archiv zuerst, dann Ziel, dann Quelle löschen; Archiv immer „umbenennen“.
- Runner: Ziel- und Archivaufbau über eine gemeinsame Fabrik (
_target). - 14 Tests (350 gesamt); Mutationsprobe 4/4 erkannt (Buchführung, Archiv übersprungen, Kollisionsregel, Platzprüfung).
Entscheidungen
- Archiv vor Ziel: Paperless holt Dateien sofort ab – andersherum würde ein gescheitertes Archiv beim Neuversuch zur doppelten Lieferung führen.
- Archiv überschreibt nie (unabhängig von der Kollisionsregel des Ziels).
- Ablage „nach Datum“ nach Verarbeitungsdatum (nicht Datei-mtime) – eindeutig nachvollziehbar, wann eine Datei durchgelaufen ist.
Stolperstein
- Downgrade von 0004 scheiterte an den CHECK-Regeln der neuen Auswahlfelder (verweisen nach dem Entfernen der Spalten ins Leere) – nur dank der Migrations- tests mit Daten aufgefallen; Downgrade entfernt sie jetzt zuerst.
2026-10-11 · 08:43–08:55 · Anmeldung je Netz (0.6.0)
Anlass – Wunsch: Anmeldung abschalten, und zwar konfigurierbar, aus welchen Netzen mit und ohne Passwort. Zugriffe kamen aus 10.0.10.0/24 und 10.1.5.0/24 (Access-Log).
Erledigt
auth.access_decision()als einzige Entscheidung (ohne PW / mit PW / abgewiesen), benutzt von Middleware (403 für fremde Netze), Guard und Anmeldeseite.- Schutz vor Aussperren beim Speichern;
reset-passwordhebt Sperren auf. - 19 Tests für die Anmeldung (336 gesamt).
Entscheidungen
/healthzund/staticnie sperren – Uptime Kuma kann in einem anderen Netz stehen.- Nicht-IP-Clients (z. B. Unix-Socket) gelten als „in keinem Netz“.
2026-10-11 · 00:54–01:16 · SFTP-Quelle, Zugangsdaten, Anmeldung abschaltbar (0.5.0)
Erledigt
mover/sources.py:SourcemitLocalSource/SftpSource; Engine greift nur noch darüber auf die Quelle zu (Scan, frischer Zustand, Lesen, Löschen, Quarantäne). Ziele bekommen stattPathein „Readable“ (Adapter) – Destinations fast unverändert.credentials.py(vorherkeys.py): gespeicherte Passwörter; Seite „Zugangsdaten“.- Anmeldung: Modus immer / vertraute Netze / nie, zentral im
guard. - 26 neue Tests (325 gesamt); alle bisherigen Mover-Tests bestanden den Umbau unverändert (nur ein Monkeypatch-Ziel verschoben).
Entscheidungen
- SFTP-Quelle: Stabilität über Größe + sekundengenaue mtime, Mindestfenster 2 s;
Verbindungsabbruch →
ENOTCONN→ Lauf abgebrochen (Anbindung gestört). - Gespeicherte Passwörter tragen den Benutzer selbst (anders als Schlüssel, deren Benutzer zur Anbindung gehört).
- „Passworteingabe deaktivieren“ als Login-Modus umgesetzt, mit Bestätigung durch
das Passwort, Warnhinweis und Notausgang über
reset-password. Ein Passwort bleibt Pflicht (Ersteinrichtung), sonst ließe sich die Anmeldung nie wieder einschalten.
Stolpersteine
- paramiko
prefetch()brauchtfstat; der Test-Server konnte es nicht – jetzt kann er es, und die Quelle übergibt die bekannte Größe (spart einen Umlauf). - Zwei TestClients mit eigenem Lebenszyklus starteten den Scheduler doppelt → zweiter Client ohne Lebenszyklus auf derselben App.
2026-10-11 · 00:31–00:50 · Pausieren, Wartungsmodus, E-Mail (0.4.0)
Erledigt
- Wartezustand (
rules.waiting_since/_reason, Migration 0003), Wartungsmodus, E-Mail (mail.py, SMTP), eine Meldung pro Anbindung mit betroffenen Regeln. - 0.4.0/0.5.0 aufgeteilt: kleinere Updates, schneller Nutzen.
- 21 neue Tests (299 gesamt), u. a. SMTP gegen echten Server (aiosmtpd) mit STARTTLS, SSL, falschem Passwort und nicht vertrauenswürdigem Zertifikat.
Entscheidungen
- Warten statt Deaktivieren; kein Fehlerzähler und nur ein Protokolleintrag beim Eintritt; Ende des Wartens automatisch beim nächsten durchkommenden Lauf.
- Benachrichtigung pro Anbindung (nicht pro Regel) – sonst bei drei Regeln am NAS drei gleiche Meldungen. Lücke geschlossen: Aushängen von außen meldete nichts.
- Wartungsmodus unterdrückt nur Regel-Alarme, Anbindungen werden weiter geprüft.
- SMTP prüft Zertifikate immer; für Tests ist nur
mail.tls_context()ersetzbar.
Stolpersteine (alle im Testaufbau)
- Globales Monkeypatchen von
ssl.create_default_contexttraf auch den Testserver → eigene Funktiontls_context(). - aiosmtpd:
AuthResult(success=False)ohnehandled=Falseschickt keine Antwort → Client hängt bis zum Zeitlimit.
Migrationen – 0003 baut rules neu; getestet von 0001 und von 0002 mit allen
verknüpften Tabellen sowie an der befüllten Demo-Datenbank; vor dem Release an einer
Kopie der Live-Datenbank auf der VM.
2026-10-11 · 00:13–00:32 · Menü, Anleitung, Roadmap, Uptime Kuma (0.3.0)
Erledigt
- Seitenleiste mit Gruppen, Symbolen (Inline-SVG), Zählern und Statusampel; Handy-Menü per Knopf (ohne JS sichtbar).
docs/ANLEITUNG.md(für Anwender) mit Sprungmarken; Hilfe-Seite rendert sie per Python-Markdown, das Paket liefertdocs/mit (hatch force-include).ROADMAP.md; Nutzerwünsche eingeplant: SFTP als Quelle, gespeicherte Zugangsdaten, automatisches Pausieren/Fortsetzen, E-Mail (0.4.0).- Uptime Kuma: ausführliches
/healthz, Einzel-Endpunkte, Push-Job. - 13 neue Tests (278 gesamt).
Wie geprüft
- Erstmals echte Screenshots mit Playwright (Chromium, Scratchpad-venv) gegen eine
Demo-Instanz (Fake-Backend), Desktop 1366 px und Handy 390 px, hell und dunkel.
Daraus gefunden und behoben: überflüssiges Passwortfeld/SMB-Hinweise bei SFTP,
sperriges Umbenennen auf der Schlüsselseite, Tabellen in der Hilfe ragten auf
dem Handy über den Rand. Dieselben Screenshots sind jetzt in der Anleitung
(128-Farben-PNG, zusammen ~150 KB; Demo-Pfad per DOM durch
/mnt/netmoverersetzt). - Ein echter Fehler, den erst der Blick auf das HTML zeigte:
{{ 'attr="x"' if … }}wird von Jinja maskiert →aria-currentunddisabled titlewirkten nicht.
Entscheidungen
- Automatisches Pausieren: kein Deaktivieren (müsste nach jedem NAS-Neustart von Hand rückgängig gemacht werden), sondern „wartet auf Anbindung“ mit automatischem Fortsetzen – für 0.4.0 vorgeschlagen.
- Regel mit 3 Fehlschlägen in Folge zählt jetzt als Problem in
/healthz.
2026-10-10 · 23:31–23:58 · Schlüsselverwaltung, Update-Skript, Releases 0.2.0/0.2.1
Erledigt
- Schlüsselverwaltung (
keys.py, Seite „Schlüssel“, Auswahl in SFTP-Anbindungen), Migration 0002 (credentials.managed,mounts.username). deploy/update.sh→netmover-update: Tags holen, Sicherung (SQLite-Online- Backup + Schlüssel + env), Checkout,install.sh, Healthcheck, Rückweg-Anleitung.- VM auf Git-Klon
/opt/netmover/srcumgestellt (Repo ist öffentlich lesbar).
Zwischenfall beim Update der VM auf 0.2.0
- Migration 0002 scheiterte:
FOREIGN KEY constraint failedbeiDROP TABLE credentials(Alembic-Batch baut die Tabelle neu;foreign_keys=ON, und die Anbindungen verweisen darauf). Danach crashte jeder Neustart an der übriggebliebenen_alembic_tmp_credentials(DDL lief ohne Transaktion). netmover-updatehat den Fehler erkannt und die Rückweg-Befehle genannt. Sicherung zurückgespielt, mitnetmover-update v0.1.1zurückgestuft – in ~1 Minute wieder lauffähig, NAS-Mount blieb durchgehend eingehängt.- Ursache, warum die Tests es nicht fanden: Migrationen wurden nur auf leeren
Datenbanken getestet. Neu:
tests/test_migrations.pymit verknüpften Daten und mit Abbruch mitten in der Migration (Datenbank muss unverändert bleiben). - Fix: eigene Migrationsverbindung (FK aus,
BEGIN IMMEDIATE,transactional_ddl,foreign_key_checkvor dem Commit). Vor dem Release an einer Kopie der echten Datenbank auf der VM geprüft. - Tag v0.2.0 bleibt (veröffentlicht), im Changelog als „nicht verwenden“ markiert.
Lehre
- Jede künftige Migration bekommt einen Test auf einer befüllten Datenbank.
2026-10-10 · 21:36–22:05 · Praxistest auf der Debian-13-VM, Release 0.1.1
Umgebung
- KVM-VM
srv-rz-netmover(10.0.10.223), Debian 13, Python 3.13, 2 GB RAM. - Installation per
deploy/install.shaus einemgit archive(VM ohne Gitea-Zugang). - Vorübergehende Testserver auf der VM: Samba (nur
lo), NFS-Exporte für127.0.0.1(mit/ohne Root-Squash), SFTP-Benutzerpaperlessmit Schlüsselrestrict,command="/usr/lib/openssh/sftp-server". - Auf Wunsch
NETMOVER_HOST=0.0.0.0, damit die Oberfläche im LAN erreichbar ist.
Bestanden
- Ersteinrichtung mit Code aus dem Log, Cookie-Flags.
- CIFS (Passwort mit Komma/Ausrufezeichen, Credentials-Datei danach weg), NFS 4,
NFS 3, Root-Squash-Erkennung mit Synology-Hinweis, SFTP mit TOFU (Fingerabdruck
=
ssh-keygen -lf), echtes OpenSSH liefertstatvfs. - Probelauf, Kollision
scan001-1.pdf,.txt/.partignoriert, keine.part-Reste, Eigentümerpaperlessim SFTP-Ziel. - Langsam wachsende Datei (20 × 50 KB/s): erst nach Ende verschoben, Prüfsumme gleich.
- Kopiermodus rekursiv mit Eigentümer/Modus; zweiter Lauf „2 bereits übertragen“.
- Samba gestoppt: Lebenszeichen 15 s → gestört → 3 Versuche → Fehler,
/healthz503, nichts angefasst. NFS-Server gestoppt (hängenderhard-Mount): Oberfläche und andere Regel liefen weiter, Rückkehr automatisch. - Mounts überstehen Stopp/Start des Dienstes (gleiche Mount-IDs).
- Neustart der VM: NFS sofort, SMB per Neuversuch nach ~1 min, Regel läuft weiter.
Gefunden und behoben (v0.1.1)
- systemd-Unit mit eigenem Mount-Namespace (ProtectSystem/PrivateTmp/…):
Mounts nur im Dienst sichtbar,
shared,slave. Härtung ohne Namespace. - CIFS-Attribut-Cache:
stat()meldete 3 s lang eine veraltete Größe einer wachsenden Datei; die Byte-Zählung beim Kopieren fing das ab. Jetztopen()+fstat()für beide Messungen. - Nach drei gescheiterten Versuchen endgültig aufgegeben – zu kurz für einen
NAS-Neustart, und beim Booten vor dem NAS kam die Freigabe nie zurück.
Jetzt minütlicher Neuversuch (nur für von NetMover selbst ausgehängte
Anbindungen mit
auto_mount) plus Scheduler-Job. mount-Zeitlimit beendete nurmount, nichtmount.nfs→ später doch eingehängt. Jetzt ganze Prozessgruppe.- Hinweistext wurde mitgeschwärzt;
debconf-Warnungen ininstall.sh.
Nicht nachstellbar
- Einmalig 46 s Verzögerung direkt nach dem Booten (CIFS-Verzeichnis-Cache bei
lokal statt per SMB angelegter Datei). Messungen danach 9–11 s, mit und ohne
nohandlecache. Als Hinweis in die Fehlersuche aufgenommen.
2026-10-10 · 19:40–19:50 · Schritt 6: Deployment, README, Release 0.1.0
Erledigt
deploy/netmover.service,deploy/netmover.env,deploy/install.sh,Dockerfile(python:3.12-slim-trixie, mehrstufig),compose.yml,.dockerignore.- README vollständig;
LICENSE(MIT, wie seit Schritt 1 inpyproject.toml). - Version 0.1.0, Changelog-Abschnitt, Tag
v0.1.0.
Geprüft
- Wheel enthält Templates, statische Dateien und Migrationen.
- Docker-Image gebaut; Container startet,
/healthz= 200,mount.cifs7.4 undmount.nfsvorhanden. Propagation getestet: tmpfs im Container unter/mnt/netmovereingehängt → mitrsharedauf dem Host sichtbar, ohne nicht. systemd-analyze verifyder Unit ohne Beanstandung. Ein echter Start der Unit als root (insb.MountFlags=sharedmitProtectSystem) war hier nicht möglich → beim ersten Einsatz mitfindmnt -R /mnt/netmoverprüfen.
Entscheidungen
- Paperless-ngx-Consumer geprüft: nutzt
watchfiles(Change.added) plus 5-Minuten-Rescan – erkennt also auch perlink()erzeugte Dateien. Für Watcher, die nurIN_MOVED_TO/IN_CLOSE_WRITEkennen, trotzdem aufrenameat2(RENAME_NOREPLACE)umgestellt (entspricht der Vorgabeos.replace, ohne die Garantie „nie überschreiben“ aufzugeben). - Härtung der Unit bewusst zurückhaltend (keine
CapabilityBoundingSet, keinProtectKernelModules): ungetestete Einschränkungen könnten Mounten brechen (z. B. NFS mit privilegiertem Quellport, Modul-Autoload für cifs). - SFTP-Schlüssel im README per
restrict,command="…/sftp-server"auf SFTP beschränkt.
Offen für später
- Erster Praxistest auf der Ziel-VM mit echtem Synology-NAS und Paperless-LXC.
2026-10-10 · 19:18–19:34 · Schritt 5: Weboberfläche und Anmeldung
Erledigt
admin.py(Formulare prüfen/speichern, frameworkunabhängig),transfer.py(Export/Import),web/auth.py,web/app.py, 14 Templates,app.css,app.js.- htmx 2.0.4 lokal eingebunden (SHA-256
e209dda5c8235479f3166defc7750e1dbcd5a5c1808b7792fc2e6733768fb447). - 25 Web-Tests (232 gesamt); Rauchtest mit echtem
netmover serve.
Entscheidungen
- Einmaliger Einrichtungscode im Log, damit nicht irgendwer im LAN beim ersten Aufruf das Admin-Passwort setzt.
- Strikte CSP (
script-src 'self'; style-src 'self') – deshalb keine Inline-Styles; Füllstand als<meter>; htmx-Indikator-Styles abgeschaltet. - Jede Anbindung pflegt eigene Zugangsdaten (Datenmodell erlaubt Teilen, die Oberfläche bleibt dadurch einfacher).
- Verbindungsdaten nur im ausgehängten Zustand änderbar; SFTP-Hostschlüssel wird bei Server-/Portwechsel verworfen.
- „Jetzt ausführen“ startet im Hintergrund (Ergebnis im Protokoll); „Probelauf“ wartet und zeigt die Tabelle.
- Webhook-URL wird nur gekürzt angezeigt; unverändert zurückgeschickte Maske = behalten.
- Import: vorhandene Namen überspringen, importierte Regeln deaktiviert,
CIFS-Anbindungen ohne
auto_mount(Zugangsdaten fehlen). - APScheduler-Logs auf WARNING (sonst alle 30 s eine Zeile pro Job).
Stolpersteine
- Starlette-Session erkennt
setdefault(...).append()nicht als Änderung → zweite Flash-Meldung ging verloren. Jetzt Neuzuweisung. request.form()liefertstarlette.datastructures.UploadFile, nicht die FastAPI-Unterklasse → Import erkannte nie eine Datei.- Settings-Import öffnete eine zweite DB-Verbindung in offener Schreib- transaktion (SQLite: „database is locked“) → Einstellungen nach dem Commit.
2026-10-10 · 19:10–19:17 · Schritt 4: Scheduler und Protokollierung
Erledigt
settings_store.py(Laufzeit-Einstellungen),notify.py(Webhook),runs.py(Lauf ausführen + protokollieren, Zähler, Aufräumen),health.py,scheduler.py,app_state.py(Verdrahtung, Start/Stop).- 24 neue Tests (207 gesamt). Mutationsprobe: 6/6 erkannt.
Entscheidungen
- Erfolg =
okoderpartial(Werkzeug arbeitet); Fehlschlag =erroroderpartial(für Benachrichtigungen). Übersprungen zählt nicht; manueller Probelauf ändert keine Zähler. - Bezug für „zu lange kein Erfolg“: spätester von letztem Erfolg, Anlage der Regel und Dienststart – sonst wäre nach einem Neustart sofort alles rot.
- „Nach N Fehlern“ meldet genau einmal beim Erreichen der Schwelle, danach Entwarnung beim ersten Erfolg (abschaltbar).
max_instances=3im Scheduler, damit überlappende Termine beim eigenen Regel-Lock ankommen und protokolliert werden, statt dass APScheduler sie still verwirft.- Leere Läufe (alle 30 s) loggen nur auf DEBUG; in
runsstehen sie trotzdem (die Oberfläche filtert standardmäßig auf Läufe mit Aktivität). - Webhook-URL wird nie geloggt (enthält oft ein Token).
Stolpersteine
- Zwei Testfehler lagen in den Tests selbst (Fake-Symlink bei
resolve(), Jitter in der Zeitschranke vergessen).
2026-10-10 · 18:53–19:07 · Schritt 3: Mover-Logik
Erledigt
- Remote
origin(Gitea) war eingerichtet →maingepusht. mover/scan.py,naming.py,ledger.py,destinations.py(lokal/Kernel-Mount und SFTP),engine.py(Ablauf pro Lauf),runner.py(Lock, Lebenszeichen, Watchdog, Ziel-Aufbau).timeouts.run_with_watchdog: Inaktivitäts-Zeitlimit mitCancelToken.- 60 neue Tests (183 gesamt), u. a. Abbruch in jeder Phase, Wettlauf um Dateinamen, hängender Lauf, SFTP-Ziel mit Kollision gegen echten SFTP-Server.
- Mutationsprobe: 9 eingebaute Fehler (keine Stabilitätsprüfung, kein fsync, keine Quellprüfung, Wettlauf, direkt unter Endnamen schreiben, keine Buchführung, keine Platzprüfung, Lock-Freigabe, kein Watchdog). Zwei überlebten zunächst (fsync, enges Wettlauf-Fenster) → zwei Tests ergänzt; jetzt alle erkannt.
Entscheidungen
- Zeitlimit als Inaktivitäts-Watchdog statt Gesamtlaufzeit: große Dateien über langsame Leitungen werden nicht abgebrochen, hängende Zugriffe schon.
- Regel-Lock gibt erst der Worker selbst frei. Nach einer Zeitüberschreitung bleibt die Regel blockiert, bis der hängende Worker endet – so kopieren nie zwei Läufe dieselbe Datei gleichzeitig. Statusseite zeigt „läuft seit …“.
- Stabilität innerhalb eines Laufs (zweite Messung nach dem Fenster) plus Quellprüfung direkt vor dem Umbenennen.
- Umbenennen ohne Überschreiben per
link()(atomar, EEXIST bei Kollision); ohne Hardlink-Unterstützung (oft CIFS) Rückfall auf „prüfen + rename“. - Bei Rekursion wird die Ordnerstruktur im Ziel beibehalten; Trennung nach Quell-Unterordnern geht über mehrere Regeln mit eigenem Unterpfad.
- Quarantänepfad ist relativ zur Quell-Anbindung (gleiches Dateisystem → rename).
copied_fileswird nach jedem vollständigen Scan bereinigt (statt nur beim Protokoll-Aufräumen) – zeitnäher, und nie nach einem abgebrochenen Scan.- ESTALE/EIO/ENOTCONN mitten im Lauf → Lauf abbrechen (nicht jede Datei einzeln scheitern lassen, nicht gegen den Quarantäne-Zähler zählen).
Stolpersteine
tmp_pathliegt auf einem Gerät → Tests liefen erst über den Schnellweg (link) statt über den Kopierweg; Fixturecross_deviceerzwingt ihn.- Prüfung „lokales Ziel nicht unter Mount-Basis“ nutzte nur
resolve()– ein Symlink umgeht das. Jetzt wörtlich und aufgelöst. - Lock-Übergabe an den Worker: doppelte Freigabe bei sehr schnellem Lauf (beim Gegenlesen gefunden, vor dem ersten Testlauf behoben).
2026-10-10 · 18:35–18:48 · Schritt 2: Mount-Schicht, Validierung, SFTP
Anforderungsänderung (Rücksprache)
- Ziel ist voraussichtlich ein LXC, der nur per SSH erreichbar ist → neuer
Anbindungstyp
sftp(nur als Ziel). Host: VM mit Debian 13 empfohlen. /healthzzählt nur aktive Anbindungen; Python ≥ 3.12 (Debian 13: 3.13).
Erledigt
validation.py: strikte Muster für Server/Freigabe/Pfade, Options-Allowlist pro Typ, gesperrte Schlüssel, Werte gegen^[A-Za-z0-9._:/@-]+$.crypto.py: Fernet-Schlüssel (0600, O_EXCL), Ed25519-Schlüsselerzeugung, Schwärzung.mounting/:MountBackend,LinuxMountBackend,FakeMountBackend,mountinfo-Parser (Oktal-Escapes, übereinanderliegende Mounts),MountManager(einhängen, aushängen, Wiederherstellung beim Start, Test, Lebenszeichen, Wiederherstellung), SFTP-Anbindung mit TOFU-Hostschlüssel.timeouts.py: Zugriffe in Daemon-Threads mit Zeitlimit (kein Pool, damit hängende Threads nichts blockieren).- 113 neue Tests (123 gesamt), u. a. gegen einen echten SFTP-Server im Prozess.
- Mutationsprobe: 5 absichtlich eingebaute Fehler (Credentials-Datei bleibt liegen,
fehlende ismount-Prüfung, kein
umount -l, nicht-leerer Mountpoint, gesperrte Optionen) werden alle von Tests erkannt.
Entscheidungen
- Migration 0001 angepasst statt 0002 – noch kein Release, keine Datenbank im Einsatz. Ab v0.1.0 sind veröffentlichte Migrationen unveränderlich.
- Mount verschwunden (nicht eingehängt) → Lauf abbrechen, kein automatisches Neu-Einhängen (Fallstrick 2). Gestört (ESTALE & Co.) → Neu-Einhängen (Fallstrick 13).
- Root-Squash-Auswertung als reine Funktion (
assess_write), damit testbar ohne root und ohne NAS. statvfs/fsyncper SFTP über die OpenSSH-Erweiterungen; fehlen sie, gibt es einen Hinweis statt eines Fehlers.- Ruff-Regel
PTHabgeschaltet: die bewusstenos.open/os.fsync-Aufrufe sind hier richtig.
Stolpersteine
- Funktionen und Klassen mit dem Präfix
test_/Testin Produktionscode würden von pytest beim Import gesammelt →check_connection,CheckReport.
Offen / nächster Schritt
- Schritt 3: Mover (Stabilität, Atomarität, Kollisionen, Kopier-Buchführung, Worker mit Zeitlimit, Quarantäne) mit Zielen lokal/Kernel-Mount und SFTP.
2026-10-10 · 17:21–17:27 · Schritt 1: Gerüst, Datenmodell, Migrationen
Erledigt
- Repo angelegt, venv (lokal Python 3.14; Code zielt auf ≥ 3.12, Debian 13 liefert 3.13).
python3-venvfehlt auf der Entwicklungsmaschine → venv mit--without-pip+get-pip. pyproject.toml(hatchling, src-Layout), ruff + mypy strict.- Datenmodell nach Spezifikation plus Ergänzungen (siehe Docstring
models.py):credentials.domain,mounts.state_since/write_ok/root_squash/last_test_*,rules.op_timeout_seconds/quarantine_after,runs.status/trigger/dry_run, Tabellefile_failures,copied_files.mtime_nsstatt Float-mtime. - Alembic-Migration 0001; Migrationen liegen im Paket, damit sie mit installiert werden.
- 10 Tests: Schema = Modell (kein Drift), Down-/Upgrade, Defaults, UTC-Roundtrip,
RESTRICT bei benutzten Mounts/Zugangsdaten, CASCADE bei Regeln, Eindeutigkeit
in
copied_files, CHECK-Constraints der Enums.
Entscheidungen
- Enums als
VARCHAR+CHECK(kein natives Enum; SQLite kann das ohnehin nicht). UTCDateTimelehnt naivedatetimeab, statt sie still als UTC zu interpretieren.- SQLite mit WAL,
foreign_keys=ON,busy_timeout=5000(Scheduler + Web + Worker-Threads).
Stolpersteine
- Alembic-Autogenerate erzeugt CHECK-Constraints bei
Enum(create_constraint=True)dreifach → expliziteCheckConstraint-Zeilen aus der Migration entfernt. - Test deckte auf: ORM setzt ohne
passive_deletesmounts.cred_idauf NULL, statt dasRESTRICTgreifen zu lassen. Behoben.
Offen / nächster Schritt
- Schritt 2: Mount-Schicht, Validierung,
FakeMountBackend, Fernet-Verschlüsselung.