From 2db4f4b5302e2e75977fc25041f67dc9ce7d9cb5 Mon Sep 17 00:00:00 2001 From: HA Date: Sun, 26 Apr 2026 23:21:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backup/README.md | 5 +-- backup/backup.conf | 6 ++-- backup/backup.sh | 79 +++++++++++++++------------------------------- 3 files changed, 32 insertions(+), 58 deletions(-) diff --git a/backup/README.md b/backup/README.md index 100b0b0..b45b6b4 100644 --- a/backup/README.md +++ b/backup/README.md @@ -84,7 +84,8 @@ bash backup.sh smb | 字段 | 说明 | | --- | --- | | `COMMON_SOURCE_PATHS` | 要备份的源路径,多个用空格分隔,需引号包裹 | -| `COMMON_TMP_DIR` | 本地临时打包目录,默认 `/tmp/backup` | +| `COMMON_TMP_DIR` | 本地临时打包目录,默认 `/tmp/backup_script` | +| `COMMON_CLEAN_LOCAL` 配套行为 | 上传完成或失败后,会清理 `${COMMON_TMP_DIR}/${COMMON_ARCHIVE_PREFIX}*` 匹配到的文件/目录(含上次失败遗留的同前缀产物),同目录下其它无关文件不受影响 | | `COMMON_ARCHIVE_PREFIX` | 归档/远端目录命名前缀,最终形如 `prefix-YYYYmmdd-HHMMSS/` | | `COMMON_CLEAN_LOCAL` | 上传后是否删除本地归档(`true` / `false`) | | `COMMON_RETENTION_DAYS` | 远端保留天数,`0` 表示不清理;按目录整体清理 | @@ -346,5 +347,5 @@ tar -xzf backup-20260426-031000.tar.gz - 命令行密码会出现在进程列表中,安全敏感场景请优先使用 `backup.conf`,并把权限收紧:`chmod 600 backup.conf`。 - macOS 上若未安装 `smbclient`,会回退到挂载方式(`mount_smbfs`),此时不支持 `--retention` 远端清理,仅完成上传。 - 远端清理仅清理符合 `${COMMON_ARCHIVE_PREFIX}-YYYYmmdd-HHMMSS` 命名规范的目录,避免误删其它内容。 -- rclone 上传完成后会自动用 `rclone hashsum SHA256` 拉取远端 hash 与本地 `.sha256` 清单逐项比对,校验失败会以非 0 状态码退出。少数远端(个别 WebDAV)不支持 SHA256 hash,遇到这种情况脚本会报错退出,可改成手动下载后用 `sha256sum -c` 校验。 - rclone 远端必须先在本机用 `rclone config` 配好;自定义 `RCLONE_CONFIG` 路径需保证脚本运行用户可读。 +- 脚本不在上传后做远端 SHA256 校验(不同后端对 hash 的支持差异太大)。如需校验,恢复时进入备份目录用 `sha256sum -c` 对照同目录下的 `.sha256` 清单即可。 diff --git a/backup/backup.conf b/backup/backup.conf index 97373df..bc23a67 100644 --- a/backup/backup.conf +++ b/backup/backup.conf @@ -11,8 +11,10 @@ # 要备份的源目录或文件(多个用空格分隔,需引号包裹) COMMON_SOURCE_PATHS="/opt" -# 本地临时打包目录(用于存放归档/分卷/sha 文件,备份完成后会清理) -COMMON_TMP_DIR="/tmp/backup" +# 本地临时打包目录(用于存放归档/分卷/sha 文件) +# 注意:当 COMMON_CLEAN_LOCAL=true 时,会清理目录下所有以 COMMON_ARCHIVE_PREFIX 开头 +# 的文件/目录(包含上次失败遗留的中间产物),其它无关文件不受影响。 +COMMON_TMP_DIR="/tmp/backup_script" # 归档/远端目录命名前缀,最终形如 ${COMMON_ARCHIVE_PREFIX}-YYYYmmdd-HHMMSS/ COMMON_ARCHIVE_PREFIX="backup" diff --git a/backup/backup.sh b/backup/backup.sh index b36ae3d..9f45ff2 100644 --- a/backup/backup.sh +++ b/backup/backup.sh @@ -27,7 +27,7 @@ CONF_FILE="${SCRIPT_DIR}/backup.conf" # ---- 默认值 ---- # 公共配置(统一以 COMMON_ 前缀,与 backup.conf 一致) COMMON_SOURCE_PATHS="" -COMMON_TMP_DIR="/tmp/backup" +COMMON_TMP_DIR="/tmp/backup_script" COMMON_ARCHIVE_PREFIX="backup" COMMON_CLEAN_LOCAL="true" COMMON_RETENTION_DAYS=0 @@ -79,7 +79,7 @@ Methods: Common options: -C, --config FILE 指定配置文件路径(默认:脚本所在目录下 backup.conf) -s, --source PATHS 要备份的源路径,多个用空格分隔,需引号包裹 - -t, --tmp-dir DIR 本地临时目录(默认:/tmp/backup) + -t, --tmp-dir DIR 本地临时目录(默认:/tmp/backup_script) -p, --prefix NAME 归档文件名前缀(默认:backup) --keep-local 上传后保留本地归档 --retention DAYS 远端保留天数,0 表示不清理 @@ -424,17 +424,34 @@ create_archive() { } cleanup_local() { - dbg "cleanup_local: COMMON_CLEAN_LOCAL=$COMMON_CLEAN_LOCAL 文件数=${#ARCHIVE_FILES[@]}" + dbg "cleanup_local: COMMON_CLEAN_LOCAL=$COMMON_CLEAN_LOCAL COMMON_TMP_DIR=$COMMON_TMP_DIR PREFIX=$COMMON_ARCHIVE_PREFIX" if [[ "$COMMON_CLEAN_LOCAL" != "true" ]]; then dbg "cleanup_local: 跳过删除" return 0 fi - local p - for p in "${ARCHIVE_FILES[@]}"; do - if [[ -f "$p" ]]; then - rm -f "$p" && log "已删除本地:$p" - fi + # 安全栅栏:必备字段不能为空,避免出现 rm -rf /* 这类灾难 + if [[ -z "$COMMON_TMP_DIR" || -z "$COMMON_ARCHIVE_PREFIX" ]]; then + warn "COMMON_TMP_DIR 或 COMMON_ARCHIVE_PREFIX 为空,拒绝清理" + return 0 + fi + if [[ ! -d "$COMMON_TMP_DIR" ]]; then + dbg "cleanup_local: 目录不存在,跳过" + return 0 + fi + # 仅清理所有以 COMMON_ARCHIVE_PREFIX 开头的文件/目录。 + # 这样既能带走本次的归档/分卷/sha,也能扫掉上次失败遗留(同样以该前缀开头)的 + # 中间产物(如 *.tar.gz、*.tar.gz.split-tmp.* 等),同时不会误伤同目录下的其它文件。 + local count=0 p + shopt -s nullglob + for p in "${COMMON_TMP_DIR%/}/${COMMON_ARCHIVE_PREFIX}"*; do + rm -rf -- "$p" && count=$((count+1)) done + shopt -u nullglob + if [[ $count -gt 0 ]]; then + log "已清理本地以 \"${COMMON_ARCHIVE_PREFIX}\" 开头的文件/目录 ${count} 项(位于 ${COMMON_TMP_DIR})" + else + dbg "cleanup_local: 没有匹配 ${COMMON_ARCHIVE_PREFIX}* 的文件" + fi } # ---------- SMB ---------- @@ -701,9 +718,6 @@ rclone_upload() { 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 @@ -711,49 +725,6 @@ rclone_upload() { 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" - - # 本地清单格式: " ";远端 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%/}"