# 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 | | **Summe** | | | **0:33** | | | --- ## 2026-10-10 · 18:53–19:07 · Schritt 3: Mover-Logik **Erledigt** - Remote `origin` (Gitea) war eingerichtet → `main` gepusht. - `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 mit `CancelToken`. - 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_files` wird 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_path` liegt auf einem Gerät → Tests liefen erst über den Schnellweg (`link`) statt über den Kopierweg; Fixture `cross_device` erzwingt 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. - `/healthz` zä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`/`fsync` per SFTP über die OpenSSH-Erweiterungen; fehlen sie, gibt es einen Hinweis statt eines Fehlers. - Ruff-Regel `PTH` abgeschaltet: die bewussten `os.open`/`os.fsync`-Aufrufe sind hier richtig. **Stolpersteine** - Funktionen und Klassen mit dem Präfix `test_`/`Test` in 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-venv` fehlt 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`, Tabelle `file_failures`, `copied_files.mtime_ns` statt 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). - `UTCDateTime` lehnt naive `datetime` ab, 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 → explizite `CheckConstraint`-Zeilen aus der Migration entfernt. - Test deckte auf: ORM setzt ohne `passive_deletes` `mounts.cred_id` auf NULL, statt das `RESTRICT` greifen zu lassen. Behoben. **Offen / nächster Schritt** - Schritt 2: Mount-Schicht, Validierung, `FakeMountBackend`, Fernet-Verschlüsselung.