mirror of
https://github.com/wolfydw/backup_OBS.git
synced 2026-05-08 06:37:02 +08:00
Refactor backup config and Telegram notifications
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
env.conf
|
env.conf
|
||||||
backup.log
|
backup.log
|
||||||
|
exclude.user.list
|
||||||
|
|||||||
@@ -3,24 +3,19 @@
|
|||||||
# - 从同目录 env.conf 加载配置,支持命令行覆盖
|
# - 从同目录 env.conf 加载配置,支持命令行覆盖
|
||||||
# - 打包多目录(支持 pigz),生成 SHA256 校验,obsutil 分片/并发上传
|
# - 打包多目录(支持 pigz),生成 SHA256 校验,obsutil 分片/并发上传
|
||||||
# - 上传 .tar.gz 以及 .tar.gz.sha256,并下载远端 .sha256 比对确保完整性
|
# - 上传 .tar.gz 以及 .tar.gz.sha256,并下载远端 .sha256 比对确保完整性
|
||||||
# - 成功/失败写入日志并 Telegram 通知(Markdown)
|
# - 成功/失败写入日志并 Telegram 通知
|
||||||
# - 支持 --dry-run / --verify / --keep-local / --concurrency / --rate / --label / --sse
|
# - 支持 --dry-run / --verify / --keep-local / --concurrency / --rate / --label / --sse
|
||||||
# 退出码:0 成功;2 配置缺失;3 打包失败;4 上传失败;5 校验失败
|
# 退出码:0 成功;2 配置缺失;3 打包失败;4 上传失败;5 校验失败
|
||||||
|
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
#############################
|
|
||||||
# 通用工具函数
|
|
||||||
#############################
|
|
||||||
# 兼容性更好的脚本路径解析(在 set -u 下也安全)
|
|
||||||
SCRIPT_PATH="${BASH_SOURCE[0]:-$0}"
|
SCRIPT_PATH="${BASH_SOURCE[0]:-$0}"
|
||||||
SCRIPT_DIR="$(cd "$(dirname -- "$SCRIPT_PATH")" && pwd -P)"
|
SCRIPT_DIR="$(cd "$(dirname -- "$SCRIPT_PATH")" && pwd -P)"
|
||||||
export PATH="$SCRIPT_DIR:$PATH"
|
export PATH="$SCRIPT_DIR:$PATH"
|
||||||
cd "$SCRIPT_DIR"
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
# 记录日志到文件与终端
|
|
||||||
log_ts() { date '+%Y-%m-%d %H:%M:%S'; }
|
|
||||||
LOG_PREFIX="[BACKUP]"
|
LOG_PREFIX="[BACKUP]"
|
||||||
|
log_ts() { date '+%Y-%m-%d %H:%M:%S'; }
|
||||||
log() {
|
log() {
|
||||||
local level="$1"; shift
|
local level="$1"; shift
|
||||||
local line
|
local line
|
||||||
@@ -28,82 +23,20 @@ log() {
|
|||||||
echo "$line" | tee -a "${LOG_FILE:-./backup.log}"
|
echo "$line" | tee -a "${LOG_FILE:-./backup.log}"
|
||||||
}
|
}
|
||||||
|
|
||||||
#############################
|
# shellcheck disable=SC1091
|
||||||
# Telegram 通知(Markdown)
|
source "${SCRIPT_DIR}/lib/telegram.sh"
|
||||||
#############################
|
|
||||||
send_telegram() {
|
|
||||||
local status="${1-}" # success | error | dryrun
|
|
||||||
local reason="${2-}" # 错误原因(可选)
|
|
||||||
local error_log="${3-}" # 详细错误日志(可选)
|
|
||||||
|
|
||||||
if [[ -z "${TG_BOT_TOKEN:-}" || -z "${TG_USER_ID:-}" ]]; then
|
|
||||||
log "INFO" "未配置Telegram通知,跳过发送"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
local api="https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage"
|
|
||||||
local current_date; current_date="$(date +'%Y-%m-%d %H:%M:%S')"
|
|
||||||
local message
|
|
||||||
|
|
||||||
if [[ "$status" == "success" ]]; then
|
|
||||||
message="*备份成功通知*\n\n"
|
|
||||||
message+="备份主机:${LABEL}\n"
|
|
||||||
message+="备份时间:${current_date}\n"
|
|
||||||
message+="备份内容:${BACKUP_DIR}\n"
|
|
||||||
message+="文件大小:${BACKUP_SIZE_HUMAN}\n"
|
|
||||||
message+="存储位置:obs://${OBS_BUCKET}/${OBS_BACKUP_DIR}/${ARCHIVE_BASE}\n"
|
|
||||||
message+="打包用时:${PACK_SECONDS}s\n"
|
|
||||||
message+="上传用时:${UP_SECONDS}s\n"
|
|
||||||
message+="平均速率:${SPEED_HUMAN}"
|
|
||||||
elif [[ "$status" == "dryrun" ]]; then
|
|
||||||
message="*备份自检(Dry-run)*\n\n"
|
|
||||||
message+="备份主机:${LABEL}\n"
|
|
||||||
message+="备份时间:${current_date}\n"
|
|
||||||
message+="备份内容:${BACKUP_DIR}\n"
|
|
||||||
message+="归档名称:${ARCHIVE_BASE}\n"
|
|
||||||
message+="文件大小:${BACKUP_SIZE_HUMAN}\n"
|
|
||||||
message+="打包用时:${PACK_SECONDS}s"
|
|
||||||
else
|
|
||||||
message="*备份失败通知*\n\n${LABEL} 备份失败!"
|
|
||||||
if [[ -n "$reason" ]]; then
|
|
||||||
message+="\n原因:${reason}"
|
|
||||||
fi
|
|
||||||
if [[ -n "$error_log" ]]; then
|
|
||||||
message+="\n错误日志:\n\`\`\`${error_log}\`\`\`"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 仅把字面 \n 转成 %0A;保留其他字符原样
|
|
||||||
local encoded_message=${message//\\n/%0A}
|
|
||||||
|
|
||||||
local response
|
|
||||||
response="$(curl -sS -X POST "$api" \
|
|
||||||
-d "chat_id=${TG_USER_ID}" \
|
|
||||||
-d "parse_mode=Markdown" \
|
|
||||||
-d "disable_web_page_preview=true" \
|
|
||||||
-d "text=${encoded_message}" 2>&1 || true)"
|
|
||||||
|
|
||||||
if echo "$response" | grep -q '"ok":true'; then
|
|
||||||
log "INFO" "Telegram通知发送成功"
|
|
||||||
else
|
|
||||||
log "WARN" "Telegram通知发送失败: $response"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 失败统一处理(触发 trap)
|
|
||||||
on_error() {
|
on_error() {
|
||||||
local exit_code=$?
|
local exit_code=$?
|
||||||
local line_no=${BASH_LINENO[0]:-}
|
local line_no=${BASH_LINENO[0]:-}
|
||||||
log "ERROR" "脚本异常退出(exit=$exit_code, line=${line_no})."
|
|
||||||
local tail_log
|
local tail_log
|
||||||
|
log "ERROR" "脚本异常退出(exit=$exit_code, line=${line_no})."
|
||||||
tail_log="$(tail -n 80 "${LOG_FILE:-./backup.log}" 2>/dev/null || true)"
|
tail_log="$(tail -n 80 "${LOG_FILE:-./backup.log}" 2>/dev/null || true)"
|
||||||
send_telegram "error" "脚本异常退出(exit=${exit_code}, line=${line_no})" "$tail_log"
|
tg_backup_error "$LABEL" "$(date +'%Y-%m-%d %H:%M:%S')" "脚本异常退出(exit=${exit_code}, line=${line_no})" "$tail_log"
|
||||||
exit "$exit_code"
|
exit "$exit_code"
|
||||||
}
|
}
|
||||||
trap on_error ERR
|
trap on_error ERR
|
||||||
|
|
||||||
# 命令可用性检查
|
|
||||||
need_cmd() {
|
need_cmd() {
|
||||||
command -v "$1" >/dev/null 2>&1 || {
|
command -v "$1" >/dev/null 2>&1 || {
|
||||||
log "ERROR" "未找到命令:$1"
|
log "ERROR" "未找到命令:$1"
|
||||||
@@ -111,7 +44,6 @@ need_cmd() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# 通用重试(指数回退 2^n 秒)
|
|
||||||
retry() {
|
retry() {
|
||||||
local max="$1"; shift
|
local max="$1"; shift
|
||||||
local desc="$1"; shift
|
local desc="$1"; shift
|
||||||
@@ -129,10 +61,6 @@ retry() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
#############################
|
|
||||||
# 加载配置与参数
|
|
||||||
#############################
|
|
||||||
|
|
||||||
CONF_FILE="${SCRIPT_DIR}/env.conf"
|
CONF_FILE="${SCRIPT_DIR}/env.conf"
|
||||||
if [[ ! -f "$CONF_FILE" ]]; then
|
if [[ ! -f "$CONF_FILE" ]]; then
|
||||||
echo "未找到配置文件 ${CONF_FILE},请先复制/编辑 env.conf" >&2
|
echo "未找到配置文件 ${CONF_FILE},请先复制/编辑 env.conf" >&2
|
||||||
@@ -142,14 +70,15 @@ fi
|
|||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
source "$CONF_FILE"
|
source "$CONF_FILE"
|
||||||
|
|
||||||
# 默认参数值(可被命令行覆盖)
|
BACKUP_PATHS=()
|
||||||
|
|
||||||
KEEP_LOCAL=false
|
KEEP_LOCAL=false
|
||||||
DRY_RUN=false
|
DRY_RUN=false
|
||||||
VERIFY_ONLY=false
|
VERIFY_ONLY=false
|
||||||
CLI_LABEL=""
|
CLI_LABEL=""
|
||||||
CLI_SSE="none" # none|kms|aes256
|
CLI_SSE="none"
|
||||||
CLI_CONCURRENCY="" # 映射到 -j
|
CLI_CONCURRENCY=""
|
||||||
CLI_RATE="" # 速率限制(示例 10MB/s)
|
CLI_RATE=""
|
||||||
GPG_RECIPIENT=""
|
GPG_RECIPIENT=""
|
||||||
GPG_SYM=false
|
GPG_SYM=false
|
||||||
|
|
||||||
@@ -164,7 +93,6 @@ usage() {
|
|||||||
USAGE
|
USAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
# 解析参数
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--dry-run) DRY_RUN=true; shift;;
|
--dry-run) DRY_RUN=true; shift;;
|
||||||
@@ -181,39 +109,52 @@ while [[ $# -gt 0 ]]; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# 应用 label 覆盖(仅影响远端前缀,不改变本地)
|
|
||||||
if [[ -n "$CLI_LABEL" ]]; then
|
if [[ -n "$CLI_LABEL" ]]; then
|
||||||
LABEL="$CLI_LABEL"
|
LABEL="$CLI_LABEL"
|
||||||
OBS_BACKUP_DIR="backup/${LABEL}"
|
OBS_BACKUP_DIR="backup/${LABEL}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 并发覆盖
|
|
||||||
if [[ -n "$CLI_CONCURRENCY" ]]; then
|
if [[ -n "$CLI_CONCURRENCY" ]]; then
|
||||||
OBS_JOBS="$CLI_CONCURRENCY"
|
OBS_JOBS="$CLI_CONCURRENCY"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 速率(obsutil 具体参数随版本可能不同,这里传递 --rate 或 -limit 的等价形式)
|
|
||||||
OBS_RATE_FLAG=""
|
OBS_RATE_FLAG=""
|
||||||
if [[ -n "${CLI_RATE:-}" ]]; then
|
if [[ -n "${CLI_RATE:-}" ]]; then
|
||||||
OBS_RATE_FLAG="--rate=${CLI_RATE}"
|
OBS_RATE_FLAG="--rate=${CLI_RATE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 路径与文件
|
DEFAULT_EXCLUDE_FILE="${SCRIPT_DIR}/exclude.list"
|
||||||
EXCLUDE_FILE="${SCRIPT_DIR}/exclude.list"
|
USER_EXCLUDE_FILE="${SCRIPT_DIR}/exclude.user.list"
|
||||||
[[ -f "$EXCLUDE_FILE" ]] || touch "$EXCLUDE_FILE"
|
[[ -f "$DEFAULT_EXCLUDE_FILE" ]] || touch "$DEFAULT_EXCLUDE_FILE"
|
||||||
|
[[ -f "$USER_EXCLUDE_FILE" ]] || cp -f "${SCRIPT_DIR}/exclude.user.list.example" "$USER_EXCLUDE_FILE"
|
||||||
|
|
||||||
# 基础校验
|
|
||||||
: "${OBS_BUCKET:?缺少 OBS_BUCKET}"
|
: "${OBS_BUCKET:?缺少 OBS_BUCKET}"
|
||||||
: "${OBS_BACKUP_DIR:?缺少 OBS_BACKUP_DIR}"
|
: "${OBS_BACKUP_DIR:?缺少 OBS_BACKUP_DIR}"
|
||||||
: "${BACKUP_DIR:?缺少 BACKUP_DIR}"
|
|
||||||
: "${LOG_FILE:?缺少 LOG_FILE}"
|
: "${LOG_FILE:?缺少 LOG_FILE}"
|
||||||
: "${OBS_PARALLEL:?缺少 OBS_PARALLEL}" "${OBS_JOBS:?缺少 OBS_JOBS}"
|
: "${OBS_PARALLEL:?缺少 OBS_PARALLEL}" "${OBS_JOBS:?缺少 OBS_JOBS}"
|
||||||
: "${OBS_PARTSIZE:?缺少 OBS_PARTSIZE}" "${OBS_THRESHOLD:?缺少 OBS_THRESHOLD}"
|
: "${OBS_PARTSIZE:?缺少 OBS_PARTSIZE}" "${OBS_THRESHOLD:?缺少 OBS_THRESHOLD}"
|
||||||
: "${RETAIN_DAYS:?缺少 RETAIN_DAYS}"
|
: "${RETAIN_DAYS:?缺少 RETAIN_DAYS}"
|
||||||
|
|
||||||
#############################
|
if [[ -z "${BACKUP_DIRS:-}" && -n "${BACKUP_DIR:-}" ]]; then
|
||||||
# 依赖检查与自检
|
BACKUP_DIRS="$(printf '%s\n' "$BACKUP_DIR")"
|
||||||
#############################
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${BACKUP_DIRS:-}" ]]; then
|
||||||
|
echo "缺少 BACKUP_DIRS 配置" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS= read -r path; do
|
||||||
|
[[ -n "$path" ]] || continue
|
||||||
|
[[ "$path" =~ ^[[:space:]]*# ]] && continue
|
||||||
|
BACKUP_PATHS+=("$path")
|
||||||
|
done <<< "$BACKUP_DIRS"
|
||||||
|
|
||||||
|
if [[ "${#BACKUP_PATHS[@]}" -eq 0 ]]; then
|
||||||
|
echo "BACKUP_DIRS 没有有效目录" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
need_cmd tar
|
need_cmd tar
|
||||||
need_cmd sha256sum
|
need_cmd sha256sum
|
||||||
need_cmd curl
|
need_cmd curl
|
||||||
@@ -225,7 +166,6 @@ if ! command -v obsutil >/dev/null 2>&1; then
|
|||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 严格按要求:配置文件位于 ~/.obsutilconfig
|
|
||||||
OBSUTIL_CONFIG_PATH="${HOME}/.obsutilconfig"
|
OBSUTIL_CONFIG_PATH="${HOME}/.obsutilconfig"
|
||||||
if [[ ! -f "$OBSUTIL_CONFIG_PATH" ]]; then
|
if [[ ! -f "$OBSUTIL_CONFIG_PATH" ]]; then
|
||||||
log "ERROR" "未检测到 obsutil 配置文件:${OBSUTIL_CONFIG_PATH}"
|
log "ERROR" "未检测到 obsutil 配置文件:${OBSUTIL_CONFIG_PATH}"
|
||||||
@@ -233,9 +173,6 @@ if [[ ! -f "$OBSUTIL_CONFIG_PATH" ]]; then
|
|||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
#############################
|
|
||||||
# verify-only: 远端可用性检查
|
|
||||||
#############################
|
|
||||||
if $VERIFY_ONLY; then
|
if $VERIFY_ONLY; then
|
||||||
log "INFO" "开始远端可用性检查(ls + stat)"
|
log "INFO" "开始远端可用性检查(ls + stat)"
|
||||||
set +e
|
set +e
|
||||||
@@ -256,39 +193,33 @@ if $VERIFY_ONLY; then
|
|||||||
fi
|
fi
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
#############################
|
|
||||||
# 打包准备
|
|
||||||
#############################
|
|
||||||
|
|
||||||
# 组装要打包的 -C 与相对路径参数
|
|
||||||
build_tar_file_list() {
|
build_tar_file_list() {
|
||||||
local args=()
|
local args=()
|
||||||
for p in $BACKUP_DIR; do
|
local p abs parent base
|
||||||
|
for p in "${BACKUP_PATHS[@]}"; do
|
||||||
if [[ ! -e "$p" ]]; then
|
if [[ ! -e "$p" ]]; then
|
||||||
log "WARN" "路径不存在,跳过:$p"
|
log "WARN" "路径不存在,跳过:$p"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
local abs; abs="$(readlink -f "$p")"
|
abs="$(readlink -f "$p")"
|
||||||
local parent; parent="$(dirname "$abs")"
|
parent="$(dirname "$abs")"
|
||||||
local base; base="$(basename "$abs")"
|
base="$(basename "$abs")"
|
||||||
args+=("-C" "$parent" "$base")
|
args+=("-C" "$parent" "$base")
|
||||||
done
|
done
|
||||||
printf '%s\n' "${args[@]}"
|
printf '%s\n' "${args[@]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 粗略空间检查
|
|
||||||
check_space() {
|
check_space() {
|
||||||
local total=0
|
local total=0
|
||||||
for p in $BACKUP_DIR; do
|
local p sz avail need
|
||||||
|
for p in "${BACKUP_PATHS[@]}"; do
|
||||||
[[ -e "$p" ]] || continue
|
[[ -e "$p" ]] || continue
|
||||||
local sz
|
sz="$(du -sb --apparent-size "$p" 2>/dev/null | cut -f1)"
|
||||||
sz=$(du -sb --apparent-size "$p" 2>/dev/null | awk '{s+=$1} END{print s+0}')
|
|
||||||
total=$((total + sz))
|
total=$((total + sz))
|
||||||
done
|
done
|
||||||
local avail
|
avail=$(df -P "$SCRIPT_DIR" | awk 'NR==2{print $4}')
|
||||||
avail=$(df -P "$SCRIPT_DIR" | awk 'NR==2{print $4}') # KB
|
|
||||||
avail=$((avail * 1024))
|
avail=$((avail * 1024))
|
||||||
local need=$(( total + total/10 + 100*1024*1024 ))
|
need=$(( total + total/10 + 100*1024*1024 ))
|
||||||
if (( avail < need )); then
|
if (( avail < need )); then
|
||||||
log "ERROR" "磁盘空间不足。需要约 $need 字节,可用 $avail 字节。"
|
log "ERROR" "磁盘空间不足。需要约 $need 字节,可用 $avail 字节。"
|
||||||
return 1
|
return 1
|
||||||
@@ -305,30 +236,35 @@ SHA_PATH="${ARCHIVE_PATH}.sha256"
|
|||||||
USE_PIGZ=false
|
USE_PIGZ=false
|
||||||
if command -v pigz >/dev/null 2>&1; then USE_PIGZ=true; fi
|
if command -v pigz >/dev/null 2>&1; then USE_PIGZ=true; fi
|
||||||
|
|
||||||
# 打包与压缩
|
|
||||||
pack() {
|
pack() {
|
||||||
local start; start=$(date +%s)
|
local start end
|
||||||
local tar_args; mapfile -t tar_args < <(build_tar_file_list)
|
local tar_args
|
||||||
|
start=$(date +%s)
|
||||||
|
mapfile -t tar_args < <(build_tar_file_list)
|
||||||
if [[ "${#tar_args[@]}" -eq 0 ]]; then
|
if [[ "${#tar_args[@]}" -eq 0 ]]; then
|
||||||
log "ERROR" "没有可打包的路径(BACKUP_DIR 均不存在)"
|
log "ERROR" "没有可打包的路径(BACKUP_DIRS 均不存在)"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "INFO" "开始打包(支持 exclude.list),pigz=${USE_PIGZ}"
|
log "INFO" "开始打包(支持默认与用户排除规则),pigz=${USE_PIGZ}"
|
||||||
if $USE_PIGZ; then
|
if $USE_PIGZ; then
|
||||||
tar --warning=no-file-changed --exclude-from="$EXCLUDE_FILE" \
|
tar --warning=no-file-changed \
|
||||||
|
--exclude-from="$DEFAULT_EXCLUDE_FILE" \
|
||||||
|
--exclude-from="$USER_EXCLUDE_FILE" \
|
||||||
-c "${tar_args[@]}" | pigz > "$ARCHIVE_PATH"
|
-c "${tar_args[@]}" | pigz > "$ARCHIVE_PATH"
|
||||||
else
|
else
|
||||||
tar --warning=no-file-changed --exclude-from="$EXCLUDE_FILE" \
|
tar --warning=no-file-changed \
|
||||||
|
--exclude-from="$DEFAULT_EXCLUDE_FILE" \
|
||||||
|
--exclude-from="$USER_EXCLUDE_FILE" \
|
||||||
-czf "$ARCHIVE_PATH" "${tar_args[@]}"
|
-czf "$ARCHIVE_PATH" "${tar_args[@]}"
|
||||||
fi
|
fi
|
||||||
local end; end=$(date +%s)
|
end=$(date +%s)
|
||||||
PACK_SECONDS=$((end - start))
|
PACK_SECONDS=$((end - start))
|
||||||
PACK_SIZE=$(stat -c '%s' "$ARCHIVE_PATH")
|
PACK_SIZE="$(stat -c '%s' "$ARCHIVE_PATH")"
|
||||||
log "INFO" "打包完成:$(numfmt --to=iec "$PACK_SIZE") 用时 ${PACK_SECONDS}s"
|
BACKUP_SIZE_HUMAN="$(numfmt --to=iec "$PACK_SIZE")"
|
||||||
|
log "INFO" "打包完成:${BACKUP_SIZE_HUMAN} 用时 ${PACK_SECONDS}s"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 可选:GPG 加密
|
|
||||||
maybe_encrypt() {
|
maybe_encrypt() {
|
||||||
if [[ -n "$GPG_RECIPIENT" ]]; then
|
if [[ -n "$GPG_RECIPIENT" ]]; then
|
||||||
log "INFO" "使用 GPG 收件人加密:$GPG_RECIPIENT"
|
log "INFO" "使用 GPG 收件人加密:$GPG_RECIPIENT"
|
||||||
@@ -349,15 +285,16 @@ maybe_encrypt() {
|
|||||||
ARCHIVE_PATH="${ARCHIVE_PATH}.gpg"
|
ARCHIVE_PATH="${ARCHIVE_PATH}.gpg"
|
||||||
ARCHIVE_BASE="${ARCHIVE_BASE}.gpg"
|
ARCHIVE_BASE="${ARCHIVE_BASE}.gpg"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
PACK_SIZE="$(stat -c '%s' "$ARCHIVE_PATH")"
|
||||||
|
BACKUP_SIZE_HUMAN="$(numfmt --to=iec "$PACK_SIZE")"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 生成 sha256
|
|
||||||
gen_sha() {
|
gen_sha() {
|
||||||
sha256sum "$ARCHIVE_PATH" > "$SHA_PATH"
|
sha256sum "$ARCHIVE_PATH" > "$SHA_PATH"
|
||||||
log "INFO" "已生成校验文件:${SHA_PATH##*/}"
|
log "INFO" "已生成校验文件:${SHA_PATH##*/}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 构建 obsutil 参数
|
|
||||||
build_obs_flags() {
|
build_obs_flags() {
|
||||||
local flags=()
|
local flags=()
|
||||||
flags+=("-f")
|
flags+=("-f")
|
||||||
@@ -383,14 +320,15 @@ build_obs_flags() {
|
|||||||
printf '%s\n' "${flags[@]}"
|
printf '%s\n' "${flags[@]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 上传(含重试)
|
|
||||||
upload() {
|
upload() {
|
||||||
local start; start=$(date +%s)
|
local start end dst_dir
|
||||||
local dst_dir="obs://${OBS_BUCKET}/${OBS_BACKUP_DIR}/"
|
local flags
|
||||||
local flags; mapfile -t flags < <(build_obs_flags)
|
start=$(date +%s)
|
||||||
|
dst_dir="obs://${OBS_BUCKET}/${OBS_BACKUP_DIR}/"
|
||||||
|
mapfile -t flags < <(build_obs_flags)
|
||||||
retry 3 "上传归档 ${ARCHIVE_BASE}" obsutil cp "$ARCHIVE_PATH" "${dst_dir}${ARCHIVE_BASE}" "${flags[@]}"
|
retry 3 "上传归档 ${ARCHIVE_BASE}" obsutil cp "$ARCHIVE_PATH" "${dst_dir}${ARCHIVE_BASE}" "${flags[@]}"
|
||||||
retry 3 "上传校验 ${ARCHIVE_BASE}.sha256" obsutil cp "$SHA_PATH" "${dst_dir}${ARCHIVE_BASE}.sha256" "${flags[@]}"
|
retry 3 "上传校验 ${ARCHIVE_BASE}.sha256" obsutil cp "$SHA_PATH" "${dst_dir}${ARCHIVE_BASE}.sha256" "${flags[@]}"
|
||||||
local end; end=$(date +%s)
|
end=$(date +%s)
|
||||||
UP_SECONDS=$((end - start))
|
UP_SECONDS=$((end - start))
|
||||||
if (( UP_SECONDS > 0 )); then
|
if (( UP_SECONDS > 0 )); then
|
||||||
AVG_SPEED="$(( PACK_SIZE / UP_SECONDS ))"
|
AVG_SPEED="$(( PACK_SIZE / UP_SECONDS ))"
|
||||||
@@ -399,11 +337,11 @@ upload() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# 远端完整性校验(下载 .sha256 比对)
|
|
||||||
verify_remote() {
|
verify_remote() {
|
||||||
local tmp_remote="${SHA_PATH}.remote"
|
local tmp_remote dst
|
||||||
|
tmp_remote="${SHA_PATH}.remote"
|
||||||
rm -f "$tmp_remote"
|
rm -f "$tmp_remote"
|
||||||
local dst="obs://${OBS_BUCKET}/${OBS_BACKUP_DIR}/${ARCHIVE_BASE}.sha256"
|
dst="obs://${OBS_BUCKET}/${OBS_BACKUP_DIR}/${ARCHIVE_BASE}.sha256"
|
||||||
retry 3 "下载远端校验 ${ARCHIVE_BASE}.sha256" obsutil cp "$dst" "$tmp_remote" -f >/dev/null
|
retry 3 "下载远端校验 ${ARCHIVE_BASE}.sha256" obsutil cp "$dst" "$tmp_remote" -f >/dev/null
|
||||||
if ! cmp -s "$SHA_PATH" "$tmp_remote"; then
|
if ! cmp -s "$SHA_PATH" "$tmp_remote"; then
|
||||||
log "ERROR" "远端 .sha256 与本地不一致"
|
log "ERROR" "远端 .sha256 与本地不一致"
|
||||||
@@ -413,10 +351,6 @@ verify_remote() {
|
|||||||
log "INFO" "远端完整性校验通过(.sha256 一致)"
|
log "INFO" "远端完整性校验通过(.sha256 一致)"
|
||||||
}
|
}
|
||||||
|
|
||||||
#############################
|
|
||||||
# 主流程
|
|
||||||
#############################
|
|
||||||
|
|
||||||
SECONDS=0
|
SECONDS=0
|
||||||
PACK_SECONDS=0
|
PACK_SECONDS=0
|
||||||
UP_SECONDS=0
|
UP_SECONDS=0
|
||||||
@@ -430,37 +364,30 @@ retry 3 "打包与压缩" pack || { log "ERROR" "打包失败"; exit 3; }
|
|||||||
maybe_encrypt
|
maybe_encrypt
|
||||||
gen_sha
|
gen_sha
|
||||||
|
|
||||||
# 计算人类可读大小
|
|
||||||
BACKUP_SIZE_HUMAN="$(numfmt --to=iec "$PACK_SIZE")"
|
|
||||||
|
|
||||||
if $DRY_RUN; then
|
if $DRY_RUN; then
|
||||||
log "INFO" "Dry-run 模式:不执行上传。归档:${ARCHIVE_BASE} 大小:${BACKUP_SIZE_HUMAN}"
|
log "INFO" "Dry-run 模式:不执行上传。归档:${ARCHIVE_BASE} 大小:${BACKUP_SIZE_HUMAN}"
|
||||||
log "INFO" "本次总用时:${SECONDS}s"
|
log "INFO" "本次总用时:${SECONDS}s"
|
||||||
send_telegram "dryrun"
|
tg_backup_dryrun "$LABEL" "$(date +'%Y-%m-%d %H:%M:%S')" "$(printf '%s\n' "${BACKUP_PATHS[@]}")" "$(tg_read_user_excludes "$USER_EXCLUDE_FILE")" "$ARCHIVE_BASE" "$BACKUP_SIZE_HUMAN" "${PACK_SECONDS}s"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 上传
|
|
||||||
retry 3 "上传到 OBS" upload || { log "ERROR" "上传失败"; exit 4; }
|
retry 3 "上传到 OBS" upload || { log "ERROR" "上传失败"; exit 4; }
|
||||||
|
|
||||||
# 完整性校验
|
|
||||||
if ! verify_remote; then
|
if ! verify_remote; then
|
||||||
|
local_tail_log="$(tail -n 80 "${LOG_FILE:-./backup.log}" 2>/dev/null || true)"
|
||||||
log "ERROR" "上传后完整性校验失败"
|
log "ERROR" "上传后完整性校验失败"
|
||||||
local tail_log; tail_log="$(tail -n 80 "${LOG_FILE:-./backup.log}" 2>/dev/null || true)"
|
tg_backup_error "$LABEL" "$(date +'%Y-%m-%d %H:%M:%S')" "上传后完整性校验失败:${ARCHIVE_BASE}" "$local_tail_log"
|
||||||
send_telegram "error" "上传后完整性校验失败:${ARCHIVE_BASE}" "$tail_log"
|
|
||||||
exit 5
|
exit 5
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 清理本地
|
|
||||||
if ! $KEEP_LOCAL; then
|
if ! $KEEP_LOCAL; then
|
||||||
rm -f "$ARCHIVE_PATH" "$SHA_PATH" || true
|
rm -f "$ARCHIVE_PATH" "$SHA_PATH" || true
|
||||||
log "INFO" "已删除本地临时归档与校验文件"
|
log "INFO" "已删除本地临时归档与校验文件"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 汇总
|
|
||||||
TOTAL_SECS=$SECONDS
|
TOTAL_SECS=$SECONDS
|
||||||
SPEED_HUMAN="$(numfmt --to=iec "$AVG_SPEED")/s"
|
SPEED_HUMAN="$(numfmt --to=iec "$AVG_SPEED")/s"
|
||||||
log "INFO" "完成:归档大小=${BACKUP_SIZE_HUMAN} 打包=${PACK_SECONDS}s 上传=${UP_SECONDS}s 平均速率=${SPEED_HUMAN} 总用时=${TOTAL_SECS}s"
|
log "INFO" "完成:归档大小=${BACKUP_SIZE_HUMAN} 打包=${PACK_SECONDS}s 上传=${UP_SECONDS}s 平均速率=${SPEED_HUMAN} 总用时=${TOTAL_SECS}s"
|
||||||
send_telegram "success"
|
tg_backup_success "$LABEL" "$(date +'%Y-%m-%d %H:%M:%S')" "$(printf '%s\n' "${BACKUP_PATHS[@]}")" "$(tg_read_user_excludes "$USER_EXCLUDE_FILE")" "$BACKUP_SIZE_HUMAN" "$ARCHIVE_BASE" "obs://${OBS_BUCKET}/${OBS_BACKUP_DIR}/${ARCHIVE_BASE}" "${PACK_SECONDS}s" "${UP_SECONDS}s" "$SPEED_HUMAN"
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
Regular → Executable
+8
-63
@@ -2,7 +2,7 @@
|
|||||||
# 清理 OBS 历史备份:按文件名前缀与日期,仅保留最近 N 天
|
# 清理 OBS 历史备份:按文件名前缀与日期,仅保留最近 N 天
|
||||||
# - 从同目录 env.conf 加载配置;支持 --retain-days / --prefix 覆盖
|
# - 从同目录 env.conf 加载配置;支持 --retain-days / --prefix 覆盖
|
||||||
# - 删除匹配 backup_YYYYmmdd_HHMMSS.tar.gz[.gpg] 以及对应 .sha256
|
# - 删除匹配 backup_YYYYmmdd_HHMMSS.tar.gz[.gpg] 以及对应 .sha256
|
||||||
# - 记录日志并发送 Telegram 摘要(Markdown)
|
# - 记录日志并发送 Telegram 摘要
|
||||||
# 退出码:0 成功;2 配置缺失/依赖缺失;6 清理失败
|
# 退出码:0 成功;2 配置缺失/依赖缺失;6 清理失败
|
||||||
|
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
@@ -21,64 +21,15 @@ log() {
|
|||||||
echo "$line" | tee -a "${LOG_FILE:-./backup.log}"
|
echo "$line" | tee -a "${LOG_FILE:-./backup.log}"
|
||||||
}
|
}
|
||||||
|
|
||||||
#############################
|
# shellcheck disable=SC1091
|
||||||
# Telegram 通知(Markdown)
|
source "${SCRIPT_DIR}/lib/telegram.sh"
|
||||||
#############################
|
|
||||||
send_telegram() {
|
|
||||||
local status="${1-}" # success | error
|
|
||||||
local reason="${2-}" # 错误原因(可选)
|
|
||||||
local error_log="${3-}" # 详细错误日志(可选)
|
|
||||||
|
|
||||||
if [[ -z "${TG_BOT_TOKEN:-}" || -z "${TG_USER_ID:-}" ]]; then
|
|
||||||
log "INFO" "未配置Telegram通知,跳过发送"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
local api="https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage"
|
|
||||||
local current_date; current_date="$(date +'%Y-%m-%d %H:%M:%S')"
|
|
||||||
local message
|
|
||||||
|
|
||||||
if [[ "$status" == "success" ]]; then
|
|
||||||
message="*清理完成通知*\n\n"
|
|
||||||
message+="主机:${LABEL}\n"
|
|
||||||
message+="时间:${current_date}\n"
|
|
||||||
message+="前缀:obs://${OBS_BUCKET}/${TARGET_PREFIX}\n"
|
|
||||||
message+="保留天数:${RETAIN_DAYS}\n"
|
|
||||||
message+="保留对象数:${#TO_KEEP[@]}\n"
|
|
||||||
message+="删除成功:${DEL_OK}\n"
|
|
||||||
message+="删除失败:${DEL_FAIL}"
|
|
||||||
else
|
|
||||||
message="*清理失败通知*\n\n${LABEL} 清理任务失败!"
|
|
||||||
if [[ -n "$reason" ]]; then
|
|
||||||
message+="\n原因:${reason}"
|
|
||||||
fi
|
|
||||||
if [[ -n "$error_log" ]]; then
|
|
||||||
message+="\n错误日志:\n\`\`\`${error_log}\`\`\`"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 转换换行符 \n -> %0A
|
|
||||||
local encoded_message=${message//\\n/%0A}
|
|
||||||
|
|
||||||
local response
|
|
||||||
response="$(curl -sS -X POST "$api" \
|
|
||||||
-d "chat_id=${TG_USER_ID}" \
|
|
||||||
-d "parse_mode=Markdown" \
|
|
||||||
-d "disable_web_page_preview=true" \
|
|
||||||
-d "text=${encoded_message}" 2>&1 || true)"
|
|
||||||
|
|
||||||
if echo "$response" | grep -q '"ok":true'; then
|
|
||||||
log "INFO" "Telegram通知发送成功"
|
|
||||||
else
|
|
||||||
log "WARN" "Telegram通知发送失败: $response"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
on_error() {
|
on_error() {
|
||||||
local exit_code=$?
|
local exit_code=$?
|
||||||
|
local tail_log
|
||||||
log "ERROR" "清理异常退出(exit=${exit_code})"
|
log "ERROR" "清理异常退出(exit=${exit_code})"
|
||||||
local tail_log; tail_log="$(tail -n 80 "${LOG_FILE:-./backup.log}" 2>/dev/null || true)"
|
tail_log="$(tail -n 80 "${LOG_FILE:-./backup.log}" 2>/dev/null || true)"
|
||||||
send_telegram "error" "脚本异常退出(exit=${exit_code})" "$tail_log"
|
tg_clean_error "$LABEL" "$(date +'%Y-%m-%d %H:%M:%S')" "脚本异常退出(exit=${exit_code})" "$tail_log"
|
||||||
exit "$exit_code"
|
exit "$exit_code"
|
||||||
}
|
}
|
||||||
trap on_error ERR
|
trap on_error ERR
|
||||||
@@ -102,7 +53,6 @@ source "$CONF_FILE"
|
|||||||
|
|
||||||
need_cmd obsutil
|
need_cmd obsutil
|
||||||
|
|
||||||
# 严格按要求:配置文件位于 ~/.obsutilconfig
|
|
||||||
OBSUTIL_CONFIG_PATH="${HOME}/.obsutilconfig"
|
OBSUTIL_CONFIG_PATH="${HOME}/.obsutilconfig"
|
||||||
if [[ ! -f "$OBSUTIL_CONFIG_PATH" ]]; then
|
if [[ ! -f "$OBSUTIL_CONFIG_PATH" ]]; then
|
||||||
log "ERROR" "未检测到 obsutil 配置文件:${OBSUTIL_CONFIG_PATH}"
|
log "ERROR" "未检测到 obsutil 配置文件:${OBSUTIL_CONFIG_PATH}"
|
||||||
@@ -110,7 +60,6 @@ if [[ ! -f "$OBSUTIL_CONFIG_PATH" ]]; then
|
|||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 覆盖参数
|
|
||||||
CLI_RETAIN=""
|
CLI_RETAIN=""
|
||||||
CLI_PREFIX=""
|
CLI_PREFIX=""
|
||||||
usage() {
|
usage() {
|
||||||
@@ -134,15 +83,13 @@ if [[ -n "$CLI_PREFIX" ]]; then TARGET_PREFIX="${TARGET_PREFIX}${CLI_PREFIX%/}/"
|
|||||||
|
|
||||||
log "INFO" "开始清理:bucket=${OBS_BUCKET} prefix=${TARGET_PREFIX} 保留最近 ${RETAIN_DAYS} 天"
|
log "INFO" "开始清理:bucket=${OBS_BUCKET} prefix=${TARGET_PREFIX} 保留最近 ${RETAIN_DAYS} 天"
|
||||||
|
|
||||||
# 计算截止日期(N 天前零点,格式 YYYYmmdd)
|
|
||||||
CUTOFF_DATE="$(date -d "-${RETAIN_DAYS} days" +%Y%m%d)"
|
CUTOFF_DATE="$(date -d "-${RETAIN_DAYS} days" +%Y%m%d)"
|
||||||
|
|
||||||
# 列举对象
|
|
||||||
LIST_OUT="$(mktemp)"
|
LIST_OUT="$(mktemp)"
|
||||||
if ! obsutil ls "obs://${OBS_BUCKET}/${TARGET_PREFIX}" >"$LIST_OUT" 2>/dev/null; then
|
if ! obsutil ls "obs://${OBS_BUCKET}/${TARGET_PREFIX}" >"$LIST_OUT" 2>/dev/null; then
|
||||||
log "ERROR" "无法列举 obs://${OBS_BUCKET}/${TARGET_PREFIX}"
|
log "ERROR" "无法列举 obs://${OBS_BUCKET}/${TARGET_PREFIX}"
|
||||||
rm -f "$LIST_OUT"
|
rm -f "$LIST_OUT"
|
||||||
send_telegram "error" "无法列举 obs://${OBS_BUCKET}/${TARGET_PREFIX}" ""
|
tg_clean_error "$LABEL" "$(date +'%Y-%m-%d %H:%M:%S')" "无法列举 obs://${OBS_BUCKET}/${TARGET_PREFIX}" ""
|
||||||
exit 6
|
exit 6
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -167,7 +114,6 @@ for obj in "${OBJECTS[@]}"; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# 去重
|
|
||||||
if [[ "${#TO_DELETE[@]}" -gt 0 ]]; then
|
if [[ "${#TO_DELETE[@]}" -gt 0 ]]; then
|
||||||
mapfile -t TO_DELETE < <(printf '%s\n' "${TO_DELETE[@]}" | awk '!a[$0]++')
|
mapfile -t TO_DELETE < <(printf '%s\n' "${TO_DELETE[@]}" | awk '!a[$0]++')
|
||||||
fi
|
fi
|
||||||
@@ -190,8 +136,7 @@ else
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 通知与退出码
|
tg_clean_success "$LABEL" "$(date +'%Y-%m-%d %H:%M:%S')" "obs://${OBS_BUCKET}/${TARGET_PREFIX}" "${RETAIN_DAYS}" "${#TO_KEEP[@]}" "${DEL_OK}" "${DEL_FAIL}"
|
||||||
send_telegram "success"
|
|
||||||
if (( DEL_FAIL > 0 )); then
|
if (( DEL_FAIL > 0 )); then
|
||||||
exit 6
|
exit 6
|
||||||
fi
|
fi
|
||||||
|
|||||||
+5
-2
@@ -7,8 +7,11 @@ OBS_BACKUP_DIR="backup/${LABEL}" # 备份存放目录
|
|||||||
TG_BOT_TOKEN="" # 留空表示不启用
|
TG_BOT_TOKEN="" # 留空表示不启用
|
||||||
TG_USER_ID="" # 留空表示不启用
|
TG_USER_ID="" # 留空表示不启用
|
||||||
|
|
||||||
# 备份目录,支持多个目录,用空格分隔
|
# 备份目录,使用多行格式;每行一个目录
|
||||||
BACKUP_DIR="/root/data"
|
BACKUP_DIRS=$(cat <<'EOF'
|
||||||
|
/root/data
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
# 日志目录
|
# 日志目录
|
||||||
LOG_FILE="./backup.log"
|
LOG_FILE="./backup.log"
|
||||||
|
|||||||
+4
-14
@@ -2,22 +2,12 @@
|
|||||||
# exclude.list
|
# exclude.list
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 说明:
|
# 说明:
|
||||||
# 1. 本文件由 tar --exclude-from 读取,匹配的是“归档内相对路径”,不是绝对路径。
|
# 1. 本文件只放“默认排除规则”,一般不用修改。
|
||||||
# 2. 排除目录时,建议同时写:
|
# 2. 如果要添加业务自定义排除规则,请编辑 exclude.user.list。
|
||||||
|
# 3. 规则由 tar --exclude-from 读取,匹配的是归档内相对路径,不是绝对路径。
|
||||||
|
# 4. 排除目录时,建议同时写:
|
||||||
# */dir
|
# */dir
|
||||||
# */dir/*
|
# */dir/*
|
||||||
# 3. 如果要排除单个业务文件或业务目录,请优先追加到下面“用户自定义规则”区域。
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# 用户自定义规则
|
|
||||||
# 在下面追加你自己的业务排除项
|
|
||||||
#
|
|
||||||
# 示例:排除单个文件
|
|
||||||
# */project/src/main.js
|
|
||||||
#
|
|
||||||
# 示例:排除一个目录
|
|
||||||
# */project/cache
|
|
||||||
# */project/cache/*
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# ============================================================
|
||||||
|
# exclude.user.list
|
||||||
|
# ============================================================
|
||||||
|
# 说明:
|
||||||
|
# 1. 这个文件只放“用户自定义排除规则”。
|
||||||
|
# 2. backup.sh 会自动同时加载 exclude.list 和 exclude.user.list。
|
||||||
|
# 3. 本文件建议复制为 exclude.user.list 后自行修改。
|
||||||
|
# 4. exclude.user.list 已加入 .gitignore,不会提交到 Git。
|
||||||
|
#
|
||||||
|
# 使用方法:
|
||||||
|
# cp exclude.user.list.example exclude.user.list
|
||||||
|
#
|
||||||
|
# 规则示例:
|
||||||
|
# - 排除单个文件
|
||||||
|
# */project/src/main.js
|
||||||
|
#
|
||||||
|
# - 排除一个目录
|
||||||
|
# */project/cache
|
||||||
|
# */project/cache/*
|
||||||
|
#
|
||||||
|
# - 排除某个固定路径
|
||||||
|
# /root/data/dify
|
||||||
|
#
|
||||||
|
# - 排除任意位置的隐藏目录
|
||||||
|
# */.halo
|
||||||
|
# */.halo/*
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
/root/data/dify
|
||||||
|
*/.halo
|
||||||
|
*/.halo/*
|
||||||
Executable
+138
@@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
tg_code_block() {
|
||||||
|
local text="${1-}"
|
||||||
|
text="$(printf '%s' "$text" | sed -e 's/\\/\\\\/g' -e 's/`/\\`/g')"
|
||||||
|
printf '```text\n%s\n```' "$text"
|
||||||
|
}
|
||||||
|
|
||||||
|
tg_send_message() {
|
||||||
|
local message="${1-}"
|
||||||
|
local response api
|
||||||
|
|
||||||
|
if [[ -z "${TG_BOT_TOKEN:-}" || -z "${TG_USER_ID:-}" ]]; then
|
||||||
|
log "INFO" "未配置Telegram通知,跳过发送"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
api="https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage"
|
||||||
|
response="$(curl -sS -X POST "$api" \
|
||||||
|
--data-urlencode "chat_id=${TG_USER_ID}" \
|
||||||
|
--data-urlencode "parse_mode=MarkdownV2" \
|
||||||
|
--data-urlencode "disable_web_page_preview=true" \
|
||||||
|
--data-urlencode "text=${message}" 2>&1 || true)"
|
||||||
|
|
||||||
|
if echo "$response" | grep -q '"ok":true'; then
|
||||||
|
log "INFO" "Telegram通知发送成功"
|
||||||
|
else
|
||||||
|
log "WARN" "Telegram通知发送失败: $response"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
tg_read_user_excludes() {
|
||||||
|
local file="${1-}"
|
||||||
|
local out="" rule
|
||||||
|
|
||||||
|
[[ -f "$file" ]] || {
|
||||||
|
printf '无'
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
while IFS= read -r rule; do
|
||||||
|
[[ -n "$rule" ]] || continue
|
||||||
|
[[ "$rule" =~ ^[[:space:]]*# ]] && continue
|
||||||
|
out+="${rule}"$'\n'
|
||||||
|
done < "$file"
|
||||||
|
|
||||||
|
if [[ -z "$out" ]]; then
|
||||||
|
printf '无'
|
||||||
|
else
|
||||||
|
printf '%s' "${out%$'\n'}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
tg_backup_success() {
|
||||||
|
local label="$1" current_date="$2" backup_dirs="$3" user_excludes="$4" backup_size="$5" archive_name="$6" location="$7" pack_time="$8" upload_time="$9" speed="${10}"
|
||||||
|
local archive_info message
|
||||||
|
archive_info=$(
|
||||||
|
cat <<EOF
|
||||||
|
文件大小: ${backup_size}
|
||||||
|
归档名称: ${archive_name}
|
||||||
|
存储位置: ${location}
|
||||||
|
打包用时: ${pack_time}
|
||||||
|
上传用时: ${upload_time}
|
||||||
|
平均速率: ${speed}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
message="*备份成功*"$'\n\n'
|
||||||
|
message+="*主机*"$'\n'"$(tg_code_block "$label")"$'\n'
|
||||||
|
message+="*时间*"$'\n'"$(tg_code_block "$current_date")"$'\n'
|
||||||
|
message+="*备份内容*"$'\n'"$(tg_code_block "$backup_dirs")"$'\n'
|
||||||
|
message+="*用户排除*"$'\n'"$(tg_code_block "$user_excludes")"$'\n'
|
||||||
|
message+="*归档信息*"$'\n'"$(tg_code_block "$archive_info")"
|
||||||
|
tg_send_message "$message"
|
||||||
|
}
|
||||||
|
|
||||||
|
tg_backup_dryrun() {
|
||||||
|
local label="$1" current_date="$2" backup_dirs="$3" user_excludes="$4" archive_name="$5" backup_size="$6" pack_time="$7"
|
||||||
|
local archive_info message
|
||||||
|
archive_info=$(
|
||||||
|
cat <<EOF
|
||||||
|
归档名称: ${archive_name}
|
||||||
|
文件大小: ${backup_size}
|
||||||
|
打包用时: ${pack_time}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
message="*备份自检*"$'\n\n'
|
||||||
|
message+="*主机*"$'\n'"$(tg_code_block "$label")"$'\n'
|
||||||
|
message+="*时间*"$'\n'"$(tg_code_block "$current_date")"$'\n'
|
||||||
|
message+="*备份内容*"$'\n'"$(tg_code_block "$backup_dirs")"$'\n'
|
||||||
|
message+="*用户排除*"$'\n'"$(tg_code_block "$user_excludes")"$'\n'
|
||||||
|
message+="*归档信息*"$'\n'"$(tg_code_block "$archive_info")"
|
||||||
|
tg_send_message "$message"
|
||||||
|
}
|
||||||
|
|
||||||
|
tg_backup_error() {
|
||||||
|
local label="$1" current_date="$2" reason="$3" error_log="$4"
|
||||||
|
local message
|
||||||
|
message="*备份失败*"$'\n\n'
|
||||||
|
message+="*主机*"$'\n'"$(tg_code_block "$label")"$'\n'
|
||||||
|
message+="*时间*"$'\n'"$(tg_code_block "$current_date")"$'\n'
|
||||||
|
message+="*原因*"$'\n'"$(tg_code_block "$reason")"
|
||||||
|
if [[ -n "$error_log" ]]; then
|
||||||
|
message+=$'\n'"*错误日志*"$'\n'"$(tg_code_block "$error_log")"
|
||||||
|
fi
|
||||||
|
tg_send_message "$message"
|
||||||
|
}
|
||||||
|
|
||||||
|
tg_clean_success() {
|
||||||
|
local label="$1" current_date="$2" prefix="$3" retain_days="$4" keep_count="$5" del_ok="$6" del_fail="$7"
|
||||||
|
local summary message
|
||||||
|
summary=$(
|
||||||
|
cat <<EOF
|
||||||
|
前缀: ${prefix}
|
||||||
|
保留天数: ${retain_days}
|
||||||
|
保留对象数: ${keep_count}
|
||||||
|
删除成功: ${del_ok}
|
||||||
|
删除失败: ${del_fail}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
message="*清理完成*"$'\n\n'
|
||||||
|
message+="*主机*"$'\n'"$(tg_code_block "$label")"$'\n'
|
||||||
|
message+="*时间*"$'\n'"$(tg_code_block "$current_date")"$'\n'
|
||||||
|
message+="*结果*"$'\n'"$(tg_code_block "$summary")"
|
||||||
|
tg_send_message "$message"
|
||||||
|
}
|
||||||
|
|
||||||
|
tg_clean_error() {
|
||||||
|
local label="$1" current_date="$2" reason="$3" error_log="$4"
|
||||||
|
local message
|
||||||
|
message="*清理失败*"$'\n\n'
|
||||||
|
message+="*主机*"$'\n'"$(tg_code_block "$label")"$'\n'
|
||||||
|
message+="*时间*"$'\n'"$(tg_code_block "$current_date")"$'\n'
|
||||||
|
message+="*原因*"$'\n'"$(tg_code_block "$reason")"
|
||||||
|
if [[ -n "$error_log" ]]; then
|
||||||
|
message+=$'\n'"*错误日志*"$'\n'"$(tg_code_block "$error_log")"
|
||||||
|
fi
|
||||||
|
tg_send_message "$message"
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
- `backup.sh`:打包目录、生成 `sha256`、上传到 OBS、下载远端校验文件做一致性校验
|
- `backup.sh`:打包目录、生成 `sha256`、上传到 OBS、下载远端校验文件做一致性校验
|
||||||
- `clean_backups.sh`:按文件名日期清理旧备份
|
- `clean_backups.sh`:按文件名日期清理旧备份
|
||||||
|
- `lib/telegram.sh`:统一处理 Telegram 通知模板和发送逻辑
|
||||||
|
|
||||||
## 配置
|
## 配置
|
||||||
|
|
||||||
@@ -11,15 +12,29 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp env.conf.example env.conf
|
cp env.conf.example env.conf
|
||||||
|
cp exclude.user.list.example exclude.user.list
|
||||||
```
|
```
|
||||||
|
|
||||||
重点配置项:
|
重点配置项:
|
||||||
|
|
||||||
- `OBS_BUCKET`:OBS 桶名
|
- `OBS_BUCKET`:OBS 桶名
|
||||||
- `LABEL`:机器或业务标识
|
- `LABEL`:机器或业务标识
|
||||||
- `BACKUP_DIR`:要备份的目录,当前格式为以空格分隔的路径列表
|
- `BACKUP_DIRS`:要备份的目录,使用多行格式,每行一个目录
|
||||||
- `TG_BOT_TOKEN` / `TG_USER_ID`:Telegram 通知配置,可留空
|
- `TG_BOT_TOKEN` / `TG_USER_ID`:Telegram 通知配置,可留空
|
||||||
|
|
||||||
|
`BACKUP_DIRS` 示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BACKUP_DIRS=$(cat <<'EOF'
|
||||||
|
/root/data
|
||||||
|
/root/.openclaw
|
||||||
|
/root/myapp
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
不再建议使用空格分隔的目录列表,因为路径中一旦包含空格,Shell 会把它拆坏。
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
备份:
|
备份:
|
||||||
@@ -46,18 +61,15 @@ cp env.conf.example env.conf
|
|||||||
./clean_backups.sh --retain-days=5
|
./clean_backups.sh --retain-days=5
|
||||||
```
|
```
|
||||||
|
|
||||||
## exclude.list 规则
|
## 排除规则
|
||||||
|
|
||||||
`exclude.list` 由 `tar --exclude-from` 读取,匹配的是归档内的相对路径,不是磁盘上的绝对路径。
|
脚本会同时读取两个排除文件:
|
||||||
|
|
||||||
不要写:
|
- `exclude.list`:默认排除规则,一般不用修改,可以提交 Git
|
||||||
|
- `exclude.user.list`:用户自定义排除规则,不提交 Git
|
||||||
|
- `exclude.user.list.example`:用户规则示例文件,可以提交 Git
|
||||||
|
|
||||||
```text
|
排除规则由 `tar --exclude-from` 读取,匹配的是归档内的相对路径,不是磁盘上的绝对路径。
|
||||||
/root/data/project/src/main.js
|
|
||||||
cache/
|
|
||||||
```
|
|
||||||
|
|
||||||
应该写相对路径或通配规则。
|
|
||||||
|
|
||||||
排除单个文件:
|
排除单个文件:
|
||||||
|
|
||||||
@@ -71,25 +83,17 @@ project/src/main.js
|
|||||||
*/project/src/main.js
|
*/project/src/main.js
|
||||||
```
|
```
|
||||||
|
|
||||||
排除某类文件:
|
|
||||||
|
|
||||||
```text
|
|
||||||
*.log
|
|
||||||
*.tmp
|
|
||||||
```
|
|
||||||
|
|
||||||
排除一个目录,建议写两行:
|
排除一个目录,建议写两行:
|
||||||
|
|
||||||
```text
|
|
||||||
*/node_modules
|
|
||||||
*/node_modules/*
|
|
||||||
```
|
|
||||||
|
|
||||||
例如排除 `project/cache`:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
*/project/cache
|
*/project/cache
|
||||||
*/project/cache/*
|
*/project/cache/*
|
||||||
```
|
```
|
||||||
|
|
||||||
当前自带的 `exclude.list` 已对常见目录采用这种写法,避免 `.cache/`、`node_modules/` 这类规则匹配不到的问题。
|
默认规则放在 `exclude.list`,业务自定义规则建议只写到 `exclude.user.list`。
|
||||||
|
|
||||||
|
更完整的默认规则、用户规则示例和配置模板,请直接查看:
|
||||||
|
|
||||||
|
- `exclude.list`
|
||||||
|
- `exclude.user.list.example`
|
||||||
|
- `env.conf.example`
|
||||||
|
|||||||
Reference in New Issue
Block a user