(feature) Link rsync logs from backup run detail

Record the final rsync log path for successful real backup runs, matching
the existing dry-run and failure result payloads.

Add a staff-only run log endpoint and surface the link on run detail pages,
including fallback log discovery for older runs based on snapshot_path.

Cover direct log links and inferred scheduled backup logs with view tests.
This commit is contained in:
2026-05-20 00:09:59 +02:00
parent f41e59e695
commit 0babc57f57
6 changed files with 85 additions and 0 deletions

View File

@@ -8,6 +8,7 @@ from pathlib import Path
from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required
from django.conf import settings
from django.http import FileResponse, Http404
from django.db.models import Count
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
@@ -362,16 +363,28 @@ def queue_manual_backup(request, host: str):
def run_detail(request, run_id: int):
run = get_object_or_404(BackupRun.objects.select_related("host", "snapshot"), id=run_id)
run_stats = run.result.get("stats") if isinstance(run.result, dict) else {}
rsync_log_path = _run_rsync_log_path(run)
context = {
"run": run,
"can_cancel": run.status in {BackupRun.Status.QUEUED, BackupRun.Status.RUNNING},
"requested": run.result.get("requested") if isinstance(run.result, dict) else {},
"stats": run_stats if isinstance(run_stats, dict) else {},
"rsync_log_path": str(rsync_log_path) if rsync_log_path is not None else "",
"rsync_log_exists": bool(rsync_log_path and rsync_log_path.exists()),
"result_json": _pretty_json(run.result),
}
return render(request, "pobsync_backend/run_detail.html", context)
@staff_member_required
def run_rsync_log(request, run_id: int):
run = get_object_or_404(BackupRun.objects.select_related("host", "snapshot"), id=run_id)
log_path = _run_rsync_log_path(run)
if log_path is None or not log_path.is_file():
raise Http404("Rsync log not found")
return FileResponse(log_path.open("rb"), content_type="text/plain; charset=utf-8")
@staff_member_required
@require_POST
def cancel_run(request, run_id: int):
@@ -634,6 +647,21 @@ def _pretty_json(value: object) -> str:
return json.dumps(value or {}, indent=2, sort_keys=True)
def _run_rsync_log_path(run: BackupRun) -> Path | None:
if isinstance(run.result, dict):
log = run.result.get("log")
if isinstance(log, str) and log:
return Path(log)
execution = run.result.get("execution")
if isinstance(execution, dict):
execution_log = execution.get("log")
if isinstance(execution_log, str) and execution_log:
return Path(execution_log)
if run.snapshot_path:
return Path(run.snapshot_path) / "meta" / "rsync.log"
return None
def _log_context(request) -> dict[str, object]:
units = ("pobsync-web.service", "pobsync-worker.service", "pobsync-scheduler.service")
priorities = {