(feature) queue manual backups from the Django host page

Add a staff-only manual backup form to host detail pages with safe
dry-run defaults and optional retention settings.

Queue manual BackupRun records through the existing worker-backed runner
path instead of executing backups inside the web request.

Validate disabled hosts, missing global config, and invalid methods with
view tests covering the new UI flow.
This commit is contained in:
2026-05-19 13:04:50 +02:00
parent fe8e65e12e
commit 3da877eb8a
5 changed files with 141 additions and 1 deletions

View File

@@ -10,7 +10,8 @@ from django.views.decorators.http import require_POST
from pobsync.errors import ConfigError
from .forms import CreateHostConfigForm, GlobalConfigForm, HostConfigForm, ScheduleConfigForm
from .backup_runner import queue_backup_run
from .forms import CreateHostConfigForm, GlobalConfigForm, HostConfigForm, ManualBackupForm, ScheduleConfigForm
from .models import BackupRun, GlobalConfig, HostConfig, ScheduleConfig, SnapshotRecord
from .retention import run_sql_retention_plan
from .snapshot_discovery import discover_snapshots
@@ -90,6 +91,7 @@ def host_detail(request, host: str):
context = {
"host": host_config,
"schedule": _schedule_for_host(host_config),
"manual_backup_form": ManualBackupForm(initial=_default_manual_backup_initial(host_config)),
"latest_runs": host_config.runs.select_related("snapshot").order_by("-created_at")[:10],
"snapshots": host_config.snapshots.select_related("base").order_by("-started_at", "dirname")[:20],
"counts": {
@@ -102,6 +104,33 @@ def host_detail(request, host: str):
return render(request, "pobsync_backend/host_detail.html", context)
@staff_member_required
@require_POST
def queue_manual_backup(request, host: str):
host_config = get_object_or_404(HostConfig, host=host)
if not host_config.enabled:
messages.error(request, f"Cannot queue backup for disabled host {host_config.host}.")
return redirect("host_detail", host=host_config.host)
if not GlobalConfig.objects.filter(name="default").exists():
messages.error(request, "Create the default global config before queueing backups.")
return redirect("host_detail", host=host_config.host)
form = ManualBackupForm(request.POST)
if not form.is_valid():
messages.error(request, "Manual backup options are invalid.")
return redirect("host_detail", host=host_config.host)
run = queue_backup_run(
host=host_config,
dry_run=form.cleaned_data["dry_run"],
prune=form.cleaned_data["prune"],
prune_max_delete=form.cleaned_data["prune_max_delete"],
prune_protect_bases=form.cleaned_data["prune_protect_bases"],
)
messages.success(request, f"Queued manual backup run {run.id} for {host_config.host}.")
return redirect("run_detail", run_id=run.id)
@staff_member_required
def run_detail(request, run_id: int):
run = get_object_or_404(BackupRun.objects.select_related("host", "snapshot"), id=run_id)
@@ -257,5 +286,15 @@ def _default_host_initial() -> dict[str, object]:
}
def _default_manual_backup_initial(host_config: HostConfig) -> dict[str, object]:
schedule = _schedule_for_host(host_config)
return {
"dry_run": True,
"prune": bool(schedule.prune) if schedule else False,
"prune_max_delete": schedule.prune_max_delete if schedule else 10,
"prune_protect_bases": bool(schedule.prune_protect_bases) if schedule else False,
}
def _pretty_json(value: object) -> str:
return json.dumps(value or {}, indent=2, sort_keys=True)