Files
PYTV/api/routers/backups.py
2026-03-20 15:00:24 -04:00

157 lines
4.4 KiB
Python

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}