From 10d2e0d75253f075978c32cc4ae310f10fd4d6dc Mon Sep 17 00:00:00 2001 From: ilyukhin Date: Wed, 17 Sep 2025 15:29:06 +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=D0=B0=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D1=8C=20=D0=BE=D0=B1=D1=80=D0=B0=D1=82=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B2=20sync=20=D1=87=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=20=D1=84=D0=BB=D0=B0=D0=B3=20-r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dsync | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/dsync b/dsync index a97ada3..282b764 100644 --- a/dsync +++ b/dsync @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -VERSION = "0.2.1" +VERSION = "0.3.0" import re import sys @@ -360,10 +360,13 @@ def parse_rsync_action(line: str) -> str: else f"{direction} {item_type.upper()}: {filename} ({', '.join(details)})" -def sync() -> None: - """Synchronize local project directory to all nodes, honoring per-node dirs. +def sync(reverse: bool = False) -> None: + """Synchronize local project directory to all nodes. - Uses rsync with `--delete` so remote deletions track local deletions. + If reverse=True: + 1) Pull files that exist on remotes but are missing locally into master + (without overwriting existing local files). + 2) Then push the updated master to all nodes using --delete (to converge). """ config, project_dir = load_config() ignore_file = project_dir / config.get("ignore_file", ".dsyncignore") @@ -377,6 +380,30 @@ def sync() -> None: print(colored("No nodes configured", Colors.YELLOW)) return + # --- Reverse phase: pull missing files from each node into master --- + if reverse: + print(colored("[Reverse] Pulling missing files from nodes into master...", Colors.CYAN)) + for n in nodes: + 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. + pull_cmd = [ + "rsync", "-avz", "--ignore-existing", + 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) + 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) --- success_count = 0 failed_nodes: List[str] = [] @@ -655,7 +682,7 @@ Usage: dsync clear-node-dir Clear node dir (fallback to master_dir) dsync list-nodes List nodes with their remote dirs dsync status Show sync status for all nodes (with dirs) - dsync sync Sync to all configured nodes + dsync sync [-r | --reverse] Sync; reverse pulls missing files from nodes to master first dsync help Show this help message dsync version Print dsync version @@ -706,7 +733,9 @@ if __name__ == "__main__": elif cmd == "status": status() elif cmd == "sync": - sync() + # Parse optional flags for sync, e.g., dsync sync -r / --reverse + reverse = any(arg in ("-r", "--reverse") for arg in sys.argv[2:]) + sync(reverse=reverse) elif cmd in ("-h", "--help", "help"): show_help() elif cmd in ("-v", "--version", "version"):