Files
pobsync/src/pobsync/commands/retention_plan.py
Peter van Arkel bb62382e18 (refactor) Remove YAML config import and export path
Drop the pre-Django YAML import/export management commands and remove the
file-based config loader fallback from the backup and retention engines.

Keep the runtime config bridge backed by Django models, and add tests that
ensure engine operations require an explicit Django config source.
2026-05-21 02:34:09 +02:00

113 lines
3.2 KiB
Python

from __future__ import annotations
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, List
from ..config.source import ConfigSource
from ..errors import ConfigError
from ..retention import Snapshot, apply_base_protection, build_retention_plan
from ..snapshot_meta import iter_snapshot_dirs, read_snapshot_meta, resolve_host_root
from ..util import sanitize_host
def _parse_snapshot_dt(dirname: str, meta: dict) -> datetime:
ts = meta.get("started_at")
if isinstance(ts, str) and ts.endswith("Z"):
try:
return datetime.strptime(ts, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
except ValueError:
pass
# fallback: dirname YYYYMMDD-HHMMSSZ__ID
try:
prefix = dirname.split("__", 1)[0]
return datetime.strptime(prefix, "%Y%m%d-%H%M%SZ").replace(tzinfo=timezone.utc)
except Exception:
return datetime.fromtimestamp(0, tz=timezone.utc)
def run_retention_plan(
prefix: Path,
host: str,
kind: str,
protect_bases: bool,
config_source: ConfigSource | None = None,
) -> dict[str, Any]:
host = sanitize_host(host)
if kind not in {"scheduled", "manual", "all"}:
raise ConfigError("kind must be scheduled, manual, or all")
if config_source is None:
raise ConfigError("A Django config source is required.")
cfg = config_source.effective_config_for_host(host)
retention = cfg.get("retention")
if not isinstance(retention, dict):
raise ConfigError("No retention config found")
backup_root = cfg.get("backup_root")
if not isinstance(backup_root, str) or not backup_root.startswith("/"):
raise ConfigError("Invalid backup_root in config")
host_root = resolve_host_root(backup_root, host)
kinds: List[str]
if kind == "all":
kinds = ["scheduled", "manual"]
else:
kinds = [kind]
snapshots: List[Snapshot] = []
for kk in kinds:
for d in iter_snapshot_dirs(host_root, kk):
meta = read_snapshot_meta(d)
dt = _parse_snapshot_dt(d.name, meta)
snapshots.append(
Snapshot(
kind=kk,
dirname=d.name,
path=str(d),
dt=dt,
status=meta.get("status"),
base=meta.get("base"),
)
)
plan = build_retention_plan(
snapshots=snapshots,
retention=retention,
now=datetime.now(timezone.utc),
)
keep = set(plan.keep)
reasons = dict(plan.reasons)
if protect_bases:
keep, reasons = apply_base_protection(snapshots=snapshots, keep=keep, reasons=reasons)
delete = [s for s in snapshots if s.dirname not in keep]
return {
"ok": True,
"host": host,
"kind": kind,
"protect_bases": bool(protect_bases),
"retention": retention,
"keep": sorted(keep),
"delete": [
{
"dirname": s.dirname,
"kind": s.kind,
"path": s.path,
"dt": s.dt.isoformat(),
"status": s.status,
}
for s in delete
],
"reasons": reasons,
}