From 54d1d6a8da3cc75abc32a23e88a7c1a819e15a5c Mon Sep 17 00:00:00 2001 From: team3 Date: Wed, 14 Oct 2026 16:14:49 +0200 Subject: [PATCH] =?UTF-8?q?Archiv:=20je=20Version=20quellen.tar=20und=20ar?= =?UTF-8?q?beit.tar=20statt=20einzelner=20Dateien=20=E2=80=93=20SMB=20scha?= =?UTF-8?q?fft=2025=20Dateien/s,=20am=20St=C3=BCck=2090=20MB/s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- API.md | 4 ++-- README.md | 7 ++++--- backend/archiv.py | 41 ++++++++++++++++++++--------------------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/API.md b/API.md index 8a720db..72f907c 100644 --- a/API.md +++ b/API.md @@ -108,5 +108,5 @@ storage// ``` Alte Ablagen (alles direkt unter `storage//`) werden beim Start nach `v1/` verschoben, fehlende Slugs nachgetragen. -Bei fertigen Versionen liegen `quellen/` (außer QUELLEN.md) und `arbeit/` im Archiv (`BUCHWERK_ARCHIV//vN/`), wenn eines -konfiguriert ist; Ereignisse dazu tragen den Agenten `archiv`. +Bei fertigen Versionen liegen `quellen/` (außer QUELLEN.md) und `arbeit/` als `quellen.tar` und `arbeit.tar` im Archiv +(`BUCHWERK_ARCHIV//vN/`), wenn eines konfiguriert ist; Ereignisse dazu tragen den Agenten `archiv`. diff --git a/README.md b/README.md index 0f4cb8d..fad685f 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,10 @@ make logs / make stop `storage/` und `.claude-data/` (Sitzungen der CLI) werden als Volumes eingebunden; die Claude-Code-CLI liegt als Binary im SDK, Node wird im Container nicht gebraucht. -Archiv: ist `ARCHIV_DIR` in `.env` gesetzt (auf dem Server die Storage Box, Mount mit `uid=1000`, da der Container als -`app` schreibt), verschiebt `backend/archiv.py` nach jedem fertigen Lauf `quellen/` (außer QUELLEN.md) und `arbeit/` -der Version dorthin – das sind die Gigabytes an Klonen. Beim Start holt er nach, was fehlt. Ohne Variable bleibt alles in `storage/`. +Archiv: ist `ARCHIV_DIR` in `.env` gesetzt (auf dem Server ein Ordner der Storage Box, deren Mount `gid=1000` hat, da der +Container als `app` schreibt), packt `backend/archiv.py` nach jedem fertigen Lauf `quellen/` (außer QUELLEN.md) und `arbeit/` +der Version als `quellen.tar` und `arbeit.tar` dorthin und löscht sie – das sind die Gigabytes an Klonen. Beim Start holt er +nach, was fehlt. Ohne Variable bleibt alles in `storage/`. Ablauf (backend/pipeline.py): Stufen seriell, innerhalb einer Stufe bis zu `PARALLEL` Agenten gleichzeitig. Jeder Agent arbeitet im Themenverzeichnis `storage//`; sein Abschlussbericht landet unter `berichte/` diff --git a/backend/archiv.py b/backend/archiv.py index 7309d1c..89c23e6 100644 --- a/backend/archiv.py +++ b/backend/archiv.py @@ -1,9 +1,12 @@ -"""Quellen und Arbeitsdateien fertiger Versionen ins Archiv verschieben (config.ARCHIV, auf dem Server eine Storage Box). -Nichts liest danach noch daraus: neue Versionen recherchieren neu, fertige Läufe setzen nie fort. QUELLEN.md bleibt im Speicher, -die Buchansicht zeigt sie. Erst kopieren, dann löschen – ein Abbruch lässt den Speicher vollständig, der nächste Start holt nach.""" +"""Quellen und Arbeitsdateien fertiger Versionen als tar ins Archiv (config.ARCHIV, auf dem Server die Storage Box). +Je Version quellen.tar (ohne QUELLEN.md, die bleibt im Speicher und steht in der Buchansicht) und arbeit.tar. Ein tar statt +einzelner Dateien, weil SMB je Datei mehrere Round-Trips braucht (25 Dateien/s), am Stück aber 90 MB/s schafft. +Nichts liest danach noch daraus: neue Versionen recherchieren neu, fertige Läufe setzen nie fort. +Erst packen, dann löschen – ein Abbruch lässt den Speicher vollständig, der nächste Start holt nach.""" import asyncio import logging import shutil +import subprocess from pathlib import Path from . import config, state @@ -25,32 +28,28 @@ def _eintraege(d: Path) -> list[Path]: def auslagern(key: str) -> None: - """Verschiebt quellen/ (außer QUELLEN.md) und arbeit/ nach ARCHIV//. Blockiert – im Thread aufrufen.""" + """Packt quellen/ (außer QUELLEN.md) und arbeit/ nach ARCHIV//.tar und löscht sie. Blockiert – im Thread aufrufen.""" v = state.verzeichnis(key) + ziel = config.ARCHIV / key + ziel.mkdir(parents=True, exist_ok=True) for u in ORDNER: - for p in _eintraege(v / u): - _kopieren(p, config.ARCHIV / key / u / p.name) + eintraege = _eintraege(v / u) + if not eintraege: + continue + tmp = ziel / f"{u}.tar.tmp" + r = subprocess.run(["tar", "-cf", str(tmp), "-C", str(v / u), *[p.name for p in eintraege]], capture_output=True, text=True) + if r.returncode: + raise OSError(f"tar {u}/ von {key}: {r.stderr.strip()[-300:]}") + tmp.replace(ziel / f"{u}.tar") + for p in eintraege: if p.is_dir() and not p.is_symlink(): shutil.rmtree(p) else: p.unlink() -def _kopieren(quelle: Path, ziel: Path) -> None: - """Ohne Rechte, Zeiten und Symlinks: CIFS kennt beides nicht. Links in Klonen entfallen.""" - if quelle.is_symlink(): - return - if quelle.is_dir(): - ziel.mkdir(parents=True, exist_ok=True) - for k in quelle.iterdir(): - _kopieren(k, ziel / k.name) - elif quelle.is_file(): - ziel.parent.mkdir(parents=True, exist_ok=True) - shutil.copyfile(quelle, ziel) - - async def _auslagern(key: str) -> None: - state.ereignis(key, "archiv", "start", f"Quellen und Arbeitsdateien nach {config.ARCHIV / key}") + state.ereignis(key, "archiv", "start", f"Quellen und Arbeitsdateien als tar nach {config.ARCHIV / key}") try: await asyncio.to_thread(auslagern, key) state.ereignis(key, "archiv", "ende", "ausgelagert") @@ -69,7 +68,7 @@ def anstossen(key: str) -> None: async def nachholen() -> None: - """Beim Start: alle fertigen Versionen mit Restbestand, nacheinander (Bestand und abgebrochene Kopien).""" + """Beim Start: alle fertigen Versionen mit Restbestand, nacheinander (Bestand und abgebrochene Läufe).""" if not config.ARCHIV: return for t in state.themen():