Skip to content

Instantly share code, notes, and snippets.

@Yunuuuu
Last active July 6, 2026 10:03
Show Gist options
  • Select an option

  • Save Yunuuuu/dee09540eb5335bf1b023385a9b77b5c to your computer and use it in GitHub Desktop.

Select an option

Save Yunuuuu/dee09540eb5335bf1b023385a9b77b5c to your computer and use it in GitHub Desktop.
面向弱网和不稳定 SSH 连接的 rsync 自动重试脚本, 支持无限重连, SSH 配置与 ssh-agent, 以及可选临时 SSH 密钥.
#!/usr/bin/env bash
# rsync-resume 是一个面向不稳定网络环境的 rsync 传输包装脚本。
#
# 它负责在 rsync 因网络、SSH 或协议连接问题中断后自动等待并重新执行传输,
# 适合跨公网、弱网、高延迟或连接容易中断的服务器之间进行长时间数据同步。
#
# 脚本本身不会为 rsync 添加默认同步参数。所有归档、删除、压缩、断点文件、
# 进度显示、限速和排除规则等传输行为, 均由用户直接通过 rsync 参数控制。
#
# 对于 SSH 远程传输, 脚本默认使用现有 SSH 配置、IdentityFile 或 ssh-agent
# 进行非交互认证。用户也可以显式启用临时 SSH 密钥模式, 在满足服务器允许
# 公钥认证且当前用户可以维护 authorized_keys 的情况下, 为本次传输创建临时
# 密钥, 并在脚本退出时尝试自动撤销。
#
# 主要功能:
#
# 网络或 SSH 中断后自动重试 rsync
# 默认无限重试, 可配置重试次数和重试间隔
# 支持本地到本地、本地到远程、远程到本地
# 尊重 ~/.ssh/config、IdentityFile 和 ssh-agent
# 可选临时 SSH 密钥,并在退出时尝试自动撤销
# SSH 连接超时和 ServerAlive 参数可配置
# wrapper 参数支持环境变量和命令行覆盖
# rsync 参数原样透传, 不注入默认同步选项
# EXIT、Ctrl+C、SIGTERM 和 SIGHUP 时统一执行清理
#
# 安装:
# mkdir -p "$HOME/.local/bin"
# curl -fsSL 'https://gist.githubusercontent.com/Yunuuuu/dee09540eb5335bf1b023385a9b77b5c/raw/rsync-resume.sh' -o "$HOME/.local/bin/rsync-resume"
# chmod 0755 "$HOME/.local/bin/rsync-resume"
#
# 卸载:
# rm -f "$HOME/.local/bin/rsync-resume"
#
set -Eeuo pipefail
PROGRAM_NAME=${0##*/}
SOURCE=''
DESTINATION=''
REMOTE_CONN=''
TMP_DIR=''
TMP_KEY=''
KEY_TYPE=''
KEY_BLOB=''
KEY_MARKER=''
TEMP_KEY_INSTALLED=0
TEMP_KEY_MODE='no'
PORT_WAS_SET=0
REMOTE_PORT=''
SSH_PORT_LABEL='SSH config/default'
if [[ ${PORT+x} ]]; then
PORT_WAS_SET=1
REMOTE_PORT=$PORT
SSH_PORT_LABEL=$PORT
fi
RETRY_DELAY=${RETRY_DELAY:-30}
MAX_RETRIES=${MAX_RETRIES:-0}
CONNECT_TIMEOUT=${CONNECT_TIMEOUT:-15}
SERVER_ALIVE_INTERVAL=${SERVER_ALIVE_INTERVAL:-15}
SERVER_ALIVE_COUNT_MAX=${SERVER_ALIVE_COUNT_MAX:-3}
CLEANUP_RETRIES=${CLEANUP_RETRIES:-3}
CLEANUP_RETRY_DELAY=${CLEANUP_RETRY_DELAY:-2}
BOOTSTRAP_SSH_OPTS=()
TEMP_SSH_OPTS=()
EXISTING_SSH_OPTS=()
SSH_RSH=''
RSYNC_EXTRA_ARGS=()
usage() {
cat <<EOF
面向弱网和不稳定 SSH 连接的 rsync 自动重试脚本, 支持无限重连、SSH 配置与 ssh-agent, 以及可选临时 SSH 密钥。
homepage: https://gist.github.com/Yunuuuu/dee09540eb5335bf1b023385a9b77b5c
用法:
$PROGRAM_NAME [脚本参数...] <源路径> <目的路径> [rsync 参数...]
$PROGRAM_NAME [脚本参数...] <源路径> <目的路径> -- [rsync 参数...]
脚本参数可放在源路径和目的路径之前、之间或之后。
遇到 -- 后停止解析脚本参数,后续参数全部原样传递给 rsync。
支持:
本地 -> 本地
本地 -> SSH 远程
SSH 远程 -> 本地
示例:
$PROGRAM_NAME /data/src/ /data/dst/ -- -avhP
$PROGRAM_NAME /data/src/ /data/dst/ -- --delete --exclude='*.tmp' --bwlimit=50M
$PROGRAM_NAME /data/src/ user@example.com:/data/dst/ -- -avhP
$PROGRAM_NAME user@example.com:/data/src/ /data/dst/ -- -avhP
$PROGRAM_NAME /data/src/ user@example.com:/data/dst/ --port=49001 --max-retries=100 -- -avhP
$PROGRAM_NAME /data/src/ user@example.com:/data/dst/ --temp-key=yes --retry-delay=10 -- --delete --dry-run
配置优先级:
命令行参数 > 环境变量 > 脚本默认值
脚本参数:
-h, --help 输出帮助文档并退出
--temp-key=yes|no 是否创建临时 SSH 密钥; 默认 no
--port=N 对应 PORT; 未设置时尊重 SSH config/默认端口
--retry-delay=N 对应 RETRY_DELAY; 默认 30 秒
--max-retries=N 对应 MAX_RETRIES; 默认 0; 0 表示无限重试
--connect-timeout=N 对应 CONNECT_TIMEOUT; 默认 15 秒
--server-alive-interval=N 对应 SERVER_ALIVE_INTERVAL; 默认 15 秒
--server-alive-count-max=N 对应 SERVER_ALIVE_COUNT_MAX; 默认 3
--cleanup-retries=N 对应 CLEANUP_RETRIES; 默认 3
--cleanup-retry-delay=N 对应 CLEANUP_RETRY_DELAY; 默认 2 秒
上述参数也支持空格写法, 例如:
--port 49001
--max-retries 100
--temp-key yes
说明:
--temp-key=yes 创建并安装临时 SSH 密钥,退出时尝试自动撤销。
--temp-key=no 不创建临时密钥, 只使用现有 SSH 非交互认证(默认)。
其余参数会原样传递给 rsync; 本脚本不为 rsync 添加 -a/-v/-h/--progress/--partial 等默认参数。
-- 可作为本脚本与 rsync 参数的可选分隔符, 不会传递给 rsync。
使用 -- 后, 后续所有参数都只传给 rsync, 不再解析本脚本参数。
SSH 认证说明:
默认使用 --temp-key=no, 要求现有 SSH 非交互认证可用。
如果私钥带有 passphrase, 请先将密钥加入 ssh-agent:
ssh-add ~/.ssh/id_ed25519
如果 ~/.ssh/config 的 IdentityFile 使用自定义密钥, 例如 ~/.ssh/backup_ed25519:
ssh-add ~/.ssh/backup_ed25519
建议先验证非交互登录:
ssh -o BatchMode=yes user@server true
如果当前只能通过密码完成首次登录, 但服务器允许公钥认证, 且当前用户
可以维护 ~/.ssh/authorized_keys, 可显式使用 --temp-key=yes。脚本会
创建临时 SSH 密钥、安装公钥, 并在退出时尝试自动撤销。
如果服务器明确禁用公钥认证, 则 --temp-key=yes 无效。本脚本不会保存、
传递或自动重复输入 SSH 密码, 因此无法为这种服务器提供无人值守的无限重连。
环境变量:
PORT=<未设置> 未设置时尊重 SSH config/默认端口; 显式设置时覆盖 SSH config 的 Port
RETRY_DELAY=30 可重试错误后的等待秒数
MAX_RETRIES=0 最大重试次数; 0 表示无限重试
CONNECT_TIMEOUT=15 SSH 连接超时秒数
SERVER_ALIVE_INTERVAL=15 SSH ServerAliveInterval
SERVER_ALIVE_COUNT_MAX=3 SSH ServerAliveCountMax
CLEANUP_RETRIES=3 临时密钥撤销最大尝试次数
CLEANUP_RETRY_DELAY=2 临时密钥撤销重试等待秒数
EOF
}
now() {
date '+%Y-%m-%d %H:%M:%S'
}
log_info() {
printf '[%s] %s\n' "$(now)" "$*"
}
log_warn() {
printf '[%s] 警告: %s\n' "$(now)" "$*" >&2
}
log_error() {
printf '[%s] 错误: %s\n' "$(now)" "$*" >&2
}
usage_error() {
log_error "$*"
printf '\n' >&2
usage >&2
exit 64
}
fatal() {
log_error "$*"
exit 1
}
validate_uint() {
local name=$1 value=$2 min=$3 max=$4 number
[[ "$value" =~ ^[0-9]+$ ]] || fatal "$name 必须是非负整数, 当前值: $value"
number=$((10#$value))
(( number >= min )) || fatal "$name 不能小于 $min, 当前值: $value"
if (( max >= 0 && number > max )); then
fatal "$name 不能大于 $max, 当前值: $value"
fi
}
validate_temp_key_mode() {
case "$TEMP_KEY_MODE" in
yes|no) return 0 ;;
*) usage_error "--temp-key 只接受 yes 或 no, 当前值: $TEMP_KEY_MODE" ;;
esac
}
require_command() {
local command_name=$1
command -v "$command_name" >/dev/null 2>&1 || fatal "缺少必需命令: $command_name"
}
is_rsync_daemon_path() {
local path=$1
[[ "$path" == rsync://* ]] || [[ "$path" =~ ^([^/@:]+@)?(\[[^]]+\]|[^/:]+):: ]]
}
is_remote_shell_path() {
local path=$1
is_rsync_daemon_path "$path" && return 1
[[ "$path" =~ ^([^/@:]+@)?(\[[^]]+\]|[^/:]+): ]]
}
extract_remote_conn() {
local path=$1
if [[ "$path" =~ ^(([^/@:]+@)?(\[[^]]+\]|[^/:]+)): ]]; then
printf '%s\n' "${BASH_REMATCH[1]}"
return 0
fi
return 1
}
rsync_exit_description() {
case "$1" in
0) printf '成功' ;;
1) printf '语法或用法错误' ;;
2) printf '协议不兼容' ;;
3) printf '选择输入/输出文件或目录时出错' ;;
4) printf '请求的操作不受支持' ;;
5) printf '启动客户端/服务器协议失败' ;;
6) printf 'daemon 无法追加日志' ;;
10) printf 'socket I/O 错误' ;;
11) printf '文件 I/O 错误' ;;
12) printf 'rsync 协议数据流错误' ;;
13) printf '程序诊断数据错误' ;;
14) printf 'IPC 错误' ;;
20) printf '收到 SIGUSR1 或 SIGINT' ;;
21) printf 'waitpid 返回错误' ;;
22) printf '分配核心内存缓冲区失败' ;;
23) printf '因错误导致部分传输' ;;
24) printf '源文件消失导致部分传输' ;;
25) printf '达到 --max-delete 限制' ;;
30) printf '数据发送/接收超时' ;;
35) printf '等待 daemon 连接超时' ;;
255) printf 'SSH/远程 shell 连接错误' ;;
*) printf '未知错误' ;;
esac
}
is_retryable_rsync_exit() {
case "$1" in
5|10|12|30|35|255) return 0 ;;
*) return 1 ;;
esac
}
install_temp_key() {
local remote_command
remote_command="sh -s -- '$KEY_TYPE' '$KEY_BLOB' '$KEY_MARKER'"
log_info "正在向 $REMOTE_CONN 安装本次传输的临时公钥..."
if ! ssh "${BOOTSTRAP_SSH_OPTS[@]}" "$REMOTE_CONN" "$remote_command" <<'REMOTE_INSTALL'
set -eu
key_type=$1
key_blob=$2
key_marker=$3
ssh_dir=$HOME/.ssh
authorized_keys=$ssh_dir/authorized_keys
umask 077
mkdir -p "$ssh_dir"
chmod 700 "$ssh_dir"
touch "$authorized_keys"
chmod 600 "$authorized_keys"
if awk -v blob="$key_blob" '
{
for (i = 1; i <= NF; i++) {
if ($i == blob) {
found = 1
break
}
}
}
END { exit(found ? 0 : 1) }
' "$authorized_keys"; then
exit 0
fi
printf '%s %s %s\n' "$key_type" "$key_blob" "$key_marker" >> "$authorized_keys"
REMOTE_INSTALL
then
fatal "临时公钥安装失败。请检查 SSH 登录、远端 HOME/.ssh 权限和 authorized_keys 配置。"
fi
TEMP_KEY_INSTALLED=1
}
revoke_temp_key_once() {
local remote_command
remote_command="sh -s -- '$KEY_BLOB'"
ssh "${TEMP_SSH_OPTS[@]}" "$REMOTE_CONN" "$remote_command" <<'REMOTE_CLEANUP'
set -eu
key_blob=$1
authorized_keys=$HOME/.ssh/authorized_keys
[ -f "$authorized_keys" ] || exit 0
umask 077
# rsync-cleanup: exact key-blob removal; the marker is for test/audit visibility.
tmp_file=${authorized_keys}.rsync-cleanup.$$
trap 'rm -f "$tmp_file"' EXIT HUP INT TERM
awk -v blob="$key_blob" '
{
remove = 0
for (i = 1; i <= NF; i++) {
if ($i == blob) {
remove = 1
break
}
}
if (!remove) print
}
' "$authorized_keys" > "$tmp_file"
chmod 600 "$tmp_file"
mv -f "$tmp_file" "$authorized_keys"
trap - EXIT HUP INT TERM
REMOTE_CLEANUP
}
revoke_temp_key() {
local attempt=1
while (( attempt <= CLEANUP_RETRIES )); do
if revoke_temp_key_once; then
return 0
fi
if (( attempt < CLEANUP_RETRIES )); then
log_warn "临时密钥撤销失败, 第 $attempt/$CLEANUP_RETRIES 次; ${CLEANUP_RETRY_DELAY} 秒后重试..."
sleep "$CLEANUP_RETRY_DELAY"
fi
attempt=$((attempt + 1))
done
return 1
}
cleanup() {
local original_status=$?
trap - EXIT INT TERM HUP
set +e
if (( TEMP_KEY_INSTALLED == 1 )); then
if revoke_temp_key; then
log_info "临时密钥已撤销。"
TEMP_KEY_INSTALLED=0
else
log_error "安全警告: 无法从 $REMOTE_CONN 撤销临时密钥!"
log_error "请恢复网络后手工删除 authorized_keys 中标记为 '$KEY_MARKER' 的公钥。"
fi
fi
if [[ -n "$TMP_DIR" && -d "$TMP_DIR" ]]; then
rm -rf -- "$TMP_DIR"
fi
exit "$original_status"
}
run_rsync_with_retry() {
local retries=0 status description
local -a command=(rsync)
command+=("${RSYNC_EXTRA_ARGS[@]}")
# 远程模式下将脚本管理的 SSH transport 放在透传参数之后。
# rsync 的 -e/--rsh 写入同一个 shell_cmd 选项; 后出现的值生效。
if [[ -n "$REMOTE_CONN" ]]; then
command+=(-e "$SSH_RSH")
fi
command+=(-- "$SOURCE" "$DESTINATION")
while true; do
if "${command[@]}"; then
return 0
else
status=$?
fi
description=$(rsync_exit_description "$status")
if ! is_retryable_rsync_exit "$status"; then
log_error "rsync 不可重试错误: exit=$status ($description)。"
return "$status"
fi
if (( MAX_RETRIES > 0 && retries >= MAX_RETRIES )); then
log_error "rsync 可重试错误仍未恢复: exit=$status ($description); 达到最大重试次数 $MAX_RETRIES。"
return "$status"
fi
retries=$((retries + 1))
if (( MAX_RETRIES == 0 )); then
log_warn "rsync 可重试错误: exit=$status ($description); ${RETRY_DELAY} 秒后进行第 $retries 次重试(无限模式)..."
else
log_warn "rsync 可重试错误: exit=$status ($description); ${RETRY_DELAY} 秒后进行第 $retries/$MAX_RETRIES 次重试..."
fi
sleep "$RETRY_DELAY"
done
}
main() {
local source_is_remote=0 destination_is_remote=0
local -a positional_args=()
RSYNC_EXTRA_ARGS=()
while (( $# > 0 )); do
case "$1" in
-h|--help)
usage
return 0
;;
--)
shift
RSYNC_EXTRA_ARGS+=("$@")
break
;;
--temp-key=*)
TEMP_KEY_MODE=${1#*=}
shift
;;
--temp-key)
(( $# >= 2 )) || usage_error '--temp-key 缺少 yes 或 no 参数。'
TEMP_KEY_MODE=$2
shift 2
;;
--port=*)
PORT_WAS_SET=1
REMOTE_PORT=${1#*=}
SSH_PORT_LABEL=$REMOTE_PORT
shift
;;
--port)
(( $# >= 2 )) || usage_error '--port 缺少端口号。'
PORT_WAS_SET=1
REMOTE_PORT=$2
SSH_PORT_LABEL=$REMOTE_PORT
shift 2
;;
--retry-delay=*)
RETRY_DELAY=${1#*=}
shift
;;
--retry-delay)
(( $# >= 2 )) || usage_error '--retry-delay 缺少秒数。'
RETRY_DELAY=$2
shift 2
;;
--max-retries=*)
MAX_RETRIES=${1#*=}
shift
;;
--max-retries)
(( $# >= 2 )) || usage_error '--max-retries 缺少次数。'
MAX_RETRIES=$2
shift 2
;;
--connect-timeout=*)
CONNECT_TIMEOUT=${1#*=}
shift
;;
--connect-timeout)
(( $# >= 2 )) || usage_error '--connect-timeout 缺少秒数。'
CONNECT_TIMEOUT=$2
shift 2
;;
--server-alive-interval=*)
SERVER_ALIVE_INTERVAL=${1#*=}
shift
;;
--server-alive-interval)
(( $# >= 2 )) || usage_error '--server-alive-interval 缺少秒数。'
SERVER_ALIVE_INTERVAL=$2
shift 2
;;
--server-alive-count-max=*)
SERVER_ALIVE_COUNT_MAX=${1#*=}
shift
;;
--server-alive-count-max)
(( $# >= 2 )) || usage_error '--server-alive-count-max 缺少次数。'
SERVER_ALIVE_COUNT_MAX=$2
shift 2
;;
--cleanup-retries=*)
CLEANUP_RETRIES=${1#*=}
shift
;;
--cleanup-retries)
(( $# >= 2 )) || usage_error '--cleanup-retries 缺少次数。'
CLEANUP_RETRIES=$2
shift 2
;;
--cleanup-retry-delay=*)
CLEANUP_RETRY_DELAY=${1#*=}
shift
;;
--cleanup-retry-delay)
(( $# >= 2 )) || usage_error '--cleanup-retry-delay 缺少秒数。'
CLEANUP_RETRY_DELAY=$2
shift 2
;;
*)
if (( ${#positional_args[@]} < 2 )); then
positional_args+=("$1")
else
RSYNC_EXTRA_ARGS+=("$1")
fi
shift
;;
esac
done
if (( ${#positional_args[@]} < 2 )); then
usage
exit 64
fi
SOURCE=${positional_args[0]}
DESTINATION=${positional_args[1]}
validate_temp_key_mode
[[ -n "$SOURCE" ]] || usage_error '源路径不能为空。'
[[ -n "$DESTINATION" ]] || usage_error '目的路径不能为空。'
if (( PORT_WAS_SET == 1 )); then
validate_uint PORT "$REMOTE_PORT" 1 65535
fi
validate_uint RETRY_DELAY "$RETRY_DELAY" 0 -1
validate_uint MAX_RETRIES "$MAX_RETRIES" 0 -1
validate_uint CONNECT_TIMEOUT "$CONNECT_TIMEOUT" 1 -1
validate_uint SERVER_ALIVE_INTERVAL "$SERVER_ALIVE_INTERVAL" 1 -1
validate_uint SERVER_ALIVE_COUNT_MAX "$SERVER_ALIVE_COUNT_MAX" 1 -1
validate_uint CLEANUP_RETRIES "$CLEANUP_RETRIES" 1 -1
validate_uint CLEANUP_RETRY_DELAY "$CLEANUP_RETRY_DELAY" 0 -1
require_command rsync
if is_rsync_daemon_path "$SOURCE" || is_rsync_daemon_path "$DESTINATION"; then
usage_error '本脚本不支持 rsync daemon(host::module 或 rsync://)模式; 临时 SSH 密钥流程只适用于 SSH remote-shell 模式。'
fi
is_remote_shell_path "$SOURCE" && source_is_remote=1
is_remote_shell_path "$DESTINATION" && destination_is_remote=1
if (( source_is_remote == 1 && destination_is_remote == 1 )); then
usage_error 'rsync 不支持两个远程端点之间直接复制。请至少让源或目的其中一端为本机。'
fi
if (( source_is_remote == 0 && destination_is_remote == 0 )); then
log_info '检测到纯本地传输, 开始 rsync 传输。'
run_rsync_with_retry
log_info '✨ 本地数据传输完成!'
return 0
fi
require_command ssh
if (( source_is_remote == 1 )); then
REMOTE_CONN=$(extract_remote_conn "$SOURCE") || fatal '无法解析远程源连接信息。'
else
REMOTE_CONN=$(extract_remote_conn "$DESTINATION") || fatal '无法解析远程目的连接信息。'
fi
EXISTING_SSH_OPTS=()
if (( PORT_WAS_SET == 1 )); then
EXISTING_SSH_OPTS+=(-p "$REMOTE_PORT")
fi
EXISTING_SSH_OPTS+=(
-o BatchMode=yes
-o "ConnectTimeout=$CONNECT_TIMEOUT"
-o "ServerAliveInterval=$SERVER_ALIVE_INTERVAL"
-o "ServerAliveCountMax=$SERVER_ALIVE_COUNT_MAX"
)
if [[ "$TEMP_KEY_MODE" == yes ]]; then
require_command ssh-keygen
require_command mktemp
umask 077
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/rsync-transfer.XXXXXXXX") || fatal '无法创建安全临时目录。'
TMP_KEY=$TMP_DIR/id_ed25519
KEY_MARKER="rsync-temp-$(date -u '+%Y%m%dT%H%M%SZ')-$$-${RANDOM}${RANDOM}"
log_info '正在生成本次传输专用 Ed25519 临时密钥...'
if ! ssh-keygen -q -t ed25519 -f "$TMP_KEY" -N '' -C "$KEY_MARKER"; then
fatal '临时 SSH 密钥生成失败。'
fi
read -r KEY_TYPE KEY_BLOB _ < "${TMP_KEY}.pub"
[[ "$KEY_TYPE" =~ ^[A-Za-z0-9@._+-]+$ ]] || fatal '生成的公钥类型格式异常。'
[[ "$KEY_BLOB" =~ ^[A-Za-z0-9+/=]+$ ]] || fatal '生成的公钥内容格式异常。'
[[ "$KEY_MARKER" =~ ^[A-Za-z0-9._-]+$ ]] || fatal '临时密钥标记格式异常。'
BOOTSTRAP_SSH_OPTS=()
TEMP_SSH_OPTS=()
if (( PORT_WAS_SET == 1 )); then
BOOTSTRAP_SSH_OPTS+=(-p "$REMOTE_PORT")
TEMP_SSH_OPTS+=(-p "$REMOTE_PORT")
fi
BOOTSTRAP_SSH_OPTS+=(
-o BatchMode=no
-o "ConnectTimeout=$CONNECT_TIMEOUT"
-o "ServerAliveInterval=$SERVER_ALIVE_INTERVAL"
-o "ServerAliveCountMax=$SERVER_ALIVE_COUNT_MAX"
)
TEMP_SSH_OPTS+=(
-i "$TMP_KEY"
-o BatchMode=yes
-o IdentitiesOnly=yes
-o "ConnectTimeout=$CONNECT_TIMEOUT"
-o "ServerAliveInterval=$SERVER_ALIVE_INTERVAL"
-o "ServerAliveCountMax=$SERVER_ALIVE_COUNT_MAX"
)
install_temp_key
log_info '正在验证临时密钥登录...'
if ! ssh "${TEMP_SSH_OPTS[@]}" "$REMOTE_CONN" true; then
fatal '临时密钥验证失败; 将尝试自动撤销已安装的临时公钥。'
fi
log_info '正在检查远端 rsync...'
if ! ssh "${TEMP_SSH_OPTS[@]}" "$REMOTE_CONN" 'command -v rsync >/dev/null 2>&1'; then
fatal '远端未找到 rsync 命令。请先在远端安装 rsync。'
fi
if (( PORT_WAS_SET == 1 )); then
printf -v SSH_RSH 'ssh -i %q -p %q -o BatchMode=yes -o IdentitiesOnly=yes -o ConnectTimeout=%q -o ServerAliveInterval=%q -o ServerAliveCountMax=%q' \
"$TMP_KEY" "$REMOTE_PORT" "$CONNECT_TIMEOUT" "$SERVER_ALIVE_INTERVAL" "$SERVER_ALIVE_COUNT_MAX"
else
printf -v SSH_RSH 'ssh -i %q -o BatchMode=yes -o IdentitiesOnly=yes -o ConnectTimeout=%q -o ServerAliveInterval=%q -o ServerAliveCountMax=%q' \
"$TMP_KEY" "$CONNECT_TIMEOUT" "$SERVER_ALIVE_INTERVAL" "$SERVER_ALIVE_COUNT_MAX"
fi
else
log_info '临时密钥已禁用; 正在验证 SSH config/ssh-agent/已有密钥的非交互认证...'
if ! ssh "${EXISTING_SSH_OPTS[@]}" "$REMOTE_CONN" true; then
fatal $'现有 SSH 非交互认证失败。\n\n如果私钥带有 passphrase, 请先运行:\n ssh-add <IdentityFile>\n\n例如:\n ssh-add ~/.ssh/id_ed25519\n\n然后重新运行本脚本。\n\n如果当前只能通过密码完成首次登录, 但服务器允许公钥认证, 且当前用户可以维护 ~/.ssh/authorized_keys, 可显式使用 --temp-key=yes。\n\n如果服务器明确禁用公钥认证, 则 --temp-key=yes 无效; 本脚本无法为该场景提供无人值守的无限重连。'
fi
log_info '正在检查远端 rsync...'
if ! ssh "${EXISTING_SSH_OPTS[@]}" "$REMOTE_CONN" 'command -v rsync >/dev/null 2>&1'; then
fatal '远端未找到 rsync 命令。请先在远端安装 rsync。'
fi
if (( PORT_WAS_SET == 1 )); then
printf -v SSH_RSH 'ssh -p %q -o BatchMode=yes -o ConnectTimeout=%q -o ServerAliveInterval=%q -o ServerAliveCountMax=%q' \
"$REMOTE_PORT" "$CONNECT_TIMEOUT" "$SERVER_ALIVE_INTERVAL" "$SERVER_ALIVE_COUNT_MAX"
else
printf -v SSH_RSH 'ssh -o BatchMode=yes -o ConnectTimeout=%q -o ServerAliveInterval=%q -o ServerAliveCountMax=%q' \
"$CONNECT_TIMEOUT" "$SERVER_ALIVE_INTERVAL" "$SERVER_ALIVE_COUNT_MAX"
fi
fi
log_info "开始 rsync 传输; 远程端点: $REMOTE_CONN, SSH 端口: $SSH_PORT_LABEL。"
run_rsync_with_retry
log_info '✨ 数据传输完成; 正在执行退出清理。'
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
trap 'exit 129' HUP
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment