feat: 修改备份脚本
这个提交包含在:
138
backup/README.md
138
backup/README.md
@@ -6,7 +6,8 @@
|
||||
|
||||
## 系统要求
|
||||
|
||||
- bash 3.2+ / tar / date
|
||||
- bash 3.2+ / tar / date / split
|
||||
- `sha256sum`(Linux 自带,属于 coreutils)或 `shasum`(macOS 自带)
|
||||
- Linux:`smbclient`(包含在 `smbclient` 或 `samba-client` 包中)
|
||||
- macOS:`smbclient`(推荐,`brew install samba`),或退回到系统自带的 `mount_smbfs`
|
||||
|
||||
@@ -74,21 +75,33 @@ bash backup.sh smb
|
||||
|
||||
脚本默认读取 **脚本所在目录** 下的 `backup.conf`,可用 `-C` / `--config` 指定其它路径。
|
||||
|
||||
配置分三段:**公共配置**(`COMMON_*`,所有方式都用)、**SMB 段**、**SFTP 段**。备份方式由命令行第一个位置参数决定(`smb` / `sftp`),脚本只会读取该方式对应段的配置。`backup.conf` 里没有 `METHOD` 字段。
|
||||
|
||||
### 公共配置(COMMON_*)
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `COMMON_SOURCE_PATHS` | 要备份的源路径,多个用空格分隔,需引号包裹 |
|
||||
| `COMMON_TMP_DIR` | 本地临时打包目录,默认 `/tmp/backup` |
|
||||
| `COMMON_ARCHIVE_PREFIX` | 归档/远端目录命名前缀,最终形如 `prefix-YYYYmmdd-HHMMSS/` |
|
||||
| `COMMON_CLEAN_LOCAL` | 上传后是否删除本地归档(`true` / `false`) |
|
||||
| `COMMON_RETENTION_DAYS` | 远端保留天数,`0` 表示不清理;按目录整体清理 |
|
||||
| `COMMON_SPLIT_SIZE` | 分卷大小(默认 `1G`;`500M` / `100k` 等),留空字符串不分卷 |
|
||||
|
||||
### SMB 段
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `SOURCE_PATHS` | 要备份的源路径,多个用空格分隔,需引号包裹 |
|
||||
| `TMP_DIR` | 本地临时打包目录,默认 `/tmp/backup` |
|
||||
| `ARCHIVE_PREFIX` | 归档文件名前缀,最终形如 `prefix-YYYYmmdd-HHMMSS.tar.gz` |
|
||||
| `METHOD` | 默认上传方式(`smb` / `sftp`),命令行参数会覆盖 |
|
||||
| `CLEAN_LOCAL` | 上传后是否删除本地归档(`true` / `false`) |
|
||||
| `RETENTION_DAYS` | 远端保留天数,`0` 表示不清理 |
|
||||
| `SMB_HOST` | SMB 服务器地址 |
|
||||
| `SMB_SHARE` | 共享名 |
|
||||
| `SMB_PATH` | 共享内的子目录(可选) |
|
||||
| `SMB_USER` / `SMB_PASSWORD` | 凭据 |
|
||||
| `SMB_DOMAIN` | 域 / 工作组(可选) |
|
||||
| `SMB_VERSION` | SMB 协议版本,如 `3.0`(可选) |
|
||||
| `SFTP_*` | 预留字段,目前未启用 |
|
||||
|
||||
### SFTP 段(预留,暂未实现)
|
||||
|
||||
`SFTP_HOST` / `SFTP_PORT` / `SFTP_USER` / `SFTP_PASSWORD` / `SFTP_KEY` / `SFTP_PATH`
|
||||
|
||||
## 命令行参数
|
||||
|
||||
@@ -99,11 +112,12 @@ bash backup.sh smb
|
||||
| 参数 | 对应配置 |
|
||||
| --- | --- |
|
||||
| `-C, --config FILE` | 指定配置文件路径 |
|
||||
| `-s, --source "P1 P2"` | `SOURCE_PATHS` |
|
||||
| `-t, --tmp-dir DIR` | `TMP_DIR` |
|
||||
| `-p, --prefix NAME` | `ARCHIVE_PREFIX` |
|
||||
| `--keep-local` | 等价于 `CLEAN_LOCAL=false` |
|
||||
| `--retention DAYS` | `RETENTION_DAYS` |
|
||||
| `-s, --source "P1 P2"` | `COMMON_SOURCE_PATHS` |
|
||||
| `-t, --tmp-dir DIR` | `COMMON_TMP_DIR` |
|
||||
| `-p, --prefix NAME` | `COMMON_ARCHIVE_PREFIX` |
|
||||
| `--keep-local` | 等价于 `COMMON_CLEAN_LOCAL=false` |
|
||||
| `--retention DAYS` | `COMMON_RETENTION_DAYS` |
|
||||
| `--split-size SIZE` | `COMMON_SPLIT_SIZE` |
|
||||
| `--debug` | 打印详细调试日志(也可用 `DEBUG=true` 环境变量) |
|
||||
| `-h, --help` | 显示帮助 |
|
||||
|
||||
@@ -160,21 +174,111 @@ bash backup.sh smb -s "/var/lib/mysql"
|
||||
|
||||
# 每天 03:17 跑一次
|
||||
17 3 * * * /bin/bash /path/to/backup/backup.sh smb > /var/log/backup.log 2>&1
|
||||
|
||||
# 每两天 03:10 跑一次(按月内的奇数日触发:1、3、5……29、31)
|
||||
10 3 */2 * * /bin/bash /path/to/backup/backup.sh smb > /var/log/backup.log 2>&1
|
||||
```
|
||||
|
||||
> `*/2` 是按月内日期号取模,并不是严格意义的「每 48 小时」。在 31 天月份的月末会出现 31 → 次月 1 号连续两天都触发的情况;如对间隔严格要求,建议改用 systemd timer 的 `OnUnitActiveSec=2d`。
|
||||
|
||||
> 如果有多条任务都写到同一个日志文件,请改用不同的日志路径(例如 `/var/log/backup-0310.log`、`/var/log/backup-0317.log`),否则后一次会覆盖前一次。
|
||||
|
||||
macOS 可用 `launchd` 或 `cron`(需要在「系统设置 → 隐私与安全性 → 完全磁盘访问权限」中授予 `cron` 权限以读取受保护目录)。
|
||||
|
||||
## 路径与文件
|
||||
## 远端目录结构
|
||||
|
||||
每次备份在远端创建一个独立子目录,内含分卷文件与 SHA256 清单:
|
||||
|
||||
```
|
||||
//${SMB_HOST}/${SMB_SHARE}/${SMB_PATH}/
|
||||
└── backup-20260426-031000/ # 本次备份目录
|
||||
├── backup-20260426-031000.tar.gz.0 # 分卷 0
|
||||
├── backup-20260426-031000.tar.gz.1 # 分卷 1
|
||||
├── backup-20260426-031000.tar.gz.2
|
||||
├── ...
|
||||
└── backup-20260426-031000.sha256 # SHA256 清单(覆盖所有分卷)
|
||||
```
|
||||
|
||||
未启用分卷(`COMMON_SPLIT_SIZE=""`)时目录里只有完整归档加清单:
|
||||
|
||||
```
|
||||
backup-20260426-031000/
|
||||
├── backup-20260426-031000.tar.gz
|
||||
└── backup-20260426-031000.sha256
|
||||
```
|
||||
|
||||
本地路径:
|
||||
|
||||
| 路径 | 说明 |
|
||||
| --- | --- |
|
||||
| `${TMP_DIR}/${ARCHIVE_PREFIX}-YYYYmmdd-HHMMSS.tar.gz` | 本地临时归档(默认上传后删除) |
|
||||
| 远端 `//${SMB_HOST}/${SMB_SHARE}/${SMB_PATH}/` | 上传目标目录 |
|
||||
| `${COMMON_TMP_DIR}/${COMMON_ARCHIVE_PREFIX}-YYYYmmdd-HHMMSS.tar.gz` | 未分卷时的归档;分卷后会被删除 |
|
||||
| `${COMMON_TMP_DIR}/${COMMON_ARCHIVE_PREFIX}-YYYYmmdd-HHMMSS.tar.gz.0` ... | 分卷文件(默认上传后删除) |
|
||||
| `${COMMON_TMP_DIR}/${COMMON_ARCHIVE_PREFIX}-YYYYmmdd-HHMMSS.sha256` | 校验清单 |
|
||||
|
||||
## 分卷(COMMON_SPLIT_SIZE)
|
||||
|
||||
当单个归档过大、SMB 上传容易中途失败、或目标文件系统有单文件大小限制时启用分卷。**默认 `1G`。**
|
||||
|
||||
```bash
|
||||
# 配置文件
|
||||
COMMON_SPLIT_SIZE="1G"
|
||||
|
||||
# 或命令行
|
||||
bash backup.sh smb --split-size 500M
|
||||
|
||||
# 关闭分卷
|
||||
bash backup.sh smb --split-size ''
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- 分卷需要本地临时空间约为「归档体积 × 2」(先生成完整 `.tar.gz`,再 split)。磁盘紧张时把 `COMMON_TMP_DIR` 指到大盘。
|
||||
- 远端清理(`COMMON_RETENTION_DAYS`)按 **目录** 清理:以 `${COMMON_ARCHIVE_PREFIX}-YYYYmmdd-HHMMSS` 命名的目录里所有内容(含 sha256)会一并删除。
|
||||
|
||||
## 校验与恢复
|
||||
|
||||
每次备份都会生成 `<basename>.sha256` 清单(标准 `sha256sum` 格式:`<hash> <filename>`),命名都用相对文件名,因此**恢复时进入备份目录直接校验即可**。
|
||||
|
||||
进入备份目录:
|
||||
|
||||
```bash
|
||||
cd /path/to/backup-20260426-031000/
|
||||
```
|
||||
|
||||
校验完整性:
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
sha256sum -c backup-20260426-031000.sha256
|
||||
|
||||
# macOS
|
||||
shasum -a 256 -c backup-20260426-031000.sha256
|
||||
```
|
||||
|
||||
合并分卷并解压(一步到位):
|
||||
|
||||
```bash
|
||||
cat $(ls *.tar.gz.* | sort -V) | tar -xzf -
|
||||
```
|
||||
|
||||
> `sort -V` 是 version-sort(自然数序),可正确排序 `.tar.gz.0 .tar.gz.1 ... .tar.gz.10 .tar.gz.11`。
|
||||
> 直接 `cat *.tar.gz.*` 走的是 shell 字典序,会把 `.10` 排在 `.2` 前面,**会损坏归档**。
|
||||
|
||||
如果只有一个文件(未分卷):
|
||||
|
||||
```bash
|
||||
tar -xzf backup-20260426-031000.tar.gz
|
||||
```
|
||||
|
||||
或者先合并成一个文件再解压:
|
||||
|
||||
```bash
|
||||
cat $(ls *.tar.gz.* | sort -V) > backup-20260426-031000.tar.gz
|
||||
tar -xzf backup-20260426-031000.tar.gz
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 命令行密码会出现在进程列表中,安全敏感场景请优先使用 `backup.conf`,并把权限收紧:`chmod 600 backup.conf`。
|
||||
- macOS 上若未安装 `smbclient`,会回退到挂载方式(`mount_smbfs`),此时不支持 `--retention` 远端清理,仅完成上传。
|
||||
- 远端清理仅清理符合 `${ARCHIVE_PREFIX}-YYYYmmdd-HHMMSS.tar.gz` 命名规范的文件,避免误删其它内容。
|
||||
- 远端清理仅清理符合 `${COMMON_ARCHIVE_PREFIX}-YYYYmmdd-HHMMSS` 命名规范的目录,避免误删其它内容。
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
# backup.sh 配置文件
|
||||
# 所有配置项均可通过命令行参数覆盖(参数优先级高于本文件)
|
||||
# 所有配置项均可通过命令行参数覆盖(参数优先级高于本文件)。
|
||||
#
|
||||
# 备份方式由命令行第一个位置参数决定:
|
||||
# bash backup.sh smb -> 使用本文件 SMB 段配置
|
||||
# bash backup.sh sftp -> 使用本文件 SFTP 段配置(暂未实现)
|
||||
|
||||
# ===== 通用配置 =====
|
||||
# ===== 公共配置(COMMON_*)=====
|
||||
|
||||
# 要备份的源目录或文件(多个用空格分隔,需引号包裹)
|
||||
SOURCE_PATHS="/etc /var/log"
|
||||
COMMON_SOURCE_PATHS="/opt"
|
||||
|
||||
# 本地临时打包目录(用于存放归档文件,备份完成后会清理)
|
||||
TMP_DIR="/tmp/backup"
|
||||
# 本地临时打包目录(用于存放归档/分卷/sha 文件,备份完成后会清理)
|
||||
COMMON_TMP_DIR="/tmp/backup"
|
||||
|
||||
# 归档文件名前缀,最终文件名形如 ${ARCHIVE_PREFIX}-YYYYmmdd-HHMMSS.tar.gz
|
||||
ARCHIVE_PREFIX="backup"
|
||||
|
||||
# 备份方式:smb / sftp(sftp 暂未实现)
|
||||
METHOD="smb"
|
||||
# 归档/远端目录命名前缀,最终形如 ${COMMON_ARCHIVE_PREFIX}-YYYYmmdd-HHMMSS/
|
||||
COMMON_ARCHIVE_PREFIX="backup"
|
||||
|
||||
# 上传完成后是否删除本地归档(true / false)
|
||||
CLEAN_LOCAL="true"
|
||||
COMMON_CLEAN_LOCAL="true"
|
||||
|
||||
# 远端保留天数,0 表示不清理
|
||||
RETENTION_DAYS=15
|
||||
# 远端保留天数,0 表示不清理;按 ${COMMON_ARCHIVE_PREFIX}-YYYYmmdd-HHMMSS 目录整体清理
|
||||
COMMON_RETENTION_DAYS=15
|
||||
|
||||
# ===== SMB / Samba 配置 =====
|
||||
# 分卷大小,默认 1G;留空表示不分卷;非空时形如 1G / 500M / 100k
|
||||
# 文件名形如 backup-YYYYmmdd-HHMMSS.tar.gz.0 / .1 / .2 ...
|
||||
# 同目录下还会生成 backup-YYYYmmdd-HHMMSS.sha256(覆盖所有分卷的校验清单)
|
||||
#
|
||||
# 恢复(在备份目录里执行):
|
||||
# sha256sum -c backup-YYYYmmdd-HHMMSS.sha256 # 校验
|
||||
# cat $(ls *.tar.gz.* | sort -V) | tar -xzf - # 合并并解压
|
||||
COMMON_SPLIT_SIZE="1G"
|
||||
|
||||
# ===== SMB / Samba 配置(仅 bash backup.sh smb 使用)=====
|
||||
|
||||
# 服务器地址,例如 192.168.1.10 或 nas.local
|
||||
SMB_HOST=""
|
||||
@@ -42,7 +52,7 @@ SMB_DOMAIN=""
|
||||
# SMB 协议版本(可选),例如 3.0
|
||||
SMB_VERSION=""
|
||||
|
||||
# ===== SFTP 配置(预留,暂未实现)=====
|
||||
# ===== SFTP 配置(仅 bash backup.sh sftp 使用,预留,暂未实现)=====
|
||||
SFTP_HOST=""
|
||||
SFTP_PORT="22"
|
||||
SFTP_USER=""
|
||||
|
||||
247
backup/backup.sh
247
backup/backup.sh
@@ -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,11 +133,14 @@ 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 "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"
|
||||
@@ -136,6 +148,16 @@ print_effective_config() {
|
||||
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,请安装 coreutils(macOS 自带 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 -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: 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"
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户