Files
buchwerk/backend/archiv.py
T

79 lines
3.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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/<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:
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)