"""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.""" import asyncio import logging import shutil 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: """Verschiebt quellen/ (außer QUELLEN.md) und arbeit/ nach ARCHIV//. Blockiert – im Thread aufrufen.""" v = state.verzeichnis(key) for u in ORDNER: for p in _eintraege(v / u): _kopieren(p, config.ARCHIV / key / u / p.name) 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}") 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 Kopien).""" 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)