626 行
21 KiB
Bash
626 行
21 KiB
Bash
#!/usr/bin/env bash
|
||
# backup.sh — 打包源目录并通过 SMB / (预留) SFTP 上传到远端
|
||
# 同时支持 Linux 与 macOS
|
||
|
||
set -o pipefail
|
||
# 注意:不开启 set -u —— macOS 自带 bash 3.2,对空位置参数 / 空数组("$@"、${arr[@]})
|
||
# 会触发 unbound 报错并静默退出,导致脚本在加载完配置后就停下。
|
||
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
CYAN='\033[0;36m'
|
||
MAGENTA='\033[0;35m'
|
||
NC='\033[0m'
|
||
|
||
DEBUG="${DEBUG:-false}"
|
||
|
||
log() { printf "${CYAN}[%s]${NC} %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; }
|
||
warn() { printf "${YELLOW}[%s] WARN:${NC} %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; }
|
||
err() { printf "${RED}[%s] ERROR:${NC} %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >&2; }
|
||
ok() { printf "${GREEN}[%s]${NC} %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; }
|
||
dbg() { [[ "$DEBUG" == "true" ]] && printf "${MAGENTA}[%s] DEBUG:${NC} %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >&2 || true; }
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
CONF_FILE="${SCRIPT_DIR}/backup.conf"
|
||
|
||
# ---- 默认值 ----
|
||
# 公共配置(统一以 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=""
|
||
SMB_USER=""
|
||
SMB_PASSWORD=""
|
||
SMB_DOMAIN=""
|
||
SMB_VERSION=""
|
||
|
||
# SFTP 专属(预留)
|
||
SFTP_HOST=""
|
||
SFTP_PORT="22"
|
||
SFTP_USER=""
|
||
SFTP_PASSWORD=""
|
||
SFTP_KEY=""
|
||
SFTP_PATH=""
|
||
|
||
# 由命令行第一个位置参数决定(smb / sftp),不从 conf 读取
|
||
METHOD=""
|
||
|
||
OS_NAME="$(uname -s)"
|
||
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() {
|
||
cat <<'EOF'
|
||
Usage: bash backup.sh <method> [options]
|
||
|
||
Methods:
|
||
smb 通过 SMB / Samba 协议上传
|
||
sftp 通过 SFTP 上传(暂未实现,已预留入口)
|
||
|
||
Common options:
|
||
-C, --config FILE 指定配置文件路径(默认:脚本所在目录下 backup.conf)
|
||
-s, --source PATHS 要备份的源路径,多个用空格分隔,需引号包裹
|
||
-t, --tmp-dir DIR 本地临时目录(默认:/tmp/backup)
|
||
-p, --prefix NAME 归档文件名前缀(默认:backup)
|
||
--keep-local 上传后保留本地归档
|
||
--retention DAYS 远端保留天数,0 表示不清理
|
||
--split-size SIZE 分卷大小(默认 1G;2G / 500M / 100k 等),传空字符串不分卷
|
||
--debug 打印详细调试日志(也可用环境变量 DEBUG=true)
|
||
-h, --help 显示帮助
|
||
|
||
SMB options:
|
||
--smb-host HOST
|
||
--smb-share NAME
|
||
--smb-path PATH 共享内子目录
|
||
--smb-user USER
|
||
--smb-password PASS
|
||
--smb-domain DOMAIN
|
||
--smb-version VER 例如 3.0
|
||
|
||
SFTP options (预留):
|
||
--sftp-host HOST
|
||
--sftp-port PORT
|
||
--sftp-user USER
|
||
--sftp-password PASS
|
||
--sftp-key FILE
|
||
--sftp-path PATH
|
||
|
||
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 smb --debug
|
||
EOF
|
||
}
|
||
|
||
mask() {
|
||
# 给敏感值打码:保留前 1 位,其余替换成 *
|
||
local s="$1"
|
||
[[ -z "$s" ]] && { echo "(empty)"; return; }
|
||
local len=${#s}
|
||
if [[ $len -le 2 ]]; then
|
||
printf '%s\n' "**"
|
||
else
|
||
printf '%s%s\n' "${s:0:1}" "$(printf '%*s' $((len-1)) '' | tr ' ' '*')"
|
||
fi
|
||
}
|
||
|
||
print_runtime_env() {
|
||
dbg "===== 运行时环境 ====="
|
||
dbg "OS_NAME=$OS_NAME"
|
||
dbg "uname -a: $(uname -a 2>/dev/null)"
|
||
dbg "BASH_VERSION=$BASH_VERSION"
|
||
dbg "PWD=$(pwd)"
|
||
dbg "SCRIPT_DIR=$SCRIPT_DIR"
|
||
dbg "脚本路径=${BASH_SOURCE[0]}"
|
||
dbg "EUID=$EUID USER=${USER:-?}"
|
||
dbg "PATH=$PATH"
|
||
dbg "原始参数($#): $*"
|
||
}
|
||
|
||
print_effective_config() {
|
||
dbg "===== 生效配置 ====="
|
||
dbg "CONF_FILE=$CONF_FILE"
|
||
dbg "METHOD=$METHOD"
|
||
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() {
|
||
dbg "load_conf: 读取 $CONF_FILE"
|
||
if [[ -f "$CONF_FILE" ]]; then
|
||
# shellcheck disable=SC1090
|
||
source "$CONF_FILE"
|
||
local rc=$?
|
||
if [[ $rc -ne 0 ]]; then
|
||
err "配置文件加载失败(rc=$rc):$CONF_FILE"
|
||
return 1
|
||
fi
|
||
log "已加载配置文件:$CONF_FILE"
|
||
dbg "load_conf: source 成功,rc=$rc"
|
||
else
|
||
warn "配置文件不存在:$CONF_FILE(仅使用命令行参数)"
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
parse_args() {
|
||
dbg "parse_args: 入参 ($#) = $*"
|
||
|
||
if [[ $# -lt 1 ]]; then
|
||
usage; exit 1
|
||
fi
|
||
|
||
# 第 0 遍:扫描 --debug,让后续日志能打印
|
||
local a
|
||
for a in "$@"; do
|
||
if [[ "$a" == "--debug" ]]; then
|
||
DEBUG="true"
|
||
dbg "已启用 DEBUG 模式"
|
||
fi
|
||
done
|
||
|
||
case "$1" in
|
||
-h|--help) usage; exit 0 ;;
|
||
smb|sftp) METHOD_ARG="$1"; shift ;;
|
||
*) err "未知方式:$1"; usage; exit 1 ;;
|
||
esac
|
||
dbg "parse_args: METHOD_ARG=$METHOD_ARG,剩余参数($#)=$*"
|
||
|
||
# 第 1 遍只取 --config / -C,使后续参数能覆盖配置
|
||
local prev=""
|
||
local arg
|
||
for arg in "$@"; do
|
||
if [[ "$prev" == "-C" || "$prev" == "--config" ]]; then
|
||
CONF_FILE="$arg"
|
||
dbg "parse_args: 命令行覆盖 CONF_FILE=$CONF_FILE"
|
||
fi
|
||
prev="$arg"
|
||
done
|
||
|
||
load_conf || return 1
|
||
METHOD="$METHOD_ARG"
|
||
dbg "parse_args: 进入第 2 遍参数解析"
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
dbg " parse: 当前=$1"
|
||
case "$1" in
|
||
-C|--config) 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 ;;
|
||
--smb-path) SMB_PATH="$2"; shift 2 ;;
|
||
--smb-user) SMB_USER="$2"; shift 2 ;;
|
||
--smb-password) SMB_PASSWORD="$2"; shift 2 ;;
|
||
--smb-domain) SMB_DOMAIN="$2"; shift 2 ;;
|
||
--smb-version) SMB_VERSION="$2"; shift 2 ;;
|
||
--sftp-host) SFTP_HOST="$2"; shift 2 ;;
|
||
--sftp-port) SFTP_PORT="$2"; shift 2 ;;
|
||
--sftp-user) SFTP_USER="$2"; shift 2 ;;
|
||
--sftp-password) SFTP_PASSWORD="$2"; shift 2 ;;
|
||
--sftp-key) SFTP_KEY="$2"; shift 2 ;;
|
||
--sftp-path) SFTP_PATH="$2"; shift 2 ;;
|
||
-h|--help) usage; exit 0 ;;
|
||
*) err "未知参数:$1"; usage; exit 1 ;;
|
||
esac
|
||
done
|
||
dbg "parse_args: 解析完成"
|
||
return 0
|
||
}
|
||
|
||
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,请安装 coreutils(macOS 自带 shasum)"
|
||
return 1
|
||
fi
|
||
dbg "detect_sha_tool: 使用 $SHA_TOOL"
|
||
}
|
||
|
||
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=($COMMON_SOURCE_PATHS)
|
||
dbg "create_archive: 解析后路径数=${#paths[@]}"
|
||
local valid=0
|
||
for p in "${paths[@]}"; do
|
||
if [[ ! -e "$p" ]]; then
|
||
warn "源路径不存在,跳过:$p"
|
||
else
|
||
dbg " 路径就绪:$p"
|
||
valid=$((valid+1))
|
||
fi
|
||
done
|
||
if [[ $valid -eq 0 ]]; then
|
||
err "没有任何有效的源路径"
|
||
return 1
|
||
fi
|
||
|
||
# 估算源大小用于显示百分比(失败则只显示已写入字节)
|
||
local src_kb=0
|
||
src_kb="$(du -sk "${paths[@]}" 2>/dev/null | awk '{s+=$1} END{print s+0}')"
|
||
if [[ "$src_kb" -gt 0 ]]; then
|
||
log "开始打包:$archive(源约 $(awk -v k="$src_kb" 'BEGIN{printf "%.1fG", k/1024/1024}'))"
|
||
else
|
||
log "开始打包:$archive"
|
||
fi
|
||
|
||
local tar_log="${archive}.tar.log"
|
||
: > "$tar_log"
|
||
tar -czf "$archive" "${paths[@]}" 2>"$tar_log" &
|
||
local tar_pid=$!
|
||
|
||
local start_ts=$(date +%s) cur_kb pct elapsed
|
||
while kill -0 "$tar_pid" 2>/dev/null; do
|
||
sleep 5
|
||
kill -0 "$tar_pid" 2>/dev/null || break
|
||
cur_kb="$(du -k "$archive" 2>/dev/null | awk '{print $1+0}')"
|
||
elapsed=$(( $(date +%s) - start_ts ))
|
||
if [[ "$src_kb" -gt 0 ]]; then
|
||
pct=$(awk -v c="$cur_kb" -v s="$src_kb" 'BEGIN{p=c*100/s; if(p>99)p=99; printf "%.0f", p}')
|
||
log "打包进度:已写入 $(awk -v k="$cur_kb" 'BEGIN{printf "%.2fG", k/1024/1024}') / $(awk -v k="$src_kb" 'BEGIN{printf "%.2fG", k/1024/1024}') (${pct}%),耗时 ${elapsed}s"
|
||
else
|
||
log "打包进度:已写入 $(awk -v k="$cur_kb" 'BEGIN{printf "%.2fG", k/1024/1024}'),耗时 ${elapsed}s"
|
||
fi
|
||
done
|
||
|
||
wait "$tar_pid"
|
||
local rc=$?
|
||
local tar_err
|
||
tar_err="$(cat "$tar_log" 2>/dev/null)"
|
||
rm -f "$tar_log"
|
||
if [[ $rc -ne 0 ]]; then
|
||
err "打包失败 (rc=$rc): $tar_err"
|
||
return 1
|
||
fi
|
||
if [[ -n "$tar_err" ]]; then
|
||
dbg "tar 输出: $tar_err"
|
||
fi
|
||
ok "打包完成:$archive ($(du -h "$archive" | awk '{print $1}')),总耗时 $(( $(date +%s) - start_ts ))s"
|
||
|
||
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 -d(macOS 旧版本不支持)。
|
||
# -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: 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 ----------
|
||
|
||
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
|
||
;;
|
||
*) err "不支持的系统:$OS_NAME"; return 1 ;;
|
||
esac
|
||
log "SMB 工具:$SMB_TOOL"
|
||
return 0
|
||
}
|
||
|
||
smb_validate() {
|
||
dbg "smb_validate: HOST='$SMB_HOST' SHARE='$SMB_SHARE' USER='$SMB_USER' PATH='$SMB_PATH'"
|
||
[[ -z "$SMB_HOST" ]] && { err "缺少 SMB_HOST"; return 1; }
|
||
[[ -z "$SMB_SHARE" ]] && { err "缺少 SMB_SHARE"; return 1; }
|
||
[[ -z "$SMB_USER" ]] && { err "缺少 SMB_USER"; return 1; }
|
||
[[ -z "$SMB_PASSWORD" ]] && warn "SMB_PASSWORD 为空(如服务器允许匿名/空密码可忽略)"
|
||
return 0
|
||
}
|
||
|
||
smb_upload_smbclient() {
|
||
dbg "smb_upload_smbclient: 进入,本次文件数=${#ARCHIVE_FILES[@]} 备份目录=$ARCHIVE_BASENAME"
|
||
local remote_base="${SMB_PATH%/}"
|
||
local commands=""
|
||
|
||
# 逐级 mkdir 共享内的 SMB_PATH(可能不存在)
|
||
if [[ -n "$remote_base" ]]; then
|
||
local IFS=/
|
||
# shellcheck disable=SC2206
|
||
local segs=($remote_base)
|
||
unset IFS
|
||
local cur=""
|
||
for seg in "${segs[@]}"; do
|
||
[[ -z "$seg" ]] && continue
|
||
cur="${cur}${seg}"
|
||
commands+="mkdir \"${cur}\";"
|
||
cur="${cur}/"
|
||
done
|
||
commands+="cd \"${remote_base}\";"
|
||
fi
|
||
# 为本次备份创建独立子目录
|
||
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}" )
|
||
[[ -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}/"
|
||
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"
|
||
|
||
if [[ "$COMMON_RETENTION_DAYS" -gt 0 ]]; then
|
||
smb_retention_smbclient "${args[@]}"
|
||
else
|
||
dbg "smb_upload_smbclient: COMMON_RETENTION_DAYS=0,跳过远端清理"
|
||
fi
|
||
}
|
||
|
||
smb_retention_smbclient() {
|
||
local args=( "$@" )
|
||
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-"${COMMON_RETENTION_DAYS}"d +%s)
|
||
else
|
||
cutoff=$(date -d "-${COMMON_RETENTION_DAYS} days" +%s)
|
||
fi
|
||
log "清理远端早于 ${COMMON_RETENTION_DAYS} 天的备份目录"
|
||
dbg "cutoff=$cutoff ($(date -r "$cutoff" 2>/dev/null || date -d "@$cutoff" 2>/dev/null))"
|
||
|
||
# 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 " 远端目录 $dir -> ftime=$ftime"
|
||
if [[ "$ftime" -lt "$cutoff" ]]; then
|
||
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
|
||
}
|
||
|
||
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
|
||
smb_validate || return 1
|
||
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
|
||
ok "SMB 上传完成"
|
||
cleanup_local
|
||
}
|
||
|
||
# ---------- SFTP(预留)----------
|
||
|
||
run_sftp() {
|
||
err "SFTP 方式尚未实现,已预留入口(参见 run_sftp 函数与 SFTP_* 配置)"
|
||
return 1
|
||
}
|
||
|
||
# ---------- 入口 ----------
|
||
|
||
main() {
|
||
# 提前处理 --debug,便于尽早开启日志
|
||
local a
|
||
for a in "$@"; do
|
||
[[ "$a" == "--debug" ]] && DEBUG="true"
|
||
done
|
||
|
||
print_runtime_env "$@"
|
||
|
||
parse_args "$@" || exit 1
|
||
|
||
print_effective_config
|
||
|
||
case "$METHOD" in
|
||
smb) run_smb ;;
|
||
sftp) run_sftp ;;
|
||
*) err "不支持的方式:$METHOD"; exit 1 ;;
|
||
esac
|
||
local rc=$?
|
||
dbg "main: 退出码=$rc"
|
||
exit $rc
|
||
}
|
||
|
||
main "$@"
|