91 lines
2.6 KiB
Python
91 lines
2.6 KiB
Python
import shutil
|
|
from datetime import datetime, timezone, timedelta
|
|
from pathlib import Path
|
|
from typing import List, Optional
|
|
from sqlalchemy import select
|
|
|
|
from app.models import Backup
|
|
|
|
|
|
class RetentionPolicy:
|
|
def __init__(self, db):
|
|
self.db = db
|
|
|
|
async def apply_retention_for_job(
|
|
self,
|
|
job_id: int,
|
|
keep_count: Optional[int] = None,
|
|
keep_days: Optional[int] = None
|
|
) -> List[Backup]:
|
|
"""
|
|
Apply retention policy for a job's backups.
|
|
|
|
Args:
|
|
job_id: The job ID to apply retention for
|
|
keep_count: Maximum number of backups to keep (oldest deleted first)
|
|
keep_days: Delete backups older than this many days
|
|
|
|
Returns:
|
|
List of deleted backups
|
|
"""
|
|
deleted_backups = []
|
|
|
|
result = await self.db.execute(
|
|
select(Backup)
|
|
.where(Backup.execution.has(job_id=job_id))
|
|
.order_by(Backup.created_at.asc())
|
|
)
|
|
backups = result.scalars().all()
|
|
|
|
if not backups:
|
|
return deleted_backups
|
|
|
|
backups_to_delete = set()
|
|
|
|
if keep_count is not None and len(backups) > keep_count:
|
|
backups_to_delete.update(backups[:-keep_count])
|
|
|
|
if keep_days is not None:
|
|
cutoff_date = datetime.now(timezone.utc) - timedelta(days=keep_days)
|
|
for backup in backups:
|
|
if backup.created_at < cutoff_date:
|
|
backups_to_delete.add(backup)
|
|
|
|
for backup in list(backups_to_delete):
|
|
await self._delete_backup(backup)
|
|
deleted_backups.append(backup)
|
|
|
|
await self.db.commit()
|
|
return deleted_backups
|
|
|
|
async def _delete_backup(self, backup: Backup):
|
|
"""Delete a backup and its storage."""
|
|
try:
|
|
storage_path = Path(backup.storage_path)
|
|
if storage_path.exists():
|
|
shutil.rmtree(storage_path)
|
|
except Exception:
|
|
pass
|
|
|
|
await self.db.delete(backup)
|
|
|
|
async def cleanup_orphaned_backups(self) -> int:
|
|
"""
|
|
Remove backup records whose storage no longer exists.
|
|
|
|
Returns:
|
|
Number of orphaned backups removed
|
|
"""
|
|
result = await self.db.execute(select(Backup))
|
|
backups = result.scalars().all()
|
|
|
|
removed_count = 0
|
|
for backup in backups:
|
|
storage_path = Path(backup.storage_path)
|
|
if not storage_path.exists():
|
|
await self.db.delete(backup)
|
|
removed_count += 1
|
|
|
|
await self.db.commit()
|
|
return removed_count
|