feat(deployer): fall back to scp when rsync is missing

This commit is contained in:
2026-09-07 15:43:38 +08:00
parent 17643bc17b
commit 83deb886b5
6 changed files with 223 additions and 34 deletions
+1 -1
View File
@@ -117,7 +117,7 @@ def check_toolchain(report: Report) -> None:
rsync_ok = shutil.which("rsync") is not None
lines = [
f"ssh: {'found' if ssh_ok else 'MISSING (blocks compose deploy)'}",
f"rsync: {'found' if rsync_ok else 'MISSING (tar-over-SSH fallback)'}",
f"rsync: {'found' if rsync_ok else 'MISSING (scp fallback)'}",
]
if not ssh_ok:
lines.append("install openssh-client")
+66 -27
View File
@@ -22,14 +22,14 @@ _SCRIPT_DIR = Path(__file__).resolve().parent
if str(_SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPT_DIR))
from lib import DEFAULT_BASE_PATH, project_root, rsync_ssh_args, service_info, ssh_cmd # noqa: E402
from lib import DEFAULT_BASE_PATH, rsync_ssh_args, scp_base_args, service_info, ssh_cmd # noqa: E402
def sync_directory(info: dict) -> int:
if shutil.which("rsync"):
return _sync_rsync(info)
print("本机没有 rsync,改用 tar over SSH(不会删除远程多余文件)")
return _sync_tar(info)
print("本机没有 rsync,改用 scp(不会删除远程多余文件)")
return _sync_scp(info)
def _sync_rsync(info: dict) -> int:
@@ -63,42 +63,81 @@ def _sync_rsync(info: dict) -> int:
print(f"错误: rsync 执行失败,退出码: {e.returncode}")
return 1
except FileNotFoundError:
print("错误: rsync 命令未找到,请确保已安装 rsync")
return 1
print("本机没有 rsync,改用 scp(不会删除远程多余文件)")
return _sync_scp(info)
def _sync_tar(info: dict) -> int:
def collect_sync_files(source_dir: str, excludes: list[str]) -> list[Path]:
"""列出待同步文件;排除规则与 rsync `--exclude=name/` 一致(任意层级的同名目录)。"""
source = Path(source_dir)
exclude_set = set(excludes)
files: list[Path] = []
for root, dirs, names in os.walk(source):
dirs[:] = [d for d in dirs if d not in exclude_set]
for name in names:
path = Path(root) / name
if path.is_file():
files.append(path)
return files
def _sync_scp(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(".")
node = info["node"]
source = Path(source_dir)
files = collect_sync_files(source_dir, info["sync_exclude"])
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}")
print(f"正在同步 {source_dir} 到 {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)
remote_dirs = {remote_dir}
grouped: dict[str, list[Path]] = {}
for path in files:
parent = path.relative_to(source).parent.as_posix()
grouped.setdefault(parent, []).append(path)
if parent != ".":
remote_dirs.add(f"{remote_dir}/{parent}")
mkdir_remote = "mkdir -p " + " ".join(shlex.quote(d) for d in sorted(remote_dirs))
mkdir_cmd = ssh_cmd(info, mkdir_remote)
print(f"执行命令: {' '.join(mkdir_cmd)}")
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
completed = subprocess.run(mkdir_cmd, check=False)
except FileNotFoundError:
print("错误: ssh 命令未找到")
return 1
if completed.returncode:
print(f"错误: 远程 tar 解包失败,退出码: {completed.returncode}")
print(f"错误: 远程 mkdir 失败,退出码: {completed.returncode}")
return completed.returncode
if not files:
print("没有需要同步的文件")
print("-" * 60)
print("同步完成!")
return 0
try:
for parent, group in sorted(grouped.items()):
dest = remote_dir if parent == "." else f"{remote_dir}/{parent}"
scp_cmd = [
*scp_base_args(info),
"-p",
"--",
*[str(p) for p in group],
f"{node}:{dest}/",
]
print(f"执行命令: {' '.join(scp_cmd)}")
subprocess.run(scp_cmd, check=True)
except subprocess.CalledProcessError as e:
print(f"错误: scp 执行失败,退出码: {e.returncode}")
return 1
except FileNotFoundError:
print("错误: scp 命令未找到,请确保已安装 openssh-client")
return 1
print("-" * 60)
print("同步完成!")
return 0