From 99c9c698628c49e9e87212752c9a8ad0f6ede3da Mon Sep 17 00:00:00 2001 From: HA Date: Sun, 26 Apr 2026 23:56:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backup/README.md | 6 +- backup/backup.sh | 164 +++++++++++++++++++---------------------------- 2 files changed, 70 insertions(+), 100 deletions(-) diff --git a/backup/README.md b/backup/README.md index b45b6b4..1f89d73 100644 --- a/backup/README.md +++ b/backup/README.md @@ -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` 清单即可。 diff --git a/backup/backup.sh b/backup/backup.sh index 9f45ff2..23a68bc 100644 --- a/backup/backup.sh +++ b/backup/backup.sh @@ -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 "未找到 smbclient(brew 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 上传失败" - return 1 - fi - dbg "smb_upload_smbclient: 上传 OK" + 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 + 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