feat: 修改备份脚本

这个提交包含在:
HA
2026-04-26 20:22:31 +08:00
父节点 8c01935574
当前提交 cd1b02b0c7
共有 3 个文件被更改,包括 336 次插入103 次删除

查看文件

@@ -25,13 +25,15 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONF_FILE="${SCRIPT_DIR}/backup.conf"
# ---- 默认值 ----
SOURCE_PATHS=""
TMP_DIR="/tmp/backup"
ARCHIVE_PREFIX="backup"
METHOD=""
CLEAN_LOCAL="true"
RETENTION_DAYS=0
# 公共配置(统一以 COMMON_ 前缀,与 backup.conf 一致)
COMMON_SOURCE_PATHS=""
COMMON_TMP_DIR="/tmp/backup"
COMMON_ARCHIVE_PREFIX="backup"
COMMON_CLEAN_LOCAL="true"
COMMON_RETENTION_DAYS=0
COMMON_SPLIT_SIZE="1G" # 留空 = 不分卷
# SMB 专属
SMB_HOST=""
SMB_SHARE=""
SMB_PATH=""
@@ -40,6 +42,7 @@ SMB_PASSWORD=""
SMB_DOMAIN=""
SMB_VERSION=""
# SFTP 专属(预留)
SFTP_HOST=""
SFTP_PORT="22"
SFTP_USER=""
@@ -47,9 +50,14 @@ SFTP_PASSWORD=""
SFTP_KEY=""
SFTP_PATH=""
# 由命令行第一个位置参数决定smb / sftp,不从 conf 读取
METHOD=""
OS_NAME="$(uname -s)"
ARCHIVE_FILE=""
ARCHIVE_NAME=""
ARCHIVE_TS="" # 当次备份时间戳,例如 20260426-031000
ARCHIVE_BASENAME="" # 当次备份的文件夹/文件名,例如 backup-20260426-031000
ARCHIVE_FILES=() # 上传时遍历的本地路径(分卷 .tar.gz.0/.1/... + .sha256
SHA_TOOL="" # sha256sum / shasum -a 256
SMB_TOOL=""
usage() {
@@ -67,6 +75,7 @@ Common options:
-p, --prefix NAME 归档文件名前缀默认backup
--keep-local 上传后保留本地归档
--retention DAYS 远端保留天数,0 表示不清理
--split-size SIZE 分卷大小(默认 1G;2G / 500M / 100k 等),传空字符串不分卷
--debug 打印详细调试日志(也可用环境变量 DEBUG=true
-h, --help 显示帮助
@@ -124,18 +133,31 @@ print_effective_config() {
dbg "===== 生效配置 ====="
dbg "CONF_FILE=$CONF_FILE"
dbg "METHOD=$METHOD"
dbg "SOURCE_PATHS=$SOURCE_PATHS"
dbg "TMP_DIR=$TMP_DIR"
dbg "ARCHIVE_PREFIX=$ARCHIVE_PREFIX"
dbg "CLEAN_LOCAL=$CLEAN_LOCAL"
dbg "RETENTION_DAYS=$RETENTION_DAYS"
dbg "SMB_HOST=$SMB_HOST"
dbg "SMB_SHARE=$SMB_SHARE"
dbg "SMB_PATH=$SMB_PATH"
dbg "SMB_USER=$SMB_USER"
dbg "SMB_PASSWORD=$(mask "$SMB_PASSWORD")"
dbg "SMB_DOMAIN=$SMB_DOMAIN"
dbg "SMB_VERSION=$SMB_VERSION"
dbg "COMMON_SOURCE_PATHS=$COMMON_SOURCE_PATHS"
dbg "COMMON_TMP_DIR=$COMMON_TMP_DIR"
dbg "COMMON_ARCHIVE_PREFIX=$COMMON_ARCHIVE_PREFIX"
dbg "COMMON_CLEAN_LOCAL=$COMMON_CLEAN_LOCAL"
dbg "COMMON_RETENTION_DAYS=$COMMON_RETENTION_DAYS"
dbg "COMMON_SPLIT_SIZE=$COMMON_SPLIT_SIZE"
case "$METHOD" in
smb)
dbg "SMB_HOST=$SMB_HOST"
dbg "SMB_SHARE=$SMB_SHARE"
dbg "SMB_PATH=$SMB_PATH"
dbg "SMB_USER=$SMB_USER"
dbg "SMB_PASSWORD=$(mask "$SMB_PASSWORD")"
dbg "SMB_DOMAIN=$SMB_DOMAIN"
dbg "SMB_VERSION=$SMB_VERSION"
;;
sftp)
dbg "SFTP_HOST=$SFTP_HOST"
dbg "SFTP_PORT=$SFTP_PORT"
dbg "SFTP_USER=$SFTP_USER"
dbg "SFTP_PASSWORD=$(mask "$SFTP_PASSWORD")"
dbg "SFTP_KEY=$SFTP_KEY"
dbg "SFTP_PATH=$SFTP_PATH"
;;
esac
}
load_conf() {
@@ -198,11 +220,12 @@ parse_args() {
dbg " parse: 当前=$1"
case "$1" in
-C|--config) shift 2 ;; # 已处理
-s|--source) SOURCE_PATHS="$2"; shift 2 ;;
-t|--tmp-dir) TMP_DIR="$2"; shift 2 ;;
-p|--prefix) ARCHIVE_PREFIX="$2"; shift 2 ;;
--keep-local) CLEAN_LOCAL="false"; shift ;;
--retention) RETENTION_DAYS="$2"; shift 2 ;;
-s|--source) COMMON_SOURCE_PATHS="$2"; shift 2 ;;
-t|--tmp-dir) COMMON_TMP_DIR="$2"; shift 2 ;;
-p|--prefix) COMMON_ARCHIVE_PREFIX="$2"; shift 2 ;;
--keep-local) COMMON_CLEAN_LOCAL="false"; shift ;;
--retention) COMMON_RETENTION_DAYS="$2"; shift 2 ;;
--split-size) COMMON_SPLIT_SIZE="$2"; shift 2 ;;
--debug) DEBUG="true"; shift ;;
--smb-host) SMB_HOST="$2"; shift 2 ;;
--smb-share) SMB_SHARE="$2"; shift 2 ;;
@@ -225,23 +248,37 @@ parse_args() {
return 0
}
create_archive() {
dbg "create_archive: 入口 SOURCE_PATHS='$SOURCE_PATHS' TMP_DIR='$TMP_DIR'"
[[ -z "$SOURCE_PATHS" ]] && { err "未配置 SOURCE_PATHS"; return 1; }
if ! mkdir -p "$TMP_DIR"; then
err "无法创建临时目录:$TMP_DIR"
detect_sha_tool() {
if command -v sha256sum >/dev/null 2>&1; then
SHA_TOOL="sha256sum"
elif command -v shasum >/dev/null 2>&1; then
SHA_TOOL="shasum -a 256"
else
err "找不到 sha256sum / shasum,请安装 coreutilsmacOS 自带 shasum"
return 1
fi
dbg "create_archive: 临时目录就绪 $TMP_DIR (空间: $(df -h "$TMP_DIR" 2>/dev/null | tail -1))"
dbg "detect_sha_tool: 使用 $SHA_TOOL"
}
local ts archive
ts="$(date '+%Y%m%d-%H%M%S')"
archive="${TMP_DIR}/${ARCHIVE_PREFIX}-${ts}.tar.gz"
dbg "create_archive: 目标归档=$archive"
create_archive() {
dbg "create_archive: 入口 COMMON_SOURCE_PATHS='$COMMON_SOURCE_PATHS' COMMON_TMP_DIR='$COMMON_TMP_DIR'"
[[ -z "$COMMON_SOURCE_PATHS" ]] && { err "未配置 COMMON_SOURCE_PATHS"; return 1; }
detect_sha_tool || return 1
if ! mkdir -p "$COMMON_TMP_DIR"; then
err "无法创建临时目录:$COMMON_TMP_DIR"
return 1
fi
dbg "create_archive: 临时目录就绪 $COMMON_TMP_DIR (空间: $(df -h "$COMMON_TMP_DIR" 2>/dev/null | tail -1))"
ARCHIVE_TS="$(date '+%Y%m%d-%H%M%S')"
ARCHIVE_BASENAME="${COMMON_ARCHIVE_PREFIX}-${ARCHIVE_TS}"
local archive="${COMMON_TMP_DIR}/${ARCHIVE_BASENAME}.tar.gz"
local sha_file="${COMMON_TMP_DIR}/${ARCHIVE_BASENAME}.sha256"
dbg "create_archive: 备份名=$ARCHIVE_BASENAME 目标归档=$archive"
# shellcheck disable=SC2206
local paths=($SOURCE_PATHS)
local paths=($COMMON_SOURCE_PATHS)
dbg "create_archive: 解析后路径数=${#paths[@]}"
local valid=0
for p in "${paths[@]}"; do
@@ -268,19 +305,77 @@ create_archive() {
if [[ -n "$tar_err" ]]; then
dbg "tar 输出: $tar_err"
fi
ARCHIVE_FILE="$archive"
ARCHIVE_NAME="$(basename "$archive")"
ok "打包完成:$archive ($(du -h "$archive" | awk '{print $1}'))"
dbg "create_archive: ARCHIVE_FILE=$ARCHIVE_FILE ARCHIVE_NAME=$ARCHIVE_NAME"
ARCHIVE_FILES=()
if [[ -n "$COMMON_SPLIT_SIZE" ]]; then
if ! command -v split >/dev/null 2>&1; then
err "未找到 split 命令,无法分卷"
return 1
fi
log "分卷:单卷上限 $COMMON_SPLIT_SIZE"
# 先用字母后缀切片split 默认行为,BSD/GNU 都支持),再批量重命名为
# .tar.gz.0 / .1 / .2 ...,避免直接依赖 split -dmacOS 旧版本不支持)。
# -a 5 给 26^5 ≈ 1200 万片的余量,1G 单卷可覆盖 ~12PB 总量。
local tmp_prefix="${archive}.split-tmp."
if ! split -a 5 -b "$COMMON_SPLIT_SIZE" "$archive" "$tmp_prefix"; then
err "split 分卷失败(请检查 COMMON_SPLIT_SIZE 格式,例如 1G / 500M / 100k"
rm -f "${tmp_prefix}"*
return 1
fi
rm -f "$archive"
# 字母后缀按 sort 是单调递增 → 直接顺次映射为 0,1,2,...
local f i=0
# shellcheck disable=SC2012
while IFS= read -r f; do
mv "$f" "${archive}.${i}" || { err "重命名分卷失败:$f"; return 1; }
ARCHIVE_FILES+=( "${archive}.${i}" )
i=$((i+1))
done < <(ls "${tmp_prefix}"* 2>/dev/null | sort)
if [[ ${#ARCHIVE_FILES[@]} -eq 0 ]]; then
err "split 后未找到分卷文件(前缀=$tmp_prefix"
return 1
fi
ok "已分成 ${#ARCHIVE_FILES[@]} 卷:${ARCHIVE_BASENAME}.tar.gz.0 ... ${ARCHIVE_BASENAME}.tar.gz.$((i-1))"
dbg "分卷列表: ${ARCHIVE_FILES[*]}"
else
ARCHIVE_FILES=( "$archive" )
fi
# 生成 sha256 清单文件,路径用相对名(恢复时在备份目录里直接 sha256sum -c
log "生成 SHA256 清单:$sha_file"
local cwd
cwd="$(pwd)"
cd "$COMMON_TMP_DIR" || { err "无法进入 $COMMON_TMP_DIR"; return 1; }
local rel_files=()
for f in "${ARCHIVE_FILES[@]}"; do
rel_files+=( "$(basename "$f")" )
done
if ! $SHA_TOOL "${rel_files[@]}" > "$sha_file"; then
err "生成 SHA256 失败"
cd "$cwd"
return 1
fi
cd "$cwd"
ARCHIVE_FILES+=( "$sha_file" )
dbg "create_archive: 完整待上传文件列表 (${#ARCHIVE_FILES[@]})${ARCHIVE_FILES[*]}"
ok "SHA256 清单完成:$sha_file"
}
cleanup_local() {
dbg "cleanup_local: CLEAN_LOCAL=$CLEAN_LOCAL ARCHIVE_FILE=$ARCHIVE_FILE"
if [[ "$CLEAN_LOCAL" == "true" && -n "$ARCHIVE_FILE" && -f "$ARCHIVE_FILE" ]]; then
rm -f "$ARCHIVE_FILE" && log "已删除本地归档:$ARCHIVE_FILE"
else
dbg "cleanup_local: COMMON_CLEAN_LOCAL=$COMMON_CLEAN_LOCAL 文件数=${#ARCHIVE_FILES[@]}"
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
done
}
# ---------- SMB ----------
@@ -325,24 +420,33 @@ smb_validate() {
}
smb_upload_smbclient() {
dbg "smb_upload_smbclient: 进入"
local remote_dir="${SMB_PATH%/}"
dbg "smb_upload_smbclient: 进入,本次文件数=${#ARCHIVE_FILES[@]} 备份目录=$ARCHIVE_BASENAME"
local remote_base="${SMB_PATH%/}"
local commands=""
if [[ -n "$remote_dir" ]]; then
# 逐级 mkdir 共享内的 SMB_PATH可能不存在
if [[ -n "$remote_base" ]]; then
local IFS=/
# shellcheck disable=SC2206
local parts=($remote_dir)
local segs=($remote_base)
unset IFS
local cur=""
for seg in "${parts[@]}"; do
for seg in "${segs[@]}"; do
[[ -z "$seg" ]] && continue
cur="${cur}${seg}"
commands+="mkdir \"${cur}\";"
cur="${cur}/"
done
commands+="cd \"${remote_dir}\";"
commands+="cd \"${remote_base}\";"
fi
commands+="put \"${ARCHIVE_FILE}\" \"${ARCHIVE_NAME}\";"
# 为本次备份创建独立子目录
commands+="mkdir \"${ARCHIVE_BASENAME}\";cd \"${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}" )
@@ -352,7 +456,7 @@ smb_upload_smbclient() {
local sm_debug=()
[[ "$DEBUG" == "true" ]] && sm_debug=( "-d" "1" )
log "通过 smbclient 上传到 //${SMB_HOST}/${SMB_SHARE}/${SMB_PATH}"
log "通过 smbclient 上传到 //${SMB_HOST}/${SMB_SHARE}/${SMB_PATH%/}/${ARCHIVE_BASENAME}/"
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 上传失败"
@@ -360,40 +464,46 @@ smb_upload_smbclient() {
fi
dbg "smb_upload_smbclient: 上传 OK"
if [[ "$RETENTION_DAYS" -gt 0 ]]; then
if [[ "$COMMON_RETENTION_DAYS" -gt 0 ]]; then
smb_retention_smbclient "${args[@]}"
else
dbg "smb_upload_smbclient: RETENTION_DAYS=0,跳过远端清理"
dbg "smb_upload_smbclient: COMMON_RETENTION_DAYS=0,跳过远端清理"
fi
}
smb_retention_smbclient() {
local args=( "$@" )
dbg "smb_retention_smbclient: RETENTION_DAYS=$RETENTION_DAYS"
dbg "smb_retention_smbclient: COMMON_RETENTION_DAYS=$COMMON_RETENTION_DAYS"
local listing
listing="$(smbclient "${args[@]}" -D "${SMB_PATH:-/}" -c "ls" 2>/dev/null)" || return 0
local cutoff
if date -v-1d >/dev/null 2>&1; then
cutoff=$(date -v-"${RETENTION_DAYS}"d +%s)
cutoff=$(date -v-"${COMMON_RETENTION_DAYS}"d +%s)
else
cutoff=$(date -d "-${RETENTION_DAYS} days" +%s)
cutoff=$(date -d "-${COMMON_RETENTION_DAYS} days" +%s)
fi
log "清理远端早于 ${RETENTION_DAYS} 天的归档"
log "清理远端早于 ${COMMON_RETENTION_DAYS} 天的备份目录"
dbg "cutoff=$cutoff ($(date -r "$cutoff" 2>/dev/null || date -d "@$cutoff" 2>/dev/null))"
echo "$listing" | awk '{print $1}' | grep -E "^${ARCHIVE_PREFIX}-[0-9]{8}-[0-9]{6}\\.tar\\.gz\$" | while read -r f; do
local fts ftime
fts="$(echo "$f" | sed -E "s/^${ARCHIVE_PREFIX}-([0-9]{8})-([0-9]{6})\\.tar\\.gz\$/\\1 \\2/")"
local d="${fts% *}" t="${fts#* }"
# smbclient ls 输出形如: name FLAGS size weekday mon dd hh:mm:ss yyyy
# 目录会带 D 标志;这里直接以名字模式 + D 标志双重匹配,减少误删风险。
echo "$listing" | awk '$2 ~ /D/ {print $1}' | 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 " 远端文件 $f -> ftime=$ftime"
dbg " 远端目录 $dir -> ftime=$ftime"
if [[ "$ftime" -lt "$cutoff" ]]; then
log "删除远端旧归档$f"
smbclient "${args[@]}" -D "${SMB_PATH:-/}" -c "del \"$f\"" >/dev/null 2>&1 || warn "删除失败:$f"
log "删除远端旧备份目录$dir"
# 先清空目录内文件,再 rmdir
smbclient "${args[@]}" -D "${SMB_PATH:-/}/${dir}" -c "prompt OFF; mask *; del *" >/dev/null 2>&1
smbclient "${args[@]}" -D "${SMB_PATH:-/}" -c "rmdir \"${dir}\"" >/dev/null 2>&1 || warn "rmdir 失败:$dir"
fi
done
}
@@ -411,17 +521,26 @@ smb_upload_mount() {
local dest="$mnt"
if [[ -n "$SMB_PATH" ]]; then
dest="$mnt/$SMB_PATH"
mkdir -p "$dest" || true
fi
dbg "smb_upload_mount: 目标 $dest"
if cp "$ARCHIVE_FILE" "$dest/"; then
ok "已复制到 $dest/"
else
err "复制失败"
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"
}