"""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 log = logging.getLogger("buchwerk") ORDNER = ("quellen", "arbeit") BLEIBT = {"QUELLEN.md"} _aufgaben: set[asyncio.Task] = set() def faellig(key: str) -> bool: """Fertig und unter quellen/ oder arbeit/ liegt noch etwas.""" v = state.verzeichnis(key) return state.zustand(key)["status"] == "fertig" and any(_eintraege(v / u) for u in ORDNER) def _eintraege(d: Path) -> list[Path]: return [p for p in d.iterdir() if p.name not in BLEIBT] if d.is_dir() else [] def auslagern(key: str) -> None: """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: 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() async def _auslagern(key: str) -> None: 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") except OSError as e: log.exception("Archiv %s", key) state.ereignis(key, "archiv", "fehler", f"{type(e).__name__}: {e}") state.speichern(key) def anstossen(key: str) -> None: """Im Hintergrund, der Lauf gilt schon als fertig.""" if config.ARCHIV and faellig(key): t = asyncio.create_task(_auslagern(key)) _aufgaben.add(t) t.add_done_callback(_aufgaben.discard) async def nachholen() -> None: """Beim Start: alle fertigen Versionen mit Restbestand, nacheinander (Bestand und abgebrochene Läufe).""" if not config.ARCHIV: return for t in state.themen(): for v in state.versionen(t["name"]): key = f"{t['name']}/v{v}" if faellig(key): await _auslagern(key)