146 lines
4.0 KiB
Python
Executable File
146 lines
4.0 KiB
Python
Executable File
#!/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())
|