(refactor) Unify run progress panels
Use a shared Run Progress presentation for dry-runs and normal backup runs so live run feedback is consistent across run types. Keep mode-specific metrics while aligning status, mode, log, and warning layout. Refs #52
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import json
|
||||
import shlex
|
||||
import shutil
|
||||
@@ -711,6 +712,7 @@ def _run_detail_context(run: BackupRun) -> dict[str, object]:
|
||||
"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()),
|
||||
"rsync_log_tail": rsync_log_tail,
|
||||
"live_progress": _run_live_progress(run, rsync_log_path),
|
||||
"dry_run_summary": _dry_run_summary(
|
||||
result=result,
|
||||
requested=requested,
|
||||
@@ -1260,6 +1262,97 @@ def _run_rsync_log_tail(rsync_result: dict, log_path: Path | None, *, max_lines:
|
||||
return []
|
||||
|
||||
|
||||
def _run_live_progress(run: BackupRun, log_path: Path | None) -> dict[str, object]:
|
||||
if run.status not in {BackupRun.Status.QUEUED, BackupRun.Status.RUNNING}:
|
||||
return {}
|
||||
|
||||
result = run.result if isinstance(run.result, dict) else {}
|
||||
requested = result.get("requested") if isinstance(result.get("requested"), dict) else {}
|
||||
if requested.get("dry_run") or result.get("dry_run"):
|
||||
return {}
|
||||
|
||||
execution = result.get("execution") if isinstance(result.get("execution"), dict) else {}
|
||||
rsync = result.get("rsync") if isinstance(result.get("rsync"), dict) else {}
|
||||
progress: dict[str, object] = {
|
||||
"phase": execution.get("phase") or ("queued" if run.status == BackupRun.Status.QUEUED else "running"),
|
||||
"worker_pid": execution.get("worker_pid"),
|
||||
"rsync_pid": rsync.get("pid"),
|
||||
"rsync_pgid": rsync.get("pgid"),
|
||||
}
|
||||
|
||||
log_stats = _live_log_stats(log_path)
|
||||
if log_stats:
|
||||
progress["log"] = log_stats
|
||||
|
||||
snapshot_path = _run_progress_snapshot_path(run, execution)
|
||||
if snapshot_path is not None:
|
||||
progress["snapshot"] = {
|
||||
"path": str(snapshot_path),
|
||||
**_scan_snapshot_progress(snapshot_path / "data" if (snapshot_path / "data").exists() else snapshot_path),
|
||||
}
|
||||
return progress
|
||||
|
||||
|
||||
def _run_progress_snapshot_path(run: BackupRun, execution: dict) -> Path | None:
|
||||
snapshot = execution.get("snapshot")
|
||||
if isinstance(snapshot, str) and snapshot:
|
||||
return Path(snapshot)
|
||||
if run.snapshot_path:
|
||||
return Path(run.snapshot_path)
|
||||
return None
|
||||
|
||||
|
||||
def _live_log_stats(log_path: Path | None) -> dict[str, object]:
|
||||
if log_path is None:
|
||||
return {}
|
||||
try:
|
||||
stat = log_path.stat()
|
||||
except OSError:
|
||||
return {"path": str(log_path), "exists": False}
|
||||
modified_at = timezone.datetime.fromtimestamp(stat.st_mtime, tz=timezone.get_current_timezone())
|
||||
return {
|
||||
"path": str(log_path),
|
||||
"exists": True,
|
||||
"size_bytes": stat.st_size,
|
||||
"modified_at": modified_at,
|
||||
"seconds_since_modified": max(0, int((timezone.now() - modified_at).total_seconds())),
|
||||
}
|
||||
|
||||
|
||||
def _scan_snapshot_progress(data_path: Path, *, max_entries: int = 20000) -> dict[str, object]:
|
||||
progress: dict[str, object] = {
|
||||
"data_path": str(data_path),
|
||||
"exists": data_path.exists(),
|
||||
"files": 0,
|
||||
"directories": 0,
|
||||
"apparent_size_bytes": 0,
|
||||
"scan_limited": False,
|
||||
}
|
||||
if not data_path.exists():
|
||||
return progress
|
||||
|
||||
entries_seen = 0
|
||||
for root, dirnames, filenames in os.walk(data_path):
|
||||
progress["directories"] = int(progress["directories"]) + len(dirnames)
|
||||
entries_seen += len(dirnames)
|
||||
for filename in filenames:
|
||||
file_path = Path(root) / filename
|
||||
try:
|
||||
file_stat = file_path.lstat()
|
||||
except OSError:
|
||||
continue
|
||||
progress["files"] = int(progress["files"]) + 1
|
||||
progress["apparent_size_bytes"] = int(progress["apparent_size_bytes"]) + int(file_stat.st_size)
|
||||
entries_seen += 1
|
||||
if entries_seen >= max_entries:
|
||||
progress["scan_limited"] = True
|
||||
return progress
|
||||
if entries_seen >= max_entries:
|
||||
progress["scan_limited"] = True
|
||||
return progress
|
||||
return progress
|
||||
|
||||
|
||||
def _dry_run_summary(
|
||||
*,
|
||||
result: dict,
|
||||
|
||||
Reference in New Issue
Block a user