feat: rclone支持

这个提交包含在:
HA
2026-04-26 21:54:27 +08:00
父节点 91b6375824
当前提交 abeb6f2e80
共有 3 个文件被更改,包括 315 次插入14 次删除

查看文件

@@ -42,6 +42,13 @@ SMB_PASSWORD=""
SMB_DOMAIN=""
SMB_VERSION=""
# rclone 专属
RCLONE_EXECUTABLE=""
RCLONE_REMOTE=""
RCLONE_PATH=""
RCLONE_CONFIG=""
RCLONE_FLAGS=""
# SFTP 专属(预留)
SFTP_HOST=""
SFTP_PORT="22"
@@ -66,6 +73,7 @@ Usage: bash backup.sh <method> [options]
Methods:
smb 通过 SMB / Samba 协议上传
rclone 通过 rclone 上传到网盘等远端(远端名需先用 rclone config 配置好)
sftp 通过 SFTP 上传(暂未实现,已预留入口)
Common options:
@@ -88,6 +96,13 @@ SMB options:
--smb-domain DOMAIN
--smb-version VER 例如 3.0
rclone options:
--rclone-executable F rclone 可执行文件路径(可选,默认从 PATH 查找)
--rclone-remote NAME 远端名(不带冒号),例如 gdrive
--rclone-path PATH 远端目标子路径
--rclone-config FILE 自定义 rclone.conf 路径
--rclone-flags STR 透传给 rclone 的额外参数,如 "--bwlimit 10M --transfers 2"
SFTP options (预留):
--sftp-host HOST
--sftp-port PORT
@@ -100,6 +115,8 @@ Examples:
bash backup.sh smb
bash backup.sh smb --smb-host 192.168.1.10 --smb-share backup --smb-user u --smb-password p
bash backup.sh smb -s "/etc /var/log" -C /path/to/backup.conf
bash backup.sh rclone --rclone-remote gdrive --rclone-path vps-backup/web1
bash backup.sh rclone --rclone-flags "--bwlimit 10M --transfers 2"
bash backup.sh smb --debug
EOF
}
@@ -149,6 +166,13 @@ print_effective_config() {
dbg "SMB_DOMAIN=$SMB_DOMAIN"
dbg "SMB_VERSION=$SMB_VERSION"
;;
rclone)
dbg "RCLONE_EXECUTABLE=$RCLONE_EXECUTABLE"
dbg "RCLONE_REMOTE=$RCLONE_REMOTE"
dbg "RCLONE_PATH=$RCLONE_PATH"
dbg "RCLONE_CONFIG=$RCLONE_CONFIG"
dbg "RCLONE_FLAGS=$RCLONE_FLAGS"
;;
sftp)
dbg "SFTP_HOST=$SFTP_HOST"
dbg "SFTP_PORT=$SFTP_PORT"
@@ -196,7 +220,7 @@ parse_args() {
case "$1" in
-h|--help) usage; exit 0 ;;
smb|sftp) METHOD_ARG="$1"; shift ;;
smb|sftp|rclone) METHOD_ARG="$1"; shift ;;
*) err "未知方式:$1"; usage; exit 1 ;;
esac
dbg "parse_args: METHOD_ARG=$METHOD_ARG,剩余参数($#)=$*"
@@ -234,6 +258,11 @@ parse_args() {
--smb-password) SMB_PASSWORD="$2"; shift 2 ;;
--smb-domain) SMB_DOMAIN="$2"; shift 2 ;;
--smb-version) SMB_VERSION="$2"; shift 2 ;;
--rclone-executable) RCLONE_EXECUTABLE="$2"; shift 2 ;;
--rclone-remote) RCLONE_REMOTE="$2"; shift 2 ;;
--rclone-path) RCLONE_PATH="$2"; shift 2 ;;
--rclone-config) RCLONE_CONFIG="$2"; shift 2 ;;
--rclone-flags) RCLONE_FLAGS="$2"; shift 2 ;;
--sftp-host) SFTP_HOST="$2"; shift 2 ;;
--sftp-port) SFTP_PORT="$2"; shift 2 ;;
--sftp-user) SFTP_USER="$2"; shift 2 ;;
@@ -591,6 +620,192 @@ run_smb() {
cleanup_local
}
# ---------- rclone ----------
rclone_check_deps() {
if [[ -n "$RCLONE_EXECUTABLE" ]]; then
if [[ ! -x "$RCLONE_EXECUTABLE" ]]; then
err "RCLONE_EXECUTABLE 不存在或不可执行:$RCLONE_EXECUTABLE"
return 1
fi
dbg "rclone_check_deps: 使用自定义路径 $RCLONE_EXECUTABLE"
elif command -v rclone >/dev/null 2>&1; then
RCLONE_EXECUTABLE="$(command -v rclone)"
dbg "rclone_check_deps: 从 PATH 找到 $RCLONE_EXECUTABLE"
else
err "未安装 rclonePATH 中找不到,且未指定 RCLONE_EXECUTABLE。安装方式curl https://rclone.org/install.sh | sudo bash"
return 1
fi
dbg "rclone_check_deps: $("$RCLONE_EXECUTABLE" version 2>/dev/null | head -1)"
return 0
}
rclone_validate() {
dbg "rclone_validate: REMOTE='$RCLONE_REMOTE' PATH='$RCLONE_PATH' CONFIG='$RCLONE_CONFIG'"
[[ -z "$RCLONE_REMOTE" ]] && { err "缺少 RCLONE_REMOTE用 \`rclone listremotes\` 查看已配置的远端)"; return 1; }
[[ -z "$RCLONE_PATH" ]] && { err "缺少 RCLONE_PATH"; return 1; }
# 自定义 rclone.conf 路径必须存在
if [[ -n "$RCLONE_CONFIG" && ! -f "$RCLONE_CONFIG" ]]; then
err "RCLONE_CONFIG 指定的文件不存在:$RCLONE_CONFIG"
return 1
fi
# 检查远端是否在 rclone 中已注册
local listremotes
listremotes="$(rclone_cmd listremotes 2>/dev/null)" || {
err "rclone listremotes 执行失败,请检查 rclone 配置"
return 1
}
if ! echo "$listremotes" | grep -qx "${RCLONE_REMOTE}:"; then
err "rclone 中未找到远端 \"${RCLONE_REMOTE}:\"。已配置的远端:"
echo "$listremotes" >&2
err "请先运行rclone config"
return 1
fi
return 0
}
# 包装 rclone,统一使用 RCLONE_EXECUTABLE,并带上 --config如指定和透传的 RCLONE_FLAGS
rclone_cmd() {
local args=()
[[ -n "$RCLONE_CONFIG" ]] && args+=( --config "$RCLONE_CONFIG" )
# RCLONE_FLAGS 是空格分隔的字符串,按词拆分透传
# shellcheck disable=SC2206
local extra=( $RCLONE_FLAGS )
"$RCLONE_EXECUTABLE" "$@" "${args[@]}" "${extra[@]}"
}
rclone_upload() {
dbg "rclone_upload: 待上传=${#ARCHIVE_FILES[@]} 远端目录=${RCLONE_REMOTE}:${RCLONE_PATH%/}/${ARCHIVE_BASENAME}/"
local remote_dir="${RCLONE_REMOTE}:${RCLONE_PATH%/}/${ARCHIVE_BASENAME}"
log "通过 rclone 上传到 ${remote_dir}/"
# 让 rclone 每 5 秒打印一行进度(已传/总量/百分比/速率/ETA,写到 stdout 而不是被 INFO 级别压住。
# --stats-one-line 让进度落在单行;NOTICE 级别能在非交互cron下也输出。
local stats_flags=( --stats=5s --stats-one-line --stats-log-level NOTICE )
local pp pname pkb start_ts elapsed rc
for pp in "${ARCHIVE_FILES[@]}"; do
pname="$(basename "$pp")"
pkb="$(du -k "$pp" 2>/dev/null | awk '{print $1+0}')"
log "上传:$pname ($(awk -v k="$pkb" 'BEGIN{if(k>=1024*1024)printf "%.2fG", k/1024/1024; else if(k>=1024)printf "%.1fM", k/1024; else printf "%dK", k}'))"
start_ts=$(date +%s)
rclone_cmd copyto "${stats_flags[@]}" "$pp" "${remote_dir}/${pname}"
rc=$?
elapsed=$(( $(date +%s) - start_ts ))
if [[ $rc -ne 0 ]]; then
err "rclone 上传失败:$pp (耗时 ${elapsed}s)"
return 1
fi
ok "上传完成:$pname (耗时 ${elapsed}s)"
done
ok "已上传全部 ${#ARCHIVE_FILES[@]} 个文件到 ${remote_dir}/"
# 远端 SHA256 校验:拉取远端 hash,与本地 .sha256 清单比对
rclone_verify_remote "$remote_dir" || return 1
if [[ "$COMMON_RETENTION_DAYS" -gt 0 ]]; then
rclone_retention
else
dbg "rclone_upload: COMMON_RETENTION_DAYS=0,跳过远端清理"
fi
}
rclone_verify_remote() {
local remote_dir="$1"
local sha_local
# 在 ARCHIVE_FILES 中找到 .sha256 清单cleanup 之前)
for f in "${ARCHIVE_FILES[@]}"; do
if [[ "$f" == *.sha256 ]]; then sha_local="$f"; break; fi
done
if [[ -z "$sha_local" || ! -f "$sha_local" ]]; then
warn "未找到本地 SHA256 清单,跳过远端校验"
return 0
fi
log "远端 SHA256 校验:${remote_dir}/"
local remote_sha
remote_sha="$(rclone_cmd hashsum SHA256 "$remote_dir" 2>/dev/null)" || {
err "rclone hashsum 执行失败(远端可能不支持 SHA256,可手动下载后用 sha256sum -c 校验)"
return 1
}
dbg "远端 hash 输出: $remote_sha"
# 本地清单格式: "<hash> <filename>";远端 rclone hashsum 输出同样格式但顺序可能不同
# 逐行比对:以文件名为 key,hash 必须一致
local fname expect actual
while IFS= read -r line; do
[[ -z "$line" ]] && continue
expect="${line%% *}"
fname="${line##* }"
# 跳过清单文件自身(不会出现在 ARCHIVE 主体里,但保险)
[[ "$fname" == "$(basename "$sha_local")" ]] && continue
actual="$(echo "$remote_sha" | awk -v f="$fname" '$2==f{print $1; exit}')"
if [[ -z "$actual" ]]; then
err "远端缺少文件:$fname"
return 1
fi
if [[ "$expect" != "$actual" ]]; then
err "远端 SHA256 不匹配:$fname (本地=$expect 远端=$actual)"
return 1
fi
dbg " 校验通过:$fname"
done < "$sha_local"
ok "远端 SHA256 校验全部通过"
}
rclone_retention() {
dbg "rclone_retention: COMMON_RETENTION_DAYS=$COMMON_RETENTION_DAYS"
local parent="${RCLONE_REMOTE}:${RCLONE_PATH%/}"
local listing
listing="$(rclone_cmd lsf --dirs-only "$parent" 2>/dev/null)" || {
warn "rclone lsf 失败,跳过远端清理"
return 0
}
local cutoff
if date -v-1d >/dev/null 2>&1; then
cutoff=$(date -v-"${COMMON_RETENTION_DAYS}"d +%s)
else
cutoff=$(date -d "-${COMMON_RETENTION_DAYS} days" +%s)
fi
log "清理远端早于 ${COMMON_RETENTION_DAYS} 天的备份目录"
dbg "cutoff=$cutoff parent=$parent"
# rclone lsf --dirs-only 输出形如 "name/",去掉尾随斜杠后做名字匹配
echo "$listing" | sed 's:/$::' | grep -E "^${COMMON_ARCHIVE_PREFIX}-[0-9]{8}-[0-9]{6}\$" | while read -r dir; do
local d="${dir#${COMMON_ARCHIVE_PREFIX}-}"
d="${d%%-*}" # YYYYmmdd
local t="${dir##*-}" # HHMMSS
local iso="${d:0:4}-${d:4:2}-${d:6:2} ${t:0:2}:${t:2:2}:${t:4:2}"
local ftime
if date -j -f "%Y-%m-%d %H:%M:%S" "$iso" +%s >/dev/null 2>&1; then
ftime=$(date -j -f "%Y-%m-%d %H:%M:%S" "$iso" +%s)
else
ftime=$(date -d "$iso" +%s)
fi
dbg " 远端目录 $dir -> ftime=$ftime"
if [[ "$ftime" -lt "$cutoff" ]]; then
log "删除远端旧备份目录:$dir"
if ! rclone_cmd purge "${parent}/${dir}" >/dev/null 2>&1; then
warn "purge 失败:$dir"
fi
fi
done
}
run_rclone() {
dbg "run_rclone: 开始"
rclone_check_deps || return 1
rclone_validate || return 1
print_effective_config
create_archive || return 1
rclone_upload || { cleanup_local; return 1; }
ok "rclone 上传完成"
cleanup_local
}
# ---------- SFTP预留----------
run_sftp() {
@@ -614,9 +829,10 @@ main() {
print_effective_config
case "$METHOD" in
smb) run_smb ;;
sftp) run_sftp ;;
*) err "不支持的方式:$METHOD"; exit 1 ;;
smb) run_smb ;;
rclone) run_rclone ;;
sftp) run_sftp ;;
*) err "不支持的方式:$METHOD"; exit 1 ;;
esac
local rc=$?
dbg "main: 退出码=$rc"