from datetime import datetime from django.http import FileResponse from ninja import File, Router, Schema from ninja.errors import HttpError from ninja.files import UploadedFile from core.models import BackupSchedule from core.services.backups import ( compute_next_run, create_backup_now, get_backup_file, import_backup_content, is_backup_due, list_backups, ) router = Router(tags=["backups"]) class BackupScheduleSchema(Schema): enabled: bool frequency: str minute: int hour: int day_of_week: int day_of_month: int retention_count: int last_run_at: datetime | None = None next_run_at: datetime | None = None class BackupScheduleUpdateSchema(Schema): enabled: bool frequency: str minute: int = 0 hour: int = 2 day_of_week: int = 0 day_of_month: int = 1 retention_count: int = 14 class BackupFileSchema(Schema): filename: str size_bytes: int created_at: str def _require_admin(request): if not request.user.is_authenticated: raise HttpError(401, "Authentication required.") if not request.user.is_staff: raise HttpError(403, "Admin permissions required.") @router.get("/settings", response=BackupScheduleSchema) def get_backup_schedule(request): _require_admin(request) schedule = BackupSchedule.get_solo() return { "enabled": schedule.enabled, "frequency": schedule.frequency, "minute": schedule.minute, "hour": schedule.hour, "day_of_week": schedule.day_of_week, "day_of_month": schedule.day_of_month, "retention_count": schedule.retention_count, "last_run_at": schedule.last_run_at, "next_run_at": compute_next_run(schedule), } @router.put("/settings", response=BackupScheduleSchema) def update_backup_schedule(request, payload: BackupScheduleUpdateSchema): _require_admin(request) if payload.frequency not in BackupSchedule.Frequency.values: raise HttpError(400, "Invalid frequency") if not 0 <= payload.minute <= 59: raise HttpError(400, "minute must be 0..59") if not 0 <= payload.hour <= 23: raise HttpError(400, "hour must be 0..23") if not 0 <= payload.day_of_week <= 6: raise HttpError(400, "day_of_week must be 0..6") if not 1 <= payload.day_of_month <= 28: raise HttpError(400, "day_of_month must be 1..28") if payload.retention_count < 1: raise HttpError(400, "retention_count must be >= 1") schedule = BackupSchedule.get_solo() for attr, value in payload.dict().items(): setattr(schedule, attr, value) schedule.save() return { "enabled": schedule.enabled, "frequency": schedule.frequency, "minute": schedule.minute, "hour": schedule.hour, "day_of_week": schedule.day_of_week, "day_of_month": schedule.day_of_month, "retention_count": schedule.retention_count, "last_run_at": schedule.last_run_at, "next_run_at": compute_next_run(schedule), } @router.post("/run") def run_backup_now(request): _require_admin(request) path = create_backup_now() return {"status": "ok", "filename": path.name} @router.get("/", response=list[BackupFileSchema]) def get_backups(request): _require_admin(request) return [x.__dict__ for x in list_backups()] @router.get("/{filename}/download") def download_backup(request, filename: str): _require_admin(request) try: target = get_backup_file(filename) except FileNotFoundError: raise HttpError(404, "Backup not found") response = FileResponse(open(target, "rb"), content_type="application/json") response["Content-Disposition"] = f'attachment; filename="{target.name}"' return response @router.post("/import") def import_backup(request, file: UploadedFile = File(...), mode: str = "append"): _require_admin(request) if mode not in {"append", "override"}: raise HttpError(400, "mode must be append or override") content = file.read().decode("utf-8") try: result = import_backup_content(content, mode=mode) except ValueError as exc: raise HttpError(400, str(exc)) return {"status": "ok", **result} @router.post("/run-if-due") def run_if_due(request): _require_admin(request) schedule = BackupSchedule.get_solo() if not is_backup_due(schedule): return {"status": "skipped"} path = create_backup_now() return {"status": "ok", "filename": path.name}