"""Temporary storage of job working directories and results. Nothing is kept longer than needed. A result lives until it is picked up or until its TTL expires, whichever comes first. """ from __future__ import annotations import logging import shutil from pathlib import Path logger = logging.getLogger(__name__) RESULT_NAME = "result.pdf" class Storage: def __init__(self, root: str) -> None: self.root = Path(root) self.root.mkdir(parents=True, exist_ok=True) def job_dir(self, job_id: str) -> Path: path = self.root / job_id path.mkdir(parents=True, exist_ok=True) return path def result_path(self, job_id: str) -> Path: return self.root / job_id / RESULT_NAME def publish(self, job_id: str, produced: Path) -> Path: """Move the produced file to its final name and drop the intermediates.""" target = self.result_path(job_id) if produced != target: produced.replace(target) for item in self.job_dir(job_id).iterdir(): if item.name == RESULT_NAME: continue self._remove(item) return target def discard(self, job_id: str) -> None: self._remove(self.root / job_id) def _remove(self, path: Path) -> None: try: if path.is_dir(): shutil.rmtree(path, ignore_errors=False) elif path.exists(): path.unlink() except OSError as exc: logger.warning("Could not remove temporary path", extra={"path": str(path)}, exc_info=exc) def sweep_orphans(self, known_job_ids: set[str]) -> int: """Remove directories that belong to no known job, for example after a restart.""" removed = 0 for item in self.root.iterdir(): if item.is_dir() and item.name not in known_job_ids: self._remove(item) removed += 1 if removed: logger.info("Removed orphaned job directories", extra={"count": removed}) return removed