feat: 更新
这个提交包含在:
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
- bash 3.2+ / tar / date / split
|
- bash 3.2+ / tar / date / split
|
||||||
- `sha256sum`(Linux 自带,属于 coreutils)或 `shasum`(macOS 自带)
|
- `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` 配好远端
|
- 上传 rclone:`rclone`,且需提前用 `rclone config` 配好远端
|
||||||
|
|
||||||
### 安装依赖
|
### 安装依赖
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
| Debian / Ubuntu | `apt install -y smbclient` | `apt install -y rclone` 或官方脚本 |
|
| Debian / Ubuntu | `apt install -y smbclient` | `apt install -y rclone` 或官方脚本 |
|
||||||
| RHEL / Rocky / Alma | `dnf install -y samba-client` | `dnf install -y rclone` 或官方脚本 |
|
| RHEL / Rocky / Alma | `dnf install -y samba-client` | `dnf install -y rclone` 或官方脚本 |
|
||||||
| Arch | `pacman -S smbclient` | `pacman -S 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` |
|
| 通用 | — | `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`。
|
- 命令行密码会出现在进程列表中,安全敏感场景请优先使用 `backup.conf`,并把权限收紧:`chmod 600 backup.conf`。
|
||||||
- macOS 上若未安装 `smbclient`,会回退到挂载方式(`mount_smbfs`),此时不支持 `--retention` 远端清理,仅完成上传。
|
- macOS 上必须安装 `smbclient`(`brew install samba`),脚本不再使用 `mount_smbfs` 回退(旧行为不支持远端清理且语义与 Linux 不一致)。
|
||||||
- 远端清理仅清理符合 `${COMMON_ARCHIVE_PREFIX}-YYYYmmdd-HHMMSS` 命名规范的目录,避免误删其它内容。
|
- 远端清理仅清理符合 `${COMMON_ARCHIVE_PREFIX}-YYYYmmdd-HHMMSS` 命名规范的目录,避免误删其它内容。
|
||||||
- rclone 远端必须先在本机用 `rclone config` 配好;自定义 `RCLONE_CONFIG` 路径需保证脚本运行用户可读。
|
- rclone 远端必须先在本机用 `rclone config` 配好;自定义 `RCLONE_CONFIG` 路径需保证脚本运行用户可读。
|
||||||
- 脚本不在上传后做远端 SHA256 校验(不同后端对 hash 的支持差异太大)。如需校验,恢复时进入备份目录用 `sha256sum -c` 对照同目录下的 `.sha256` 清单即可。
|
- 脚本不在上传后做远端 SHA256 校验(不同后端对 hash 的支持差异太大)。如需校验,恢复时进入备份目录用 `sha256sum -c` 对照同目录下的 `.sha256` 清单即可。
|
||||||
|
|||||||
164
backup/backup.sh
164
backup/backup.sh
@@ -459,29 +459,18 @@ cleanup_local() {
|
|||||||
smb_check_deps() {
|
smb_check_deps() {
|
||||||
dbg "smb_check_deps: OS=$OS_NAME"
|
dbg "smb_check_deps: OS=$OS_NAME"
|
||||||
case "$OS_NAME" in
|
case "$OS_NAME" in
|
||||||
Linux)
|
Linux|Darwin) ;;
|
||||||
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 ;;
|
*) err "不支持的系统:$OS_NAME"; return 1 ;;
|
||||||
esac
|
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"
|
log "SMB 工具:$SMB_TOOL"
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -498,9 +487,21 @@ smb_validate() {
|
|||||||
smb_upload_smbclient() {
|
smb_upload_smbclient() {
|
||||||
dbg "smb_upload_smbclient: 进入,本次文件数=${#ARCHIVE_FILES[@]} 备份目录=$ARCHIVE_BASENAME"
|
dbg "smb_upload_smbclient: 进入,本次文件数=${#ARCHIVE_FILES[@]} 备份目录=$ARCHIVE_BASENAME"
|
||||||
local remote_base="${SMB_PATH%/}"
|
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
|
if [[ -n "$remote_base" ]]; then
|
||||||
local IFS=/
|
local IFS=/
|
||||||
# shellcheck disable=SC2206
|
# shellcheck disable=SC2206
|
||||||
@@ -510,35 +511,38 @@ smb_upload_smbclient() {
|
|||||||
for seg in "${segs[@]}"; do
|
for seg in "${segs[@]}"; do
|
||||||
[[ -z "$seg" ]] && continue
|
[[ -z "$seg" ]] && continue
|
||||||
cur="${cur}${seg}"
|
cur="${cur}${seg}"
|
||||||
commands+="mkdir \"${cur}\";"
|
mkdir_cmd+="mkdir \"${cur}\";"
|
||||||
cur="${cur}/"
|
cur="${cur}/"
|
||||||
done
|
done
|
||||||
commands+="cd \"${remote_base}\";"
|
mkdir_cmd+="cd \"${remote_base}\";"
|
||||||
fi
|
fi
|
||||||
# 为本次备份创建独立子目录
|
mkdir_cmd+="mkdir \"${ARCHIVE_BASENAME}\";"
|
||||||
commands+="mkdir \"${ARCHIVE_BASENAME}\";cd \"${ARCHIVE_BASENAME}\";"
|
|
||||||
|
|
||||||
local pp pname
|
log "通过 smbclient 上传到 //${SMB_HOST}/${SMB_SHARE}/${archive_dir}/(共 ${#ARCHIVE_FILES[@]} 个文件)"
|
||||||
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//./}}"
|
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
|
dbg "smbclient mkdir 命令串=$mkdir_cmd"
|
||||||
err "smbclient 上传失败"
|
# mkdir 失败可能是已存在,不致命;smbclient 不会非 0 退出,这里只做一次连接确保目录就绪
|
||||||
return 1
|
smbclient "${args[@]}" "${sm_debug[@]}" -c "$mkdir_cmd" >/dev/null 2>&1 || true
|
||||||
fi
|
|
||||||
dbg "smb_upload_smbclient: 上传 OK"
|
# 第 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
|
if [[ "$COMMON_RETENTION_DAYS" -gt 0 ]]; then
|
||||||
smb_retention_smbclient "${args[@]}"
|
smb_retention_smbclient "${args[@]}"
|
||||||
@@ -584,43 +588,6 @@ smb_retention_smbclient() {
|
|||||||
done
|
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() {
|
run_smb() {
|
||||||
dbg "run_smb: 开始"
|
dbg "run_smb: 开始"
|
||||||
smb_check_deps || return 1
|
smb_check_deps || return 1
|
||||||
@@ -628,13 +595,8 @@ run_smb() {
|
|||||||
print_effective_config
|
print_effective_config
|
||||||
create_archive || return 1
|
create_archive || return 1
|
||||||
|
|
||||||
if [[ "$SMB_TOOL" == "smbclient" ]]; then
|
smb_upload_smbclient || return 1
|
||||||
smb_upload_smbclient || { cleanup_local; return 1; }
|
|
||||||
else
|
|
||||||
smb_upload_mount || { cleanup_local; return 1; }
|
|
||||||
fi
|
|
||||||
ok "SMB 上传完成"
|
ok "SMB 上传完成"
|
||||||
cleanup_local
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------- rclone ----------
|
# ---------- rclone ----------
|
||||||
@@ -697,26 +659,29 @@ rclone_upload() {
|
|||||||
dbg "rclone_upload: 待上传=${#ARCHIVE_FILES[@]} 远端目录=${RCLONE_REMOTE}:${RCLONE_PATH%/}/${ARCHIVE_BASENAME}/"
|
dbg "rclone_upload: 待上传=${#ARCHIVE_FILES[@]} 远端目录=${RCLONE_REMOTE}:${RCLONE_PATH%/}/${ARCHIVE_BASENAME}/"
|
||||||
local remote_dir="${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 级别压住。
|
# 让 rclone 每 5 秒打印一行进度(已传/总量/百分比/速率/ETA),写到 stdout 而不是被 INFO 级别压住。
|
||||||
# --stats-one-line 让进度落在单行;NOTICE 级别能在非交互(cron)下也输出。
|
# --stats-one-line 让进度落在单行;NOTICE 级别能在非交互(cron)下也输出。
|
||||||
local stats_flags=( --stats=5s --stats-one-line --stats-log-level NOTICE )
|
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
|
local pp pname pkb start_ts elapsed rc
|
||||||
for pp in "${ARCHIVE_FILES[@]}"; do
|
for pp in "${ARCHIVE_FILES[@]}"; do
|
||||||
|
idx=$((idx+1))
|
||||||
pname="$(basename "$pp")"
|
pname="$(basename "$pp")"
|
||||||
pkb="$(du -k "$pp" 2>/dev/null | awk '{print $1+0}')"
|
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)
|
start_ts=$(date +%s)
|
||||||
rclone_cmd copyto "${stats_flags[@]}" "$pp" "${remote_dir}/${pname}"
|
rclone_cmd copyto "${stats_flags[@]}" "$pp" "${remote_dir}/${pname}"
|
||||||
rc=$?
|
rc=$?
|
||||||
elapsed=$(( $(date +%s) - start_ts ))
|
elapsed=$(( $(date +%s) - start_ts ))
|
||||||
if [[ $rc -ne 0 ]]; then
|
if [[ $rc -ne 0 ]]; then
|
||||||
err "rclone 上传失败:$pp (耗时 ${elapsed}s)"
|
err "上传失败 [${idx}/${total}]:$pp (耗时 ${elapsed}s)"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
ok "上传完成:$pname (耗时 ${elapsed}s)"
|
ok "上传完成 [${idx}/${total}]:$pname (耗时 ${elapsed}s)"
|
||||||
done
|
done
|
||||||
ok "已上传全部 ${#ARCHIVE_FILES[@]} 个文件到 ${remote_dir}/"
|
ok "已上传全部 ${total} 个文件到 ${remote_dir}/"
|
||||||
|
|
||||||
if [[ "$COMMON_RETENTION_DAYS" -gt 0 ]]; then
|
if [[ "$COMMON_RETENTION_DAYS" -gt 0 ]]; then
|
||||||
rclone_retention
|
rclone_retention
|
||||||
@@ -772,9 +737,8 @@ run_rclone() {
|
|||||||
print_effective_config
|
print_effective_config
|
||||||
create_archive || return 1
|
create_archive || return 1
|
||||||
|
|
||||||
rclone_upload || { cleanup_local; return 1; }
|
rclone_upload || return 1
|
||||||
ok "rclone 上传完成"
|
ok "rclone 上传完成"
|
||||||
cleanup_local
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------- SFTP(预留)----------
|
# ---------- SFTP(预留)----------
|
||||||
@@ -797,6 +761,12 @@ main() {
|
|||||||
|
|
||||||
parse_args "$@" || exit 1
|
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
|
print_effective_config
|
||||||
|
|
||||||
case "$METHOD" in
|
case "$METHOD" in
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户