From 0fda00ba856b9469835ea128c78fa0990f3ba6b5 Mon Sep 17 00:00:00 2001 From: ilyukhin Date: Wed, 17 Sep 2025 16:02:48 +0000 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80=D0=B5=D1=81=D1=81=20?= =?UTF-8?q?=D0=B1=D0=B0=D1=80=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=B8=D0=BD?= =?UTF-8?q?=D1=85=D1=80=D0=BE=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dsync | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 9 deletions(-) diff --git a/dsync b/dsync index 282b764..cbc6bf6 100644 --- a/dsync +++ b/dsync @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -VERSION = "0.3.0" +VERSION = "0.4.0" import re import sys @@ -25,6 +25,10 @@ class Colors: BOLD = '\033[1m' +# Simple inline progress bar renderer +_BAR_WIDTH = 28 + + def colored(text: str, color: str) -> str: """Wrap text with an ANSI color. @@ -359,6 +363,66 @@ def parse_rsync_action(line: str) -> str: return f"{direction} {item_type.upper()}: {filename}" if not details \ else f"{direction} {item_type.upper()}: {filename} ({', '.join(details)})" +def _render_progress(prefix: str, pct: int) -> None: + """Render a single-line progress bar like: '[#####.....] 42%'. + + Args: + prefix: Text shown before the bar (e.g., node label). + pct: Integer percent [0..100]. + """ + pct = max(0, min(100, pct)) + filled = int(_BAR_WIDTH * pct / 100) + bar = "#" * filled + "." * (_BAR_WIDTH - filled) + end = "\r" if sys.stdout.isatty() else "\n" + print(f"{prefix} [{bar}] {pct:3d}% ", end=end, flush=True) + + +def _run_rsync_with_progress(cmd: List[str], label: str) -> None: + """Execute rsync and show a best-effort progress bar. + + Notes: + - Removes '-v' for cleaner output and appends '--info=progress2'. + - Parses percentage from rsync output and updates a single-line bar. + - Forces a final 100% on success when rsync emits no/low progress. + """ + base = [c for c in cmd if c != "-v"] + if "--info=progress2" not in base: + base.append("--info=progress2") + + proc = subprocess.Popen( + base, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + ) + last_pct = -1 + try: + assert proc.stdout is not None + for line in proc.stdout: + m = re.search(r"(\d+)%", line) + if m: + pct = int(m.group(1)) + if pct != last_pct: + _render_progress(label, pct) + last_pct = pct + + rc = proc.wait() + + # NEW: if rsync completed successfully but never reached 100% (or printed nothing), + # render a final 100% so the user doesn't see a stuck 0%. + if rc == 0 and last_pct != 100: + _render_progress(label, 100) + + if sys.stdout.isatty(): + print() # finalize the progress line + + if rc != 0: + raise subprocess.CalledProcessError(rc, base) + finally: + if proc.stdout: + proc.stdout.close() + def sync(reverse: bool = False) -> None: """Synchronize local project directory to all nodes. @@ -387,23 +451,22 @@ def sync(reverse: bool = False) -> None: node = n["name"] rdir = n["remote_dir"] or master_dir remote = f"{node}:{rdir}/" # trailing slash to copy dir contents - print(f"[Reverse] From {remote} -> {project_dir}/ (ignore-existing)") - # --ignore-existing ensures we only fetch files that do NOT exist locally. + label = f"[Reverse] {node} -> master" pull_cmd = [ - "rsync", "-avz", "--ignore-existing", + "rsync", "-az", "--ignore-existing", # dropped '-v' for cleaner progress f"--exclude-from={ignore_file}", "-e", "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10", remote, f"{project_dir}/" ] try: - subprocess.run(pull_cmd, check=True, capture_output=True, text=True) + _run_rsync_with_progress(pull_cmd, label) print(colored(f"[OK] Pulled missing files from {node}", Colors.GREEN)) except subprocess.CalledProcessError as e: print(colored(f"[X] Failed reverse-pull from {node} ({rdir}): {e}", Colors.RED)) print() # spacing - # --- Forward phase: push master to all nodes (same as before) --- + # --- Forward phase: push master to all nodes --- success_count = 0 failed_nodes: List[str] = [] @@ -411,16 +474,16 @@ def sync(reverse: bool = False) -> None: node = n["name"] rdir = n["remote_dir"] or master_dir remote = f"{node}:{rdir}" - print(f"Syncing to {remote}...") + label = f"[Sync] {node}" cmd = [ - "rsync", "-avz", "--delete", + "rsync", "-az", "--delete", # dropped '-v' for cleaner progress f"--exclude-from={ignore_file}", "-e", "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10", f"{project_dir}/", remote ] try: - subprocess.run(cmd, check=True, capture_output=True, text=True) + _run_rsync_with_progress(cmd, label) print(colored(f"[OK] Successfully synced to {node} ({rdir})", Colors.GREEN)) success_count += 1 except subprocess.CalledProcessError as e: