diff --git a/src/pobsync/commands/run_scheduled.py b/src/pobsync/commands/run_scheduled.py index 435e5ac..8076625 100644 --- a/src/pobsync/commands/run_scheduled.py +++ b/src/pobsync/commands/run_scheduled.py @@ -140,6 +140,7 @@ def run_scheduled( config_source: ConfigSource | None = None, run_id: int | None = None, cancel_check: Callable[[], bool] | None = None, + verbose_output: bool = False, ) -> dict[str, Any]: host = sanitize_host(host) @@ -228,6 +229,7 @@ def run_scheduled( "log": str(dryrun_log), "cancelled": result.cancelled, "timeout_seconds": effective_timeout_seconds, + "verbose_output": True, "ssh_credential": cfg.get("ssh_credential"), "rsync": { "exit_code": result.exit_code, @@ -276,6 +278,7 @@ def run_scheduled( bwlimit_kbps=bwlimit_kbps, extra_excludes=list(excludes), extra_includes=list(includes), + verbose_output=bool(verbose_output), ) meta: dict[str, Any] = { @@ -284,6 +287,7 @@ def run_scheduled( "host": host, "type": "scheduled", "label": None, + "verbose_output": bool(verbose_output), "status": "running", "started_at": format_iso_z(ts), "ended_at": None, @@ -328,6 +332,7 @@ def run_scheduled( "status": meta["status"], "cancelled": result.cancelled, "log": str(log_path), + "verbose_output": bool(verbose_output), "ssh_credential": cfg.get("ssh_credential"), "rsync": { "exit_code": result.exit_code, @@ -362,5 +367,6 @@ def run_scheduled( "snapshot": str(final_dir), "base": str(base_dir) if base_dir else None, "rsync": {"exit_code": result.exit_code}, + "verbose_output": bool(verbose_output), "prune": prune_result, } diff --git a/src/pobsync/rsync.py b/src/pobsync/rsync.py index d3b5990..8ab6c79 100644 --- a/src/pobsync/rsync.py +++ b/src/pobsync/rsync.py @@ -10,7 +10,7 @@ from pathlib import Path from typing import Callable, Sequence -DEFAULT_DRY_RUN_OUTPUT_ARGS = ["--itemize-changes", "--info=flist2,progress2,stats2"] +DEFAULT_VERBOSE_OUTPUT_ARGS = ["--itemize-changes", "--info=flist2,progress2,stats2"] @dataclass(frozen=True) @@ -43,12 +43,13 @@ def build_rsync_command( bwlimit_kbps: int, extra_excludes: Sequence[str], extra_includes: Sequence[str], + verbose_output: bool = False, ) -> list[str]: cmd: list[str] = [rsync_binary] cmd.extend(list(rsync_args)) - if dry_run: - _append_default_dry_run_output_args(cmd) + if dry_run or verbose_output: + _append_default_verbose_output_args(cmd) # includes/excludes: keep it simple for now: # - if includes are provided, user is responsible for correct rsync include logic. @@ -123,7 +124,7 @@ def _terminate_process_group(process: subprocess.Popen) -> None: process.wait(timeout=10) -def _append_default_dry_run_output_args(command: list[str]) -> None: +def _append_default_verbose_output_args(command: list[str]) -> None: if not _has_itemize_arg(command): command.append("--itemize-changes") if not any(arg.startswith("--info=") for arg in command): diff --git a/src/pobsync_backend/backup_runner.py b/src/pobsync_backend/backup_runner.py index 8f4876b..26372be 100644 --- a/src/pobsync_backend/backup_runner.py +++ b/src/pobsync_backend/backup_runner.py @@ -18,6 +18,7 @@ def queue_backup_run( host: HostConfig, run_type: str = BackupRun.RunType.MANUAL, dry_run: bool = False, + verbose_output: bool = False, prune: bool = False, prune_max_delete: int = 10, prune_protect_bases: bool = False, @@ -29,6 +30,7 @@ def queue_backup_run( result={ "requested": { "dry_run": bool(dry_run), + "verbose_output": bool(dry_run or verbose_output), "prune": bool(prune), "prune_max_delete": int(prune_max_delete), "prune_protect_bases": bool(prune_protect_bases), @@ -42,6 +44,7 @@ def execute_backup_run( run: BackupRun, prefix: Path, dry_run: bool = False, + verbose_output: bool = False, prune: bool = False, prune_max_delete: int = 10, prune_protect_bases: bool = False, @@ -60,6 +63,7 @@ def execute_backup_run( config_source=DjangoConfigSource(), run_id=run.id, cancel_check=lambda: _run_cancel_requested(run.id), + verbose_output=bool(dry_run or verbose_output), ) except Exception as exc: run.refresh_from_db() diff --git a/src/pobsync_backend/forms.py b/src/pobsync_backend/forms.py index 9a4b3f8..887c082 100644 --- a/src/pobsync_backend/forms.py +++ b/src/pobsync_backend/forms.py @@ -133,6 +133,11 @@ class ManualBackupForm(forms.Form): initial=True, help_text="Queue rsync in dry-run mode without writing a snapshot.", ) + verbose_output = forms.BooleanField( + label="Verbose rsync output", + required=False, + help_text="Write itemized rsync changes, file-list progress, and stats to the run log. Dry-runs always use this.", + ) prune = forms.BooleanField( label="Apply retention after success", required=False, diff --git a/src/pobsync_backend/management/commands/run_pobsync_backup.py b/src/pobsync_backend/management/commands/run_pobsync_backup.py index d54fcee..a0ffcce 100644 --- a/src/pobsync_backend/management/commands/run_pobsync_backup.py +++ b/src/pobsync_backend/management/commands/run_pobsync_backup.py @@ -18,6 +18,7 @@ class Command(BaseCommand): parser.add_argument("host", help="Host to back up") parser.add_argument("--prefix", default=settings.POBSYNC_HOME, help="Pobsync home directory") parser.add_argument("--dry-run", action="store_true", help="Run rsync --dry-run") + parser.add_argument("--verbose-rsync", action="store_true", help="Write itemized rsync output to the run log") parser.add_argument("--prune", action="store_true", help="Apply retention after a successful run") parser.add_argument("--prune-max-delete", type=int, default=10) parser.add_argument("--prune-protect-bases", action="store_true") @@ -35,11 +36,21 @@ class Command(BaseCommand): host=host, run_type=BackupRun.RunType.MANUAL if options["manual"] else BackupRun.RunType.SCHEDULED, status=BackupRun.Status.RUNNING, + result={ + "requested": { + "dry_run": bool(options["dry_run"]), + "verbose_output": bool(options["dry_run"] or options["verbose_rsync"]), + "prune": bool(options["prune"]), + "prune_max_delete": int(options["prune_max_delete"]), + "prune_protect_bases": bool(options["prune_protect_bases"]), + } + }, ) execute_backup_run( run=run, prefix=paths.home, dry_run=bool(options["dry_run"]), + verbose_output=bool(options["dry_run"] or options["verbose_rsync"]), prune=bool(options["prune"]), prune_max_delete=int(options["prune_max_delete"]), prune_protect_bases=bool(options["prune_protect_bases"]), diff --git a/src/pobsync_backend/management/commands/run_pobsync_worker.py b/src/pobsync_backend/management/commands/run_pobsync_worker.py index 303c951..e1288c0 100644 --- a/src/pobsync_backend/management/commands/run_pobsync_worker.py +++ b/src/pobsync_backend/management/commands/run_pobsync_worker.py @@ -44,6 +44,7 @@ class Command(BaseCommand): run=run, prefix=prefix, dry_run=bool(options.get("dry_run", False)), + verbose_output=bool(options.get("verbose_output", False)), prune=bool(options.get("prune", False)), prune_max_delete=int(options.get("prune_max_delete", 10)), prune_protect_bases=bool(options.get("prune_protect_bases", False)), diff --git a/src/pobsync_backend/templates/pobsync_backend/host_detail.html b/src/pobsync_backend/templates/pobsync_backend/host_detail.html index f812772..fc2be55 100644 --- a/src/pobsync_backend/templates/pobsync_backend/host_detail.html +++ b/src/pobsync_backend/templates/pobsync_backend/host_detail.html @@ -127,6 +127,7 @@
diff --git a/src/pobsync_backend/templates/pobsync_backend/run_detail.html b/src/pobsync_backend/templates/pobsync_backend/run_detail.html index c8bc9d3..5c9581c 100644 --- a/src/pobsync_backend/templates/pobsync_backend/run_detail.html +++ b/src/pobsync_backend/templates/pobsync_backend/run_detail.html @@ -46,6 +46,7 @@