mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 10:45:27 +00:00
- Create/Download/Upload/Restore database backups (PostgreSQL and SQLite) - Configurable data directory backups (via settings.py) - Scheduled backups (daily/weekly) via Celery Beat - Retention policy (keep last N backups) - Token-based auth for async task polling - X-Accel-Redirect support for nginx file serving - Comprehensive tests
106 lines
3.5 KiB
Python
106 lines
3.5 KiB
Python
import logging
|
|
import traceback
|
|
from celery import shared_task
|
|
|
|
from . import services
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _cleanup_old_backups(retention_count: int) -> int:
|
|
"""Delete old backups, keeping only the most recent N. Returns count deleted."""
|
|
if retention_count <= 0:
|
|
return 0
|
|
|
|
backups = services.list_backups()
|
|
if len(backups) <= retention_count:
|
|
return 0
|
|
|
|
# Backups are sorted newest first, so delete from the end
|
|
to_delete = backups[retention_count:]
|
|
deleted = 0
|
|
|
|
for backup in to_delete:
|
|
try:
|
|
services.delete_backup(backup["name"])
|
|
deleted += 1
|
|
logger.info(f"[CLEANUP] Deleted old backup: {backup['name']}")
|
|
except Exception as e:
|
|
logger.error(f"[CLEANUP] Failed to delete {backup['name']}: {e}")
|
|
|
|
return deleted
|
|
|
|
|
|
@shared_task(bind=True)
|
|
def create_backup_task(self):
|
|
"""Celery task to create a backup asynchronously."""
|
|
try:
|
|
logger.info(f"[BACKUP] Starting backup task {self.request.id}")
|
|
backup_file = services.create_backup()
|
|
logger.info(f"[BACKUP] Task {self.request.id} completed: {backup_file.name}")
|
|
return {
|
|
"status": "completed",
|
|
"filename": backup_file.name,
|
|
"size": backup_file.stat().st_size,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"[BACKUP] Task {self.request.id} failed: {str(e)}")
|
|
logger.error(f"[BACKUP] Traceback: {traceback.format_exc()}")
|
|
return {
|
|
"status": "failed",
|
|
"error": str(e),
|
|
}
|
|
|
|
|
|
@shared_task(bind=True)
|
|
def restore_backup_task(self, filename: str):
|
|
"""Celery task to restore a backup asynchronously."""
|
|
try:
|
|
logger.info(f"[RESTORE] Starting restore task {self.request.id} for {filename}")
|
|
backup_dir = services.get_backup_dir()
|
|
backup_file = backup_dir / filename
|
|
logger.info(f"[RESTORE] Backup file path: {backup_file}")
|
|
services.restore_backup(backup_file)
|
|
logger.info(f"[RESTORE] Task {self.request.id} completed successfully")
|
|
return {
|
|
"status": "completed",
|
|
"filename": filename,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"[RESTORE] Task {self.request.id} failed: {str(e)}")
|
|
logger.error(f"[RESTORE] Traceback: {traceback.format_exc()}")
|
|
return {
|
|
"status": "failed",
|
|
"error": str(e),
|
|
}
|
|
|
|
|
|
@shared_task(bind=True)
|
|
def scheduled_backup_task(self, retention_count: int = 0):
|
|
"""Celery task for scheduled backups with optional retention cleanup."""
|
|
try:
|
|
logger.info(f"[SCHEDULED] Starting scheduled backup task {self.request.id}")
|
|
|
|
# Create backup
|
|
backup_file = services.create_backup()
|
|
logger.info(f"[SCHEDULED] Backup created: {backup_file.name}")
|
|
|
|
# Cleanup old backups if retention is set
|
|
deleted = 0
|
|
if retention_count > 0:
|
|
deleted = _cleanup_old_backups(retention_count)
|
|
logger.info(f"[SCHEDULED] Cleanup complete, deleted {deleted} old backup(s)")
|
|
|
|
return {
|
|
"status": "completed",
|
|
"filename": backup_file.name,
|
|
"size": backup_file.stat().st_size,
|
|
"deleted_count": deleted,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"[SCHEDULED] Task {self.request.id} failed: {str(e)}")
|
|
logger.error(f"[SCHEDULED] Traceback: {traceback.format_exc()}")
|
|
return {
|
|
"status": "failed",
|
|
"error": str(e),
|
|
}
|