Spaces:
Running
Running
AuthorBot
Production hardening: JWT blacklist, TOTP, Pydantic schemas, Prometheus, SSRF fix, CSP, Redis auth, Celery backup — 35 items across P0-P5
131d826 | """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)) | |
| 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() | |