Arag / app /tasks /backup_task.py
AuthorBot
Production hardening: JWT blacklist, TOTP, Pydantic schemas, Prometheus, SSRF fix, CSP, Redis auth, Celery backup — 35 items across P0-P5
131d826
Raw
History Blame Contribute Delete
4.06 kB
"""Author RAG Chatbot SaaS — Automated Backup Task.
Scheduled database and ChromaDB backups with retention policies.
Run via: python -m app.tasks.backup_task
Or schedule via cron: 0 2 * * * (daily at 2 AM)
"""
import hashlib
import os
import shutil
from datetime import datetime, timezone
from pathlib import Path
import structlog
from app.config import get_settings
from app.tasks.celery_app import celery_app
logger = structlog.get_logger(__name__)
cfg = get_settings()
BACKUP_DIR = Path("/data/backups")
RETENTION = {
"daily": 7, # Keep last 7 daily backups
"weekly": 4, # Keep last 4 weekly backups
"monthly": 3, # Keep last 3 monthly backups
}
def _sha256(path: Path) -> str:
"""Calculate SHA-256 checksum for integrity verification."""
h = hashlib.sha256()
with open(path, "rb") as f:
while chunk := f.read(8192):
h.update(chunk)
return h.hexdigest()
def backup_sqlite() -> Path | None:
"""Create a compressed backup of the SQLite database."""
db_url = str(cfg.DATABASE_URL)
if not db_url.startswith("sqlite"):
logger.info("Not using SQLite, skipping SQLite backup")
return None
# Extract file path from URL
db_path = db_url.split("///")[-1]
if not os.path.exists(db_path):
logger.warning("SQLite DB file not found", path=db_path)
return None
now = datetime.now(timezone.utc)
backup_subdir = BACKUP_DIR / "sqlite" / now.strftime("%Y-%m-%d")
backup_subdir.mkdir(parents=True, exist_ok=True)
backup_file = backup_subdir / f"db_{now.strftime('%H%M%S')}.sqlite3"
shutil.copy2(db_path, backup_file)
# Write checksum
checksum = _sha256(backup_file)
(backup_subdir / f"db_{now.strftime('%H%M%S')}.sha256").write_text(checksum)
size_mb = backup_file.stat().st_size / 1024 / 1024
logger.info("SQLite backup created", path=str(backup_file), size_mb=f"{size_mb:.2f}", checksum=checksum[:16])
return backup_file
def backup_chromadb() -> Path | None:
"""Backup ChromaDB persistent directory."""
chroma_dir = Path(cfg.CHROMA_PERSIST_DIR)
if not chroma_dir.exists():
logger.warning("ChromaDB dir not found", path=str(chroma_dir))
return None
now = datetime.now(timezone.utc)
backup_subdir = BACKUP_DIR / "chromadb" / now.strftime("%Y-%m-%d")
backup_subdir.mkdir(parents=True, exist_ok=True)
archive_path = backup_subdir / f"chroma_{now.strftime('%H%M%S')}"
shutil.copytree(chroma_dir, archive_path, dirs_exist_ok=True)
logger.info("ChromaDB backup created", path=str(archive_path))
return archive_path
def _cleanup_old_backups(subdir: str) -> None:
"""Remove backups older than retention policy."""
base = BACKUP_DIR / subdir
if not base.exists():
return
dirs = sorted(base.iterdir(), reverse=True)
max_keep = RETENTION["daily"] # Keep last N daily backups
for i, d in enumerate(dirs):
if i >= max_keep and d.is_dir():
shutil.rmtree(d)
logger.info("Removed old backup", path=str(d))
@celery_app.task(name="app.tasks.backup_task.run_backup")
def run_backup() -> dict:
"""Run full backup cycle."""
results = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"sqlite": None,
"chromadb": None,
}
try:
sqlite_path = backup_sqlite()
results["sqlite"] = str(sqlite_path) if sqlite_path else "skipped"
except Exception as e:
logger.error("SQLite backup failed", error=str(e))
results["sqlite"] = f"error: {e}"
try:
chroma_path = backup_chromadb()
results["chromadb"] = str(chroma_path) if chroma_path else "skipped"
except Exception as e:
logger.error("ChromaDB backup failed", error=str(e))
results["chromadb"] = f"error: {e}"
# Cleanup old backups
_cleanup_old_backups("sqlite")
_cleanup_old_backups("chromadb")
logger.info("Backup cycle complete", results=results)
return results
if __name__ == "__main__":
run_backup()