63 lines
2.1 KiB
Python
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)
|