157 lines
4.4 KiB
Python
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}
|
|
|