feat: 更新

这个提交包含在:
HA
2026-04-26 23:56:39 +08:00
父节点 8d03fbac89
当前提交 99c9c69862
共有 2 个文件被更改,包括 70 次插入100 次删除

查看文件

@@ -8,7 +8,7 @@
- bash 3.2+ / tar / date / split
- `sha256sum`Linux 自带,属于 coreutils`shasum`macOS 自带)
- 上传 SMB`smbclient`Linux 一般在 `smbclient``samba-client` 包中;macOS 推荐 `brew install samba`没装会回退到 `mount_smbfs`
- 上传 SMB`smbclient`Linux 一般在 `smbclient``samba-client` 包中;macOS 必须 `brew install samba`脚本不再回退到 `mount_smbfs`
- 上传 rclone`rclone`,且需提前用 `rclone config` 配好远端
### 安装依赖
@@ -18,7 +18,7 @@
| Debian / Ubuntu | `apt install -y smbclient` | `apt install -y rclone` 或官方脚本 |
| RHEL / Rocky / Alma | `dnf install -y samba-client` | `dnf install -y rclone` 或官方脚本 |
| Arch | `pacman -S smbclient` | `pacman -S rclone` |
| macOS | `brew install samba`可选 | `brew install rclone` |
| macOS | `brew install samba`必需 | `brew install rclone` |
| 通用 | — | `curl https://rclone.org/install.sh \| sudo bash` |
## 文件
@@ -345,7 +345,7 @@ tar -xzf backup-20260426-031000.tar.gz
## 注意事项
- 命令行密码会出现在进程列表中,安全敏感场景请优先使用 `backup.conf`,并把权限收紧:`chmod 600 backup.conf`
- macOS 上若未安装 `smbclient`,会回退到挂载方式(`mount_smbfs`),此时不支持 `--retention` 远端清理,仅完成上传
- macOS 上必须安装 `smbclient``brew install samba`),脚本不再使用 `mount_smbfs` 回退(旧行为不支持远端清理且语义与 Linux 不一致)
- 远端清理仅清理符合 `${COMMON_ARCHIVE_PREFIX}-YYYYmmdd-HHMMSS` 命名规范的目录,避免误删其它内容。
- rclone 远端必须先在本机用 `rclone config` 配好;自定义 `RCLONE_CONFIG` 路径需保证脚本运行用户可读。
- 脚本不在上传后做远端 SHA256 校验(不同后端对 hash 的支持差异太大)。如需校验,恢复时进入备份目录用 `sha256sum -c` 对照同目录下的 `.sha256` 清单即可。

查看文件

@@ -459,29 +459,18 @@ cleanup_local() {
smb_check_deps() {
dbg "smb_check_deps: OS=$OS_NAME"
case "$OS_NAME" in
Linux)
if command -v smbclient >/dev/null 2>&1; then
SMB_TOOL="smbclient"
dbg "smb_check_deps: 找到 smbclient => $(command -v smbclient)"
else
err "未安装 smbclient。Debian/Ubuntu: apt install smbclient;RHEL/Alma: dnf install samba-client"
return 1
fi
;;
Darwin)
if command -v smbclient >/dev/null 2>&1; then
SMB_TOOL="smbclient"
dbg "smb_check_deps: 找到 smbclient => $(command -v smbclient)"
elif command -v mount_smbfs >/dev/null 2>&1; then
SMB_TOOL="mount_smbfs"
dbg "smb_check_deps: 未找到 smbclient,回退 mount_smbfs => $(command -v mount_smbfs)"
else
err "未找到 smbclientbrew install samba或 mount_smbfs"
return 1
fi
;;
Linux|Darwin) ;;
*) err "不支持的系统:$OS_NAME"; return 1 ;;
esac
if ! command -v smbclient >/dev/null 2>&1; then
case "$OS_NAME" in
Linux) err "未安装 smbclient。Debian/Ubuntu: apt install smbclient;RHEL/Alma: dnf install samba-client" ;;
Darwin) err "未安装 smbclient。请先安装brew install samba" ;;
esac
return 1
fi
SMB_TOOL="smbclient"
dbg "smb_check_deps: 找到 smbclient => $(command -v smbclient)"
log "SMB 工具:$SMB_TOOL"
return 0
}
@@ -498,9 +487,21 @@ smb_validate() {
smb_upload_smbclient() {
dbg "smb_upload_smbclient: 进入,本次文件数=${#ARCHIVE_FILES[@]} 备份目录=$ARCHIVE_BASENAME"
local remote_base="${SMB_PATH%/}"
local commands=""
local archive_dir
if [[ -n "$remote_base" ]]; then
archive_dir="${remote_base}/${ARCHIVE_BASENAME}"
else
archive_dir="${ARCHIVE_BASENAME}"
fi
# 逐级 mkdir 共享内的 SMB_PATH可能不存在
local args=( "//${SMB_HOST}/${SMB_SHARE}" "-U" "${SMB_USER}%${SMB_PASSWORD}" )
[[ -n "$SMB_DOMAIN" ]] && args+=( "-W" "$SMB_DOMAIN" )
[[ -n "$SMB_VERSION" ]] && args+=( "-m" "SMB${SMB_VERSION//./}" )
local sm_debug=()
[[ "$DEBUG" == "true" ]] && sm_debug=( "-d" "1" )
# 第 1 步:建目录(一次连接,逐级 mkdir SMB_PATH 再 mkdir 本次备份子目录)
local mkdir_cmd=""
if [[ -n "$remote_base" ]]; then
local IFS=/
# shellcheck disable=SC2206
@@ -510,35 +511,38 @@ smb_upload_smbclient() {
for seg in "${segs[@]}"; do
[[ -z "$seg" ]] && continue
cur="${cur}${seg}"
commands+="mkdir \"${cur}\";"
mkdir_cmd+="mkdir \"${cur}\";"
cur="${cur}/"
done
commands+="cd \"${remote_base}\";"
mkdir_cmd+="cd \"${remote_base}\";"
fi
# 为本次备份创建独立子目录
commands+="mkdir \"${ARCHIVE_BASENAME}\";cd \"${ARCHIVE_BASENAME}\";"
mkdir_cmd+="mkdir \"${ARCHIVE_BASENAME}\";"
local pp pname
for pp in "${ARCHIVE_FILES[@]}"; do
pname="$(basename "$pp")"
commands+="put \"${pp}\" \"${pname}\";"
done
dbg "smb_upload_smbclient: smbclient 命令串=$commands"
local args=( "//${SMB_HOST}/${SMB_SHARE}" "-U" "${SMB_USER}%${SMB_PASSWORD}" )
[[ -n "$SMB_DOMAIN" ]] && args+=( "-W" "$SMB_DOMAIN" )
[[ -n "$SMB_VERSION" ]] && args+=( "-m" "SMB${SMB_VERSION//./}" )
# debug 模式给 smbclient 也加上 -d 1
local sm_debug=()
[[ "$DEBUG" == "true" ]] && sm_debug=( "-d" "1" )
log "通过 smbclient 上传到 //${SMB_HOST}/${SMB_SHARE}/${SMB_PATH%/}/${ARCHIVE_BASENAME}/"
log "通过 smbclient 上传到 //${SMB_HOST}/${SMB_SHARE}/${archive_dir}/(共 ${#ARCHIVE_FILES[@]} 个文件)"
dbg "smbclient 参数(脱敏)://${SMB_HOST}/${SMB_SHARE} -U ${SMB_USER}%$(mask "$SMB_PASSWORD") ${SMB_DOMAIN:+-W $SMB_DOMAIN} ${SMB_VERSION:+-m SMB${SMB_VERSION//./}}"
if ! smbclient "${args[@]}" "${sm_debug[@]}" -c "$commands"; then
err "smbclient 上传失败"
dbg "smbclient mkdir 命令串=$mkdir_cmd"
# mkdir 失败可能是已存在,不致命;smbclient 不会非 0 退出,这里只做一次连接确保目录就绪
smbclient "${args[@]}" "${sm_debug[@]}" -c "$mkdir_cmd" >/dev/null 2>&1 || true
# 第 2 步:每个文件单独一次 smbclient 调用,方便打印 [i/N] 进度与单文件耗时
local total=${#ARCHIVE_FILES[@]}
local idx=0
local pp pname pkb start_ts elapsed
for pp in "${ARCHIVE_FILES[@]}"; do
idx=$((idx+1))
pname="$(basename "$pp")"
pkb="$(du -k "$pp" 2>/dev/null | awk '{print $1+0}')"
log "上传 [${idx}/${total}]$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)
if ! smbclient "${args[@]}" "${sm_debug[@]}" -D "$archive_dir" -c "put \"${pp}\" \"${pname}\""; then
elapsed=$(( $(date +%s) - start_ts ))
err "上传失败 [${idx}/${total}]$pp (耗时 ${elapsed}s)"
return 1
fi
dbg "smb_upload_smbclient: 上传 OK"
elapsed=$(( $(date +%s) - start_ts ))
ok "上传完成 [${idx}/${total}]$pname (耗时 ${elapsed}s)"
done
ok "已上传全部 ${total} 个文件到 //${SMB_HOST}/${SMB_SHARE}/${archive_dir}/"
if [[ "$COMMON_RETENTION_DAYS" -gt 0 ]]; then
smb_retention_smbclient "${args[@]}"
@@ -584,43 +588,6 @@ smb_retention_smbclient() {
done
}
smb_upload_mount() {
dbg "smb_upload_mount: 使用 mount_smbfs 兜底"
local mnt
mnt="$(mktemp -d /tmp/smbmnt.XXXXXX)"
local url="//${SMB_USER}:${SMB_PASSWORD}@${SMB_HOST}/${SMB_SHARE}"
log "挂载 SMB${mnt}"
dbg "mount_smbfs URL脱敏//${SMB_USER}:$(mask "$SMB_PASSWORD")@${SMB_HOST}/${SMB_SHARE}"
if ! mount_smbfs "$url" "$mnt"; then
err "挂载 SMB 失败"; rmdir "$mnt"; return 1
fi
local dest="$mnt"
if [[ -n "$SMB_PATH" ]]; then
dest="$mnt/$SMB_PATH"
fi
dest="$dest/$ARCHIVE_BASENAME"
if ! mkdir -p "$dest"; then
err "无法创建远端目录:$dest"
umount "$mnt" 2>/dev/null
rmdir "$mnt"
return 1
fi
dbg "smb_upload_mount: 目标 $dest,待上传=${#ARCHIVE_FILES[@]}"
local pp
for pp in "${ARCHIVE_FILES[@]}"; do
log "复制:$(basename "$pp")"
if ! cp "$pp" "$dest/"; then
err "复制失败:$pp"
umount "$mnt" 2>/dev/null
rmdir "$mnt"
return 1
fi
done
ok "已复制全部 ${#ARCHIVE_FILES[@]} 个文件到 $dest/"
umount "$mnt" 2>/dev/null
rmdir "$mnt"
}
run_smb() {
dbg "run_smb: 开始"
smb_check_deps || return 1
@@ -628,13 +595,8 @@ run_smb() {
print_effective_config
create_archive || return 1
if [[ "$SMB_TOOL" == "smbclient" ]]; then
smb_upload_smbclient || { cleanup_local; return 1; }
else
smb_upload_mount || { cleanup_local; return 1; }
fi
smb_upload_smbclient || return 1
ok "SMB 上传完成"
cleanup_local
}
# ---------- rclone ----------
@@ -697,26 +659,29 @@ 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}/"
log "通过 rclone 上传到 ${remote_dir}/(共 ${#ARCHIVE_FILES[@]} 个文件)"
# 让 rclone 每 5 秒打印一行进度(已传/总量/百分比/速率/ETA,写到 stdout 而不是被 INFO 级别压住。
# --stats-one-line 让进度落在单行;NOTICE 级别能在非交互cron下也输出。
local stats_flags=( --stats=5s --stats-one-line --stats-log-level NOTICE )
local total=${#ARCHIVE_FILES[@]}
local idx=0
local pp pname pkb start_ts elapsed rc
for pp in "${ARCHIVE_FILES[@]}"; do
idx=$((idx+1))
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}'))"
log "上传 [${idx}/${total}]$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)"
err "上传失败 [${idx}/${total}]$pp (耗时 ${elapsed}s)"
return 1
fi
ok "上传完成:$pname (耗时 ${elapsed}s)"
ok "上传完成 [${idx}/${total}]$pname (耗时 ${elapsed}s)"
done
ok "已上传全部 ${#ARCHIVE_FILES[@]} 个文件到 ${remote_dir}/"
ok "已上传全部 ${total} 个文件到 ${remote_dir}/"
if [[ "$COMMON_RETENTION_DAYS" -gt 0 ]]; then
rclone_retention
@@ -772,9 +737,8 @@ run_rclone() {
print_effective_config
create_archive || return 1
rclone_upload || { cleanup_local; return 1; }
rclone_upload || return 1
ok "rclone 上传完成"
cleanup_local
}
# ---------- SFTP预留----------
@@ -797,6 +761,12 @@ main() {
parse_args "$@" || exit 1
# 任何退出路径(成功 / 失败 / Ctrl-C / kill都触发一次清理cleanup_local 自身
# 受 COMMON_CLEAN_LOCAL 控制,且只删 ${COMMON_ARCHIVE_PREFIX}* 匹配项,幂等可重入。
trap 'cleanup_local' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
print_effective_config
case "$METHOD" in