Finance-Accounts/ar-aging-app/backend/app/services/retention.py

63 lines
2.1 KiB
Python

"""
Retention: purge generated export workbooks older than AR_RETENTION_DAYS.
EXPORTS ONLY, by design. Uploaded source files are never auto-deleted — they are the audit
source for every published figure, and re-generating the full workbook re-parses them.
Export files are pure derivatives: anything purged can be regenerated with one click, and
the DB row is kept so the Exports list still shows what was generated and when
(`available: false` once the file is gone).
AR_RETENTION_DAYS=0 keeps everything forever.
"""
from __future__ import annotations
import asyncio
import datetime as dt
import logging
import os
from ..config import RETENTION_DAYS
from ..db import models
from ..db.database import SessionLocal
logger = logging.getLogger(__name__)
_SWEEP_INTERVAL_S = 24 * 3600
def purge_old_exports(retention_days: int | None = None) -> int:
"""Delete export files older than the retention window. Returns files removed."""
days = RETENTION_DAYS if retention_days is None else retention_days
if days <= 0:
return 0
cutoff = dt.datetime.utcnow() - dt.timedelta(days=days)
removed = 0
db = SessionLocal()
try:
rows = db.query(models.ExportRecord).filter(
models.ExportRecord.generated_at < cutoff).all()
for r in rows:
if not r.path:
continue
try:
if os.path.exists(r.path):
os.remove(r.path)
removed += 1
except OSError:
logger.warning("retention: could not remove %s", r.path)
if removed:
logger.info("retention: removed %d export file(s) older than %d days",
removed, days)
except Exception: # noqa: BLE001 — a failed sweep must never take the app down
logger.exception("retention sweep failed")
finally:
db.close()
return removed
async def retention_loop() -> None:
"""Daily sweep, started from the app lifespan. Cancelled cleanly on shutdown."""
while True:
await asyncio.to_thread(purge_old_exports)
await asyncio.sleep(_SWEEP_INTERVAL_S)