Archiv: je Version quellen.tar und arbeit.tar statt einzelner Dateien – SMB schafft 25 Dateien/s, am Stück 90 MB/s

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
team3andClaude Fable 5.1 committed 2026-10-14 16:14:49 +02:00
1 parent 7ba08b68e0
commit 54d1d6a8da
3 files changed
+26 -26

No files matched your search

+2 -2
View File
@@ -108,5 +108,5 @@ storage/<Thema>/
```
Alte Ablagen (alles direkt unter `storage/<Thema>/`) werden beim Start nach `v1/` verschoben, fehlende Slugs nachgetragen.
Bei fertigen Versionen liegen `quellen/` (außer QUELLEN.md) und `arbeit/` im Archiv (`BUCHWERK_ARCHIV/<Thema>/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/<Thema>/vN/`), wenn eines konfiguriert ist; Ereignisse dazu tragen den Agenten `archiv`.
+4 -3
View File
@@ -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/<Thema>/`; sein Abschlussbericht landet unter `berichte/`
+20 -21
View File
@@ -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/<key>/. Blockiert – im Thread aufrufen."""
"""Packt quellen/ (außer QUELLEN.md) und arbeit/ nach ARCHIV/<key>/<ordner>.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():