#!/usr/bin/env python3 """ 目录同步脚本 将指定目录同步到远程机器 用法: python sync.py <目录路径> 例如: python sync.py hosts/web1/myapp """ from __future__ import annotations import argparse import os import shlex import shutil import subprocess import sys from lib import DEFAULT_BASE_PATH, project_root, rsync_ssh_args, service_info, ssh_cmd def sync_directory(info: dict) -> int: if shutil.which("rsync"): return _sync_rsync(info) print("本机没有 rsync,改用 tar over SSH(不会删除远程多余文件)") return _sync_tar(info) def _sync_rsync(info: dict) -> int: source_dir = info["service_dir"] node = info["node"] remote_path = f"{node}:{info['remote_dir']}" rsync_cmd = [ "rsync", "-avz", "--delete", *rsync_ssh_args(info), ] for item in info["sync_exclude"]: rsync_cmd.append(f"--exclude={item}/") rsync_cmd.extend([f"{source_dir}/", remote_path]) print(f"正在同步 {source_dir} 到 {remote_path}") if info["sync_exclude"]: print(f"排除目录: {', '.join(info['sync_exclude'])}") print(f"执行命令: {' '.join(rsync_cmd)}") print("-" * 60) try: subprocess.run(rsync_cmd, check=True) print("-" * 60) print("同步完成!") return 0 except subprocess.CalledProcessError as e: print(f"错误: rsync 执行失败,退出码: {e.returncode}") return 1 except FileNotFoundError: print("错误: rsync 命令未找到,请确保已安装 rsync") return 1 def _sync_tar(info: dict) -> int: source_dir = info["service_dir"] remote_dir = info["remote_dir"] tar_cmd = ["tar", "czf", "-", "-C", source_dir] for item in info["sync_exclude"]: tar_cmd.append(f"--exclude={item}") tar_cmd.append(".") remote_shell = ( f"mkdir -p {shlex.quote(remote_dir)} && " f"tar xzf - -C {shlex.quote(remote_dir)}" ) ssh = ssh_cmd(info, remote_shell) print(f"正在同步 {source_dir} 到 {info['node']}:{remote_dir}") if info["sync_exclude"]: print(f"排除目录: {', '.join(info['sync_exclude'])}") print(f"执行命令: tar | {' '.join(ssh)}") print("-" * 60) tar = subprocess.Popen(tar_cmd, stdout=subprocess.PIPE) try: completed = subprocess.run(ssh, stdin=tar.stdout, check=False) finally: if tar.stdout: tar.stdout.close() tar.wait() if tar.returncode: print(f"错误: tar 打包失败,退出码: {tar.returncode}") return tar.returncode if completed.returncode: print(f"错误: 远程 tar 解包失败,退出码: {completed.returncode}") return completed.returncode print("-" * 60) print("同步完成!") return 0 def main() -> int: parser = argparse.ArgumentParser( description="将指定目录同步到远程机器", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" 示例: python sync.py hosts/web1/myapp python sync.py infra/traefik --base-path /opt/app """, ) parser.add_argument( "directory", help="要同步的目录路径(相对项目根,例如: hosts/web1/myapp)", ) parser.add_argument( "--base-path", default=DEFAULT_BASE_PATH, help=f"远程基础路径(默认: {DEFAULT_BASE_PATH})", ) args = parser.parse_args() info = service_info(args.directory) if args.base_path != DEFAULT_BASE_PATH: name = info["name"] info["base_path"] = args.base_path info["remote_dir"] = f"{args.base_path}/{name}" print(f"目标节点: {info['node']}") if info.get("port") is not None: print(f"SSH 端口: {info['port']}") if info.get("identity_file"): print(f"SSH 密钥: {info['identity_file']}") print(f"源目录: {args.directory}") print(f"远程路径: {info['remote_dir']}") print() return sync_directory(info) if __name__ == "__main__": sys.exit(main())