linux_sync_remote
交互式 Rsync 远程同步管理工具,支持任务增删、密码 / 密钥认证、定时同步、批量推送与连接检测。
一键脚本
bash <(curl -sL gitee.com/meimolihan/linux-command_sh/raw/master/linux_sync_remote.sh)
效果预览
脚本源码
#!/bin/bash
set -uo pipefail
gl_hui='\033[38;5;59m'
gl_hong='\033[38;5;9m'
gl_lv='\033[38;5;10m'
gl_huang='\033[38;5;11m'
gl_lan='\033[38;5;32m'
gl_bai='\033[38;5;15m'
gl_zi='\033[38;5;13m'
gl_bufan='\033[38;5;14m'
log_info() { echo -e "${gl_lan}[信息]${gl_bai} $*"; }
log_ok() { echo -e "${gl_lv}[成功]${gl_bai} $*"; }
log_warn() { echo -e "${gl_huang}[警告]${gl_bai} $*"; }
log_error() { echo -e "${gl_hong}[错误]${gl_bai} $*" >&2; }
break_end() {
echo -e "${gl_lv}操作完成${gl_bai}"
echo -e "${gl_bai}按任意键继续${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai} \c"
read -r -n 1 -s -p ""
echo ""
clear
}
exit_script() {
echo ""
echo -ne "${gl_hong}感谢使用,再见!${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.5
echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.6
clear
exit 0
}
sleep_fractional() {
local seconds=$1
if sleep "$seconds" 2>/dev/null; then return 0; fi
if command -v perl >/dev/null 2>&1; then perl -e "select(undef, undef, undef, $seconds)"; return 0; fi
if command -v python3 >/dev/null 2>&1; then python3 -c "import time; time.sleep($seconds)"; return 0; fi
if command -v python >/dev/null 2>&1; then python -c "import time; time.sleep($seconds)"; return 0; fi
local int_seconds=$(echo "$seconds" | awk '{print int($1+0.999)}')
sleep "$int_seconds"
}
exit_animation() {
echo -ne "\r${gl_lv}即将退出 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.5
echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.6
echo ""
}
cancel_return() {
local menu_name="${1:-上一级选单}"
echo -ne "${gl_lv}即将返回 ${gl_huang}${menu_name}${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.5
echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.6
echo ""
clear
}
cancel_empty() {
local menu_name="${1:-上一级选单}"
echo -e "${gl_hong}空输入,返回 ${gl_huang}${menu_name}${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.5
echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.6
echo ""
clear
}
install() {
[[ $# -eq 0 ]] && {
log_error "未提供软件包参数!"
return 1
}
local pkg mgr ver cmd_ver pkg_ver installed=false
for pkg in "$@"; do
installed=false
ver=""
if command -v "$pkg" &>/dev/null; then
cmd_ver=$("$pkg" --version 2>/dev/null | head -n1 | tr -cd '[:print:]' | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1 || echo "")
[[ -n "$cmd_ver" ]] && ver="$cmd_ver"
installed=true
fi
if [[ "$pkg" == "7zip" || "$pkg" == "7z" ]]; then
if command -v 7z &>/dev/null; then
ver=$(7z 2>&1 | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1 || echo "")
[[ -n "$ver" ]] && installed=true
fi
fi
if [[ "$installed" == false ]]; then
if command -v opkg &>/dev/null; then
if opkg list-installed | grep -q "^${pkg} "; then
installed=true
ver=$(opkg list-installed | grep "^${pkg} " | awk '{print $3}' 2>/dev/null || echo "")
fi
elif command -v dpkg-query &>/dev/null; then
if dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "install ok installed"; then
installed=true
ver=$(dpkg-query -W -f='${Version}' "$pkg" 2>/dev/null || echo "")
fi
elif command -v rpm &>/dev/null; then
if rpm -q "$pkg" &>/dev/null; then
installed=true
ver=$(rpm -q --qf '%{VERSION}' "$pkg" 2>/dev/null || echo "")
fi
elif command -v apk &>/dev/null; then
if apk info "$pkg" 2>/dev/null | grep -q "^installed"; then
installed=true
ver=$(apk info -a "$pkg" 2>/dev/null | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1 || echo "")
fi
elif command -v pacman &>/dev/null; then
if pacman -Qi "$pkg" &>/dev/null; then
installed=true
ver=$(pacman -Qi "$pkg" 2>/dev/null | grep -i "version" | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1 || echo "")
fi
fi
fi
if [[ "$installed" == true ]]; then
echo -e "${gl_huang}${pkg}${gl_bai} ${gl_lv}已安装${gl_bai}" \
"$([[ -n "$ver" ]] && echo "版本 ${gl_lv}${ver}${gl_bai}")"
continue
fi
echo -e ""
echo -e "${gl_huang}开始安装:${gl_bai}${pkg}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local install_success=false
for mgr in opkg dnf yum apt apk pacman zypper pkg; do
if ! command -v "$mgr" &>/dev/null; then
continue
fi
case $mgr in
opkg)
echo -e "${gl_bai}使用包管理器: ${gl_zi}opkg (OpenWrt/iStoreOS)${gl_bai}"
if [[ "$pkg" == "7zip" || "$pkg" == "7z" ]]; then
echo -e "${gl_bai}正在安装: ${gl_lv}p7zip${gl_bai}"
opkg update && opkg install p7zip && install_success=true
else
opkg update && opkg install "$pkg" && install_success=true
fi
;;
dnf)
echo -e "${gl_bai}使用包管理器: ${gl_zi}dnf (Fedora/RHEL)${gl_bai}"
dnf -y update && dnf install -y "$pkg" && install_success=true
;;
yum)
echo -e "${gl_bai}使用包管理器: ${gl_zi}yum (CentOS/RHEL)${gl_bai}"
yum -y update && yum install -y "$pkg" && install_success=true
;;
apt)
echo -e "${gl_bai}使用包管理器: ${gl_zi}apt (Debian/Ubuntu)${gl_bai}"
apt update -y && apt install -y "$pkg" && install_success=true
;;
apk)
echo -e "${gl_bai}使用包管理器: ${gl_zi}apk (Alpine)${gl_bai}"
apk update && apk add "$pkg" && install_success=true
;;
pacman)
echo -e "${gl_bai}使用包管理器: ${gl_zi}pacman (Arch/Manjaro)${gl_bai}"
pacman -Syu --noconfirm && pacman -S --noconfirm "$pkg" && install_success=true
;;
zypper)
echo -e "${gl_bai}使用包管理器: ${gl_zi}zypper (openSUSE)${gl_bai}"
zypper refresh && zypper install -y "$pkg" && install_success=true
;;
pkg)
echo -e "${gl_bai}使用包管理器: ${gl_zi}pkg (FreeBSD)${gl_bai}"
pkg update && pkg install -y "$pkg" && install_success=true
;;
esac
[[ "$install_success" == true ]] && break
done
if [[ "$install_success" == true ]]; then
echo -e "${gl_lv}✓ ${pkg} 安装成功${gl_bai}"
else
echo -e "${gl_hong}✗ ${pkg} 安装失败${gl_bai}"
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
done
}
list_tasks() {
echo "已保存的远程同步任务:"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if [[ ! -f "$CONFIG_FILE" ]]; then
echo -e "${gl_huang}配置文件不存在,暂无同步任务${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
return 0
fi
if [[ ! -s "$CONFIG_FILE" ]]; then
echo -e "${gl_huang}暂无同步任务${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
return 0
fi
local line_num=1
while IFS='|' read -r name local_path remote remote_path port options auth_method password_or_key; do
printf "${gl_bufan}%3d. ${gl_bai}- ${gl_zi}%-10s${gl_bai} ( ${gl_huang}%s${gl_bai} -> ${gl_bufan}%s${gl_bai}:${gl_lv}%s${gl_bai} )\n" \
"$line_num" "$name" "$local_path" "$remote" "$remote_path"
((line_num++))
done <"$CONFIG_FILE"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
}
remote_add_task() {
clear
echo ""
echo -e "${gl_huang}>>> 创建新任务 (远程同步)${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo "创建新同步任务示例:"
echo -e " ${gl_bai}- 任务名称: ${gl_huang}backup_www${gl_bai}"
echo -e " ${gl_bai}- 本地目录: ${gl_huang}/var/www${gl_bai}"
echo -e " ${gl_bai}- 远程地址: ${gl_huang}user@192.168.1.100${gl_bai}"
echo -e " ${gl_bai}- 远程目录: ${gl_huang}/backup/www${gl_bai}"
echo -e " ${gl_bai}- 端口号 : ${gl_huang}(默认 22)${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "请输入任务名称 (${gl_huang}0${gl_bai}返回): ")" name
[[ -z "$name" ]] && { cancel_empty "上一级选单"; return 1; }
[[ "$name" == "0" ]] && { cancel_return "上一级选单"; return 1; }
read -r -e -p "$(echo -e "请输入本地目录 (${gl_huang}0${gl_bai}返回): ")" local_path
[[ -z "$local_path" ]] && { cancel_empty "上一级选单"; return 1; }
[[ "$local_path" == "0" ]] && { cancel_return "上一级选单"; return 1; }
read -r -e -p "$(echo -e "请输入远程用户@IP (${gl_huang}0${gl_bai}返回): ")" remote
[[ -z "$remote" ]] && { cancel_empty "上一级选单"; return 1; }
[[ "$remote" == "0" ]] && { cancel_return "上一级选单"; return 1; }
read -r -e -p "$(echo -e "请输入远程目录 (${gl_huang}0${gl_bai}返回): ")" remote_path
[[ -z "$remote_path" ]] && { cancel_empty "上一级选单"; return 1; }
[[ "$remote_path" == "0" ]] && { cancel_return "上一级选单"; return 1; }
read -r -e -p "$(echo -e "请输入 SSH 端口 (默认 22, ${gl_huang}0${gl_bai}返回): ")" port
[[ "$port" == "0" ]] && { cancel_return "上一级选单"; return 1; } # break 或 continue 或 return ,视上下文而定
port=${port:-22}
local_path="${local_path%/}/"
remote_path="${remote_path%/}/"
echo ""
echo -e "${gl_huang}>>> 请选择身份验证方式${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bufan}1. ${gl_bai}密码"
echo -e "${gl_bufan}2. ${gl_bai}密钥"
echo -e "${gl_bufan}0. ${gl_bai}返回"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "请输入你的选择: " auth_choice
[[ -z "$auth_choice" ]] && { cancel_empty "上一级选单"; continue; }
[[ "$auth_choice" == "0" ]] && { cancel_return "上一级选单"; continue; }
case $auth_choice in
1)
read -r -s -p "请输入密码 (输入0并回车可返回): " password_or_key
echo
if [[ "$password_or_key" == "0" ]]; then return; fi
auth_method="password"
;;
2)
echo "请粘贴密钥内容 (粘贴完成后按两次回车,输入0并回车可返回):"
IFS= read -r first_line
if [[ "$first_line" == "0" ]]; then return; fi
local password_or_key="$first_line"$'\n'
while IFS= read -r line; do
if [[ -z "$line" && "$password_or_key" == *"-----BEGIN"* ]]; then
break
fi
if [[ -n "$line" || "$password_or_key" == *"-----BEGIN"* ]]; then
password_or_key+="${line}"$'\n'
fi
done
if [[ "$password_or_key" == *"-----BEGIN"* && "$password_or_key" == *"PRIVATE KEY-----"* ]]; then
local key_file="$KEY_DIR/${name}_sync.key"
echo -n "$password_or_key" >"$key_file"
chmod 600 "$key_file"
password_or_key="$key_file"
auth_method="key"
else
echo -e "${gl_hong}无效的密钥内容!${gl_bai}"
exit_animation
return
fi
;;
*)
echo -e "${gl_hong}无效的选择!${gl_bai}"
exit_animation
return
;;
esac
echo ""
echo -e "${gl_huang}>>> 请选择同步模式${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bufan}1. ${gl_bai}标准模式 (-avz)"
echo -e "${gl_bufan}2. ${gl_bai}删除目标文件 (-avz --progress --delete-delay)"
echo -e "${gl_bufan}0. ${gl_bai}返回"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "请输入你的选择: " mode
if [[ "$mode" == "0" ]]; then return; fi
case $mode in
1) options="-avz" ;;
2) options="-avz --progress --delete-delay" ;;
*)
echo "无效选择,使用默认 -avz"
exit_animation
options="-avz"
;;
esac
echo "$name|$local_path|$remote|$remote_path|$port|$options|$auth_method|$password_or_key" >>"$CONFIG_FILE"
echo -e "${gl_lv}任务已保存!${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
}
remote_delete_task() {
echo ""
echo -e "${gl_zi}>>> 删除远程同步任务${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}请输入要删除的任务编号(${gl_huang}0${gl_bai}返回): ")" num
[ "$num" = "0" ] && { cancel_return "Rsync远程同步工具"; return 1; }
[ -z "$num" ] && { cancel_empty "上一级选单"; return 1; }
local task
task=$(sed -n "${num}p" "$CONFIG_FILE")
if [[ -z "$task" ]]; then
echo -e "${gl_hong}错误:未找到对应的任务。${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
exit_animation
return
fi
IFS='|' read -r -r name local_path remote remote_path port options auth_method password_or_key <<<"$task"
if [[ "$auth_method" == "key" && "$password_or_key" == "$KEY_DIR"* ]]; then
rm -f "$password_or_key"
fi
sed -i "${num}d" "$CONFIG_FILE"
echo -e "${gl_lv}任务已删除!${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
}
remote_run_task() {
echo ""
echo -e "${gl_zi}>>> 执行同步任务${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
CONFIG_FILE="$HOME/.remote_rsync_tasks"
CRON_FILE="$HOME/.remote_rsync_cron"
local direction="push"
local num
if [[ "$1" == "push" || "$1" == "pull" ]]; then
direction="$1"
num="$2"
else
num="$1"
fi
read -r -e -p "$(echo -e "${gl_bai}请输入要执行的任务编号(${gl_huang}0${gl_bai}返回): ")" num
[ "$num" = "0" ] && { cancel_return "Rsync远程同步工具"; return 1; }
[ -z "$num" ] && { cancel_empty "上一级选单"; return 1; }
local task=$(sed -n "${num}p" "$CONFIG_FILE")
if [[ -z "$task" ]]; then
echo -e "${gl_hong}错误: 未找到该任务!${gl_bai}"
return
fi
IFS='|' read -r name local_path remote remote_path port options auth_method password_or_key <<<"$task"
if [[ "$direction" == "pull" ]]; then
echo -e ""
echo -e "${gl_zi}>>> 正在拉取同步到本地: ${gl_huang}$remote:$remote_path ${gl_bai}-> ${gl_lv}$local_path${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
source="$remote:$remote_path"
destination="$local_path"
else
echo -e ""
echo -e "${gl_zi}>>> 正在推送同步到远端: ${gl_huang}$local_path ${gl_bai}-> ${gl_lv}$remote:$remote_path${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
source="$local_path"
destination="$remote:$remote_path"
fi
local ssh_options="-p $port -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
if [[ "$auth_method" == "password" ]]; then
if ! command -v sshpass &>/dev/null; then
echo -e "${gl_hong}错误:未安装 ${gl_huang}sshpass${gl_hong},请先安装 ${gl_huang}sshpass${gl_hong}。${gl_bai}"
echo "安装方法:"
echo -e " - ${gl_huang}Ubuntu/Debian: ${gl_lv}apt install sshpass${gl_bai}"
echo -e " - ${gl_huang}CentOS/RHEL: ${gl_lv}yum install sshpass${gl_bai}"
exit_animation
return
fi
sshpass -p "$password_or_key" rsync $options -e "ssh $ssh_options" "$source" "$destination"
else
if [[ ! -f "$password_or_key" ]]; then
echo -e "${gl_hong}错误:密钥文件不存在:${gl_huang}$password_or_key${gl_bai}"
exit_animation
return
fi
if [[ "$(stat -c %a "$password_or_key")" != "600" ]]; then
echo -e "${gl_huang}警告:密钥文件权限不正确,正在修复${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
chmod 600 "$password_or_key"
fi
rsync $options -e "ssh -i $password_or_key $ssh_options" "$source" "$destination"
fi
if [[ $? -eq 0 ]]; then
echo -e "${gl_lv}同步完成!${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
else
echo -e ""
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_hong}同步失败! 请检查以下内容:${gl_bai}"
echo -e "${gl_bufan}1. ${gl_bai}网络连接是否正常"
echo -e "${gl_bufan}2. ${gl_bai}远程主机是否可访问"
echo -e "${gl_bufan}3. ${gl_bai}认证信息是否正确"
echo -e "${gl_bufan}4. ${gl_bai}本地和远程目录是否有正确的访问权限"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
fi
}
remote_schedule_task() {
echo -e ""
echo -e "${gl_zi}>>> 创建远程定时任务${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}请输入要定时同步的任务编号(${gl_huang}0${gl_bai}返回): ")" num
[ "$num" = "0" ] && { cancel_return "Rsync远程同步工具"; return 1; }
[ -z "$num" ] && { cancel_empty "上一级选单"; return 1; }
if ! [[ "$num" =~ ^[0-9]+$ ]]; then
echo "错误: 请输入有效的任务编号!"
exit_animation
return
fi
local task_name=""
if [[ -f "$CONFIG_FILE" ]]; then
task_name=$(sed -n "${num}p" "$CONFIG_FILE" 2>/dev/null | cut -d'|' -f1)
fi
if [[ -z "$task_name" ]]; then
echo -e "${gl_hong}错误: 未找到编号为 ${gl_huang}$num ${gl_hong}的任务!${gl_bai}"
exit_animation
return
fi
echo ""
echo -e "${gl_huang}>>> 请选择定时执行间隔${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bufan}1. ${gl_bai}每小时执行一次"
echo -e "${gl_bufan}2. ${gl_bai}每天执行一次"
echo -e "${gl_bufan}3. ${gl_bai}每周执行一次"
echo -e "${gl_bufan}4. ${gl_bai}每月执行一次"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "请输入你的选项: " interval
local random_minute
random_minute=$(shuf -i 0-59 -n 1)
local cron_time=""
case "$interval" in
1) cron_time="$random_minute * * * *" ;; # 每小时,随机分钟执行
2) cron_time="$random_minute 0 * * *" ;; # 每天,随机分钟执行
3) cron_time="$random_minute 0 * * 1" ;; # 每周,随机分钟执行
4) cron_time="$random_minute 4 1 * *" ;; # 每月1号凌晨4点,随机分钟执行
*)
echo "错误: 请输入有效的选项!"
exit_animation
return
;;
esac
local cron_job="$cron_time m remote_rsync_run $num"
if crontab -l | grep -q "m remote_rsync_run $num"; then
echo -e "${gl_hong}错误: 该任务的定时同步已存在!${gl_bai}"
exit_animation
return
fi
(
crontab -l 2>/dev/null
echo "# 远程Rsync定时任务: $task_name"
echo "$cron_job"
echo ""
) | crontab -
echo -e "${gl_lv}✓ 定时任务已创建:${gl_huang}$cron_job${gl_bai}"
echo -e "${gl_bai}任务名称: ${gl_huang}$task_name${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
}
view_tasks() {
echo "当前的定时任务:"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if ! crontab -l >/dev/null 2>&1; then
echo -e "${gl_huang}当前用户暂无定时任务${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
return 0
fi
local crontab_content
crontab_content=$(crontab -l 2>/dev/null)
local task_count=0
while IFS= read -r line; do
[[ -z "$line" ]] && continue
if [[ "$line" == *"m remote_rsync_run"* ]]; then
((task_count++))
local task_num=$(echo "$line" | grep -oE "m remote_rsync_run [0-9]+" | awk '{print $3}')
local task_name=""
local local_path=""
local remote=""
local remote_path=""
local options=""
local prev_line=$(echo "$crontab_content" | sed -n "/^#.*远程Rsync定时任务:.*任务编号: $task_num\$/p" | head -1)
if [[ -n "$prev_line" ]]; then
task_name=$(echo "$prev_line" | sed -n 's/^#.*远程Rsync定时任务: \(.*\) (任务编号: [0-9]*)$/\1/p')
fi
if [[ -z "$task_name" ]] && [[ -n "$task_num" ]] && [[ -f "$CONFIG_FILE" ]]; then
local task_line=$(sed -n "${task_num}p" "$CONFIG_FILE" 2>/dev/null)
if [[ -n "$task_line" ]]; then
IFS='|' read -r task_name local_path remote remote_path port options auth_method password_or_key <<<"$task_line"
fi
fi
echo -e "${gl_bufan}${task_count}. ${gl_bai}Cron表达式: ${gl_zi}${line}${gl_bai}"
if [[ -n "$task_name" ]]; then
echo -e " 任务名称: ${gl_huang}${task_name}${gl_bai}"
echo -e " 任务编号: ${gl_huang}${task_num}${gl_bai}"
if [[ -n "$local_path" ]]; then
echo -e " 本地目录: ${gl_huang}${local_path}${gl_bai}"
echo -e " 远程地址: ${gl_lv}${remote}:${remote_path}${gl_bai}"
echo -e " 端口号: ${gl_bai}${port}${gl_bai}"
echo -e " 同步选项: ${gl_zi}${options}${gl_bai}"
echo -e " 认证方式: ${gl_huang}${auth_method}${gl_bai}"
fi
else
echo -e " ${gl_hong}警告: 无法获取任务详细信息(可能对应的同步任务已被删除)${gl_bai}"
fi
echo ""
fi
done <<<"$crontab_content"
if [[ $task_count -eq 0 ]]; then
echo -e "${gl_huang}暂无定时同步任务${gl_bai}"
else
echo -e "${gl_bai}共找到 ${gl_huang}${task_count} ${gl_bai}个定时任务${gl_bai}"
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
}
remote_delete_task_schedule() {
echo ""
echo -e "${gl_zi}>>> 删除定时任务${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local crontab_content
crontab_content=$(crontab -l 2>/dev/null)
if [[ $? -ne 0 ]] || [[ -z "$crontab_content" ]]; then
echo -e "${gl_huang}当前用户暂无定时任务${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
exit_animation
return 0
fi
local remote_task_count=0
while IFS= read -r line; do
[[ -z "$line" ]] && continue
if [[ "$line" == *"m remote_rsync_run"* ]]; then
((remote_task_count++))
fi
done <<<"$crontab_content"
if [[ $remote_task_count -eq 0 ]]; then
echo -e "${gl_huang}暂无远程同步定时任务,无需删除。${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
exit_animation
return 0
fi
echo -e "${gl_bai}提示: 请输入要删除的任务在列表中的序号 (1~${remote_task_count})${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}请输入要删除的任务编号(${gl_huang}0${gl_bai}返回): ")" num
[ "$num" = "0" ] && { cancel_return "Rsync远程同步工具"; return 1; }
[ -z "$num" ] && { cancel_empty "上一级选单"; return 1; }
if ! [[ "$num" =~ ^[0-9]+$ ]]; then
echo "错误: 请输入有效的任务编号!"
exit_animation
return
fi
if [[ "$num" -lt 1 ]] || [[ "$num" -gt "$remote_task_count" ]]; then
echo -e "${gl_hong}错误: 请输入有效的任务编号 (1~${remote_task_count})!${gl_bai}"
exit_animation
return
fi
local current_task=0
local target_line=""
local target_comment_line=""
local line_num=0
while IFS= read -r line; do
((line_num++))
[[ -z "$line" ]] && continue
if [[ "$line" == *"m remote_rsync_run"* ]]; then
((current_task++))
if [[ $current_task -eq $num ]]; then
target_line="$line"
if [[ $line_num -gt 1 ]]; then
local prev_line_num=$((line_num - 1))
local prev_line=$(echo "$crontab_content" | sed -n "${prev_line_num}p")
if [[ "$prev_line" =~ ^#.*远程Rsync定时任务 ]]; then
target_comment_line="$prev_line"
fi
fi
break
fi
fi
done <<<"$crontab_content"
if [[ -z "$target_line" ]]; then
echo -e "${gl_hong}错误: 无法找到对应的定时任务${gl_bai}"
exit_animation
return
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bai}即将删除的任务:${gl_bai}"
if [[ -n "$target_comment_line" ]]; then
echo -e " ${gl_huang}注释行: $target_comment_line${gl_bai}"
fi
echo -e " ${gl_zi}任务行: $target_line${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}确认删除吗? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" confirm
[ "$confirm" = "0" ] && { cancel_return "上一级选单"; return 1; }
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
echo -e "${gl_huang}取消删除操作${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
exit_animation
return
fi
local temp_file
temp_file=$(mktemp)
if [[ -n "$target_comment_line" ]]; then
crontab -l 2>/dev/null | grep -vF "$target_comment_line" | grep -vF "$target_line" > "$temp_file"
else
crontab -l 2>/dev/null | grep -vF "$target_line" > "$temp_file"
fi
crontab "$temp_file"
rm -f "$temp_file"
echo -e "${gl_lv}定时任务已删除!${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
}
remote_show_task_details() {
echo ""
echo -e "${gl_zi}>>> 查看远程任务详细信息${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if [[ ! -f "$CONFIG_FILE" ]]; then
echo -e "${gl_huang}配置文件不存在,暂无同步任务${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
exit_animation
return 0
fi
if [[ ! -s "$CONFIG_FILE" ]]; then
echo -e "${gl_huang}暂无同步任务${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
exit_animation
return 0
fi
echo -e "${gl_bai}提示: 直接${gl_lv}回车${gl_bai}查看全部任务,输入${gl_huang}0${gl_bai}返回${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "请输入要查看的任务编号: " num
[ "$num" = "0" ] && { cancel_return "Rsync远程同步工具"; return 1; }
if [[ -z "$num" ]]; then
echo ""
echo -e "${gl_zi}>>> 所有同步任务详情${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local line_num=1
while IFS='|' read -r name local_path remote remote_path port options auth_method password_or_key; do
echo -e "${gl_bufan}任务 #${line_num}${gl_bai}"
echo -e " ${gl_bai}任务名称: ${gl_zi}$name${gl_bai}"
echo -e " ${gl_bai}本地目录: ${gl_huang}$local_path${gl_bai}"
echo -e " ${gl_bai}远程地址: ${gl_bufan}$remote${gl_bai}"
echo -e " ${gl_bai}远程目录: ${gl_lv}$remote_path${gl_bai}"
echo -e " ${gl_bai}SSH 端口: ${gl_bai}$port"
echo -e " ${gl_bai}同步选项: ${gl_zi}$options${gl_bai}"
echo -e " ${gl_bai}认证方式: ${gl_huang}$auth_method${gl_bai}"
if [[ "$auth_method" == "password" ]]; then
echo -e " ${gl_bai}密码: ${gl_hong}****** (已加密)${gl_bai}"
elif [[ "$auth_method" == "key" ]]; then
echo -e " ${gl_bai}密钥文件: ${gl_lv}$password_or_key${gl_bai}"
if [[ -f "$password_or_key" ]]; then
local key_perms=$(stat -c "%a" "$password_or_key" 2>/dev/null)
echo -e " ${gl_bai}密钥权限: ${gl_bai}${key_perms}"
if [[ "$key_perms" != "600" ]]; then
echo -e " ${gl_hong}警告: 密钥文件权限不安全!${gl_bai}"
fi
else
echo -e " ${gl_hong}警告: 密钥文件不存在!${gl_bai}"
fi
fi
if [[ -d "$local_path" ]]; then
local local_size=$(du -sh "$local_path" 2>/dev/null | cut -f1)
echo -e " ${gl_bai}本地目录大小: ${gl_bai}${local_size:-未知}"
else
echo -e " ${gl_hong}警告: 本地目录不存在!${gl_bai}"
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
((line_num++))
done <"$CONFIG_FILE"
echo -e "${gl_lv}共找到 $((line_num-1)) 个同步任务${gl_bai}"
else
local task=$(sed -n "${num}p" "$CONFIG_FILE" 2>/dev/null)
if [[ -z "$task" ]]; then
echo -e "${gl_hong}错误: 未找到任务 #$num${gl_bai}"
exit_animation
return
fi
IFS='|' read -r name local_path remote remote_path port options auth_method password_or_key <<<"$task"
echo ""
echo -e "${gl_zi}>>> 任务 #$num 详情${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e " ${gl_bai}任务名称: ${gl_zi}$name${gl_bai}"
echo -e " ${gl_bai}本地目录: ${gl_huang}$local_path${gl_bai}"
echo -e " ${gl_bai}远程地址: ${gl_bufan}$remote${gl_bai}"
echo -e " ${gl_bai}远程目录: ${gl_lv}$remote_path${gl_bai}"
echo -e " ${gl_bai}SSH 端口: ${gl_bai}$port"
echo -e " ${gl_bai}同步选项: ${gl_zi}$options${gl_bai}"
echo -e " ${gl_bai}认证方式: ${gl_huang}$auth_method${gl_bai}"
if [[ "$auth_method" == "password" ]]; then
echo -e " ${gl_bai}密码: ${gl_hong}****** (已加密)${gl_bai}"
elif [[ "$auth_method" == "key" ]]; then
echo -e " ${gl_bai}密钥文件: ${gl_lv}$password_or_key${gl_bai}"
if [[ -f "$password_or_key" ]]; then
local key_perms=$(stat -c "%a" "$password_or_key" 2>/dev/null)
echo -e " ${gl_bai}密钥权限: ${gl_bai}${key_perms}"
if [[ "$key_perms" != "600" ]]; then
echo -e " ${gl_hong}警告: 密钥文件权限不安全! 建议执行:${gl_bai}"
echo -e " ${gl_bai}chmod 600 \"$password_or_key\"${gl_bai}"
fi
echo -e " ${gl_bai}密钥文件信息:${gl_bai}"
if file "$password_or_key" | grep -q "PEM RSA private key"; then
echo -e " ${gl_bai} - 类型: RSA 私钥"
elif file "$password_or_key" | grep -q "PEM EC private key"; then
echo -e " ${gl_bai} - 类型: EC 私钥"
elif file "$password_or_key" | grep -q "PEM DSA private key"; then
echo -e " ${gl_bai} - 类型: DSA 私钥"
elif file "$password_or_key" | grep -q "OpenSSH private key"; then
echo -e " ${gl_bai} - 类型: OpenSSH 私钥"
else
echo -e " ${gl_bai} - 类型: 未知"
fi
else
echo -e " ${gl_hong}错误: 密钥文件不存在!${gl_bai}"
fi
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e " ${gl_bai}本地目录检查:${gl_bai}"
if [[ -d "$local_path" ]]; then
local local_size=$(du -sh "$local_path" 2>/dev/null | cut -f1)
local file_count=$(find "$local_path" -type f 2>/dev/null | wc -l)
echo -e " ${gl_bai} - 存在: ${gl_lv}是${gl_bai}"
echo -e " ${gl_bai} - 大小: ${gl_bai}${local_size:-未知}"
echo -e " ${gl_bai} - 文件数: ${gl_bai}${file_count}"
else
echo -e " ${gl_bai} - 存在: ${gl_hong}否${gl_bai}"
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e " ${gl_bai}连接测试:${gl_bai}"
echo -e " ${gl_bai}正在测试 SSH 连接${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
local ssh_test_cmd=""
if [[ "$auth_method" == "password" ]]; then
if command -v sshpass &>/dev/null; then
ssh_test_cmd="sshpass -p '$password_or_key' ssh -p $port -o StrictHostKeyChecking=no -o ConnectTimeout=5 $remote 'echo connected' 2>&1"
else
echo -e " ${gl_hong} - sshpass 未安装,无法测试密码连接${gl_bai}"
fi
else
ssh_test_cmd="ssh -i '$password_or_key' -p $port -o StrictHostKeyChecking=no -o ConnectTimeout=5 $remote 'echo connected' 2>&1"
fi
if [[ -n "$ssh_test_cmd" ]]; then
local ssh_output
ssh_output=$(eval "$ssh_test_cmd" 2>&1)
if echo "$ssh_output" | grep -q "connected"; then
echo -e " ${gl_bai} - SSH 连接: ${gl_lv}成功${gl_bai}"
else
echo -e " ${gl_bai} - SSH 连接: ${gl_hong}失败${gl_bai}"
echo -e " ${gl_bai} - 错误信息: ${gl_hong}$ssh_output${gl_bai}"
fi
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e " ${gl_bai}执行同步命令:${gl_bai}"
if [[ "$auth_method" == "password" ]]; then
if command -v sshpass &>/dev/null; then
echo -e " ${gl_bai} - 推送到远端: ${gl_zi}sshpass -p '******' rsync $options -e \"ssh -p $port -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" \"$local_path\" \"$remote:$remote_path\"${gl_bai}"
echo -e " ${gl_bai} - 从远端拉取: ${gl_zi}sshpass -p '******' rsync $options -e \"ssh -p $port -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" \"$remote:$remote_path\" \"$local_path\"${gl_bai}"
else
echo -e " ${gl_hong} - sshpass 未安装,请先安装${gl_bai}"
fi
else
echo -e " ${gl_bai} - 推送到远端: ${gl_zi}rsync $options -e \"ssh -i '$password_or_key' -p $port -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" \"$local_path\" \"$remote:$remote_path\"${gl_bai}"
echo -e " ${gl_bai} - 从远端拉取: ${gl_zi}rsync $options -e \"ssh -i '$password_or_key' -p $port -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" \"$remote:$remote_path\" \"$local_path\"${gl_bai}"
fi
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
}
remote_run_all_tasks_push() {
echo ""
echo -e "${gl_huang}>>> 批量推送所有任务(推送到远端)${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if [[ ! -f "$CONFIG_FILE" ]]; then
echo -e "${gl_huang}配置文件不存在,暂无同步任务${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
exit_animation
return 0
fi
if [[ ! -s "$CONFIG_FILE" ]]; then
echo -e "${gl_huang}暂无同步任务${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
exit_animation
return 0
fi
local total_tasks=$(wc -l <"$CONFIG_FILE")
echo -e "${gl_bai}找到 ${gl_huang}${total_tasks} ${gl_bai}个同步任务${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_hong}⚠ 警告:即将批量执行所有任务,这可能会花费较长时间${gl_bai}"
echo -e "${gl_huang}请确保网络连接稳定,且远程服务器可访问${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}确认执行批量推送吗? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" confirm
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
echo -e "${gl_huang}已取消批量执行${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
exit_animation
return
fi
echo -e "${gl_bai}开始批量执行推送任务${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local success_count=0
local failed_count=0
local current_task=0
while IFS='|' read -r name local_path remote remote_path port options auth_method password_or_key; do
((current_task++))
echo ""
echo -e "${gl_bai}[${gl_lv}${current_task}${gl_bai}/${gl_huang}${total_tasks}${gl_bai}] 执行任务: ${gl_huang}${name}${gl_bai}"
echo -e "${gl_bai}同步方向: ${gl_huang}${local_path} ${gl_hong}-> ${gl_lv}${remote}:${remote_path}${gl_bai}"
if [[ ! -d "$local_path" ]]; then
echo -e "${gl_hong}✗ 失败: 本地目录不存在${gl_bai}"
((failed_count++))
exit_animation
continue
fi
if [[ "$auth_method" == "password" ]] && ! command -v sshpass &>/dev/null; then
echo -e "${gl_hong}✗ 失败: sshpass 未安装${gl_bai}"
((failed_count++))
exit_animation
continue
fi
if [[ "$auth_method" == "key" ]] && [[ ! -f "$password_or_key" ]]; then
echo -e "${gl_hong}✗ 失败: 密钥文件不存在${gl_bai}"
((failed_count++))
exit_animation
continue
fi
local ssh_options="-p $port -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
local output_file
output_file=$(mktemp)
local sync_result=0
if [[ "$auth_method" == "password" ]]; then
sshpass -p "$password_or_key" rsync $options -e "ssh $ssh_options" "$local_path" "$remote:$remote_path" >"$output_file" 2>&1
sync_result=$?
else
if [[ "$(stat -c %a "$password_or_key")" != "600" ]]; then
chmod 600 "$password_or_key" 2>/dev/null
fi
rsync $options -e "ssh -i $password_or_key $ssh_options" "$local_path" "$remote:$remote_path" >"$output_file" 2>&1
sync_result=$?
fi
if [[ $sync_result -eq 0 ]]; then
echo -e "${gl_lv}✓ 推送成功${gl_bai}"
((success_count++))
else
echo -e "${gl_hong}✗ 推送失败 (错误代码: $sync_result)${gl_bai}"
echo -e "${gl_bai}错误信息:"
tail -5 "$output_file" 2>/dev/null || echo "无法获取错误信息"
((failed_count++))
fi
rm -f "$output_file"
done <"$CONFIG_FILE"
echo ""
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bai}批量执行完成:${gl_bai}"
echo -e " ${gl_lv}✓ 成功: ${success_count}${gl_bai}"
echo -e " ${gl_hong}✗ 失败: ${failed_count}${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if [[ $failed_count -eq 0 ]]; then
echo -e "${gl_lv}✓ 所有任务执行成功!${gl_bai}"
elif [[ $success_count -eq 0 ]]; then
echo -e "${gl_hong}✗ 所有任务执行失败!${gl_bai}"
else
echo -e "${gl_huang}⚠ 部分任务执行失败,请检查错误信息${gl_bai}"
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
if [[ $failed_count -gt 0 ]]; then
return 1
fi
return 0
}
remote_rsync_manager() {
CONFIG_FILE="$HOME/.remote_rsync_tasks"
CRON_FILE="$HOME/.remote_rsync_cron"
install sshpass rsync
while true; do
clear
list_tasks
echo
view_tasks
echo -e ""
echo -e "${gl_zi}>>> Rsync远程同步工具${gl_bai}"
echo "远程目录之间同步,支持增量同步,高效稳定。"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bufan}1. ${gl_bai}创建新任务 ${gl_bufan}2. ${gl_bai}查看任务详情"
echo -e "${gl_bufan}3. ${gl_bai}执行本地同步到远端 ${gl_bufan}4. ${gl_bai}执行远端同步到本地"
echo -e "${gl_bufan}5. ${gl_bai}批量推送所有任务 ${gl_bufan}6. ${gl_bai}删除同步任务"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bufan}7. ${gl_bai}创建定时任务 ${gl_bufan}8. ${gl_bai}删除定时任务"
echo -e "${gl_bufan}9. ${gl_bai}编辑cron文件 ${gl_bufan}10. ${gl_bai}查看cron文件"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_hong}0. ${gl_bai}退出脚本"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "请输入你的选择: " choice
case $choice in
1) remote_add_task ;;
2) remote_show_task_details ;;
3) remote_run_task push ;;
4) remote_run_task pull ;;
5) remote_run_all_tasks_push ;;
6) remote_delete_task ;;
7) remote_schedule_task ;;
8) remote_delete_task_schedule ;;
9) edit_crontab; continue ;;
10) view_raw_crontab ;;
0) exit_script ;;
*) handle_invalid_input ;;
esac
done
}
remote_rsync_manager
创建本地脚本
new_script="new_test.sh"
cat > "$new_script" << 'EOF'
#!/bin/bash
# 粘贴脚本源码
EOF
# 保留本地脚本,去掉 rm -f "$new_script"
chmod +x "$new_script" && ./"$new_script" && rm -f "$new_script"