Files
backup_OBS/clean_backups.sh
T
2026-03-16 15:53:38 +08:00

199 lines
5.6 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# 清理 OBS 历史备份:按文件名前缀与日期,仅保留最近 N 天
# - 从同目录 env.conf 加载配置;支持 --retain-days / --prefix 覆盖
# - 删除匹配 backup_YYYYmmdd_HHMMSS.tar.gz[.gpg] 以及对应 .sha256
# - 记录日志并发送 Telegram 摘要(Markdown
# 退出码:0 成功;2 配置缺失/依赖缺失;6 清理失败
set -Eeuo pipefail
SCRIPT_PATH="${BASH_SOURCE[0]:-$0}"
SCRIPT_DIR="$(cd "$(dirname -- "$SCRIPT_PATH")" && pwd -P)"
export PATH="$SCRIPT_DIR:$PATH"
cd "$SCRIPT_DIR"
log_ts() { date '+%Y-%m-%d %H:%M:%S'; }
LOG_PREFIX="[CLEAN]"
log() {
local level="$1"; shift
local line
line="$(log_ts) ${LOG_PREFIX}[$level] $*"
echo "$line" | tee -a "${LOG_FILE:-./backup.log}"
}
#############################
# Telegram 通知(Markdown
#############################
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() {
local exit_code=$?
log "ERROR" "清理异常退出(exit=${exit_code})"
local tail_log; tail_log="$(tail -n 80 "${LOG_FILE:-./backup.log}" 2>/dev/null || true)"
send_telegram "error" "脚本异常退出(exit=${exit_code}" "$tail_log"
exit "$exit_code"
}
trap on_error ERR
need_cmd() {
command -v "$1" >/dev/null 2>&1 || {
log "ERROR" "未找到命令:$1"
return 1
}
}
CONF_FILE="${SCRIPT_DIR}/env.conf"
[[ -f "$CONF_FILE" ]] || { echo "缺少配置 ${CONF_FILE}" >&2; exit 2; }
# shellcheck disable=SC1090
source "$CONF_FILE"
: "${OBS_BUCKET:?缺少 OBS_BUCKET}"
: "${OBS_BACKUP_DIR:?缺少 OBS_BACKUP_DIR}"
: "${RETAIN_DAYS:?缺少 RETAIN_DAYS}"
: "${LOG_FILE:?缺少 LOG_FILE}"
need_cmd obsutil
# 严格按要求:配置文件位于 ~/.obsutilconfig
OBSUTIL_CONFIG_PATH="${HOME}/.obsutilconfig"
if [[ ! -f "$OBSUTIL_CONFIG_PATH" ]]; then
log "ERROR" "未检测到 obsutil 配置文件:${OBSUTIL_CONFIG_PATH}"
log "ERROR" "请先执行:./obsutil config -interactive 完成初始化(AK/SK、Endpoint、默认桶)"
exit 2
fi
# 覆盖参数
CLI_RETAIN=""
CLI_PREFIX=""
usage() {
cat <<'USAGE'
用法:
./clean_backups.sh [--retain-days=N] [--prefix=SUBDIR]
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--retain-days=*) CLI_RETAIN="${1#*=}"; shift;;
--prefix=*) CLI_PREFIX="${1#*=}"; shift;;
-h|--help) usage; exit 0;;
*) echo "未知参数:$1"; usage; exit 2;;
esac
done
if [[ -n "$CLI_RETAIN" ]]; then RETAIN_DAYS="$CLI_RETAIN"; fi
TARGET_PREFIX="${OBS_BACKUP_DIR%/}/"
if [[ -n "$CLI_PREFIX" ]]; then TARGET_PREFIX="${TARGET_PREFIX}${CLI_PREFIX%/}/"; fi
log "INFO" "开始清理:bucket=${OBS_BUCKET} prefix=${TARGET_PREFIX} 保留最近 ${RETAIN_DAYS}"
# 计算截止日期(N 天前零点,格式 YYYYmmdd)
CUTOFF_DATE="$(date -d "-${RETAIN_DAYS} days" +%Y%m%d)"
# 列举对象
LIST_OUT="$(mktemp)"
if ! obsutil ls "obs://${OBS_BUCKET}/${TARGET_PREFIX}" >"$LIST_OUT" 2>/dev/null; then
log "ERROR" "无法列举 obs://${OBS_BUCKET}/${TARGET_PREFIX}"
rm -f "$LIST_OUT"
send_telegram "error" "无法列举 obs://${OBS_BUCKET}/${TARGET_PREFIX}" ""
exit 6
fi
mapfile -t OBJECTS < <(awk '{for(i=1;i<=NF;i++){if($i~/^obs:\/\//)print $i}}' "$LIST_OUT")
rm -f "$LIST_OUT"
TO_DELETE=()
TO_KEEP=()
for obj in "${OBJECTS[@]}"; do
fname="${obj##*/}"
if [[ "$fname" =~ ^backup_([0-9]{8})_[0-9]{6}\.tar\.gz(\.gpg)?(\.sha256)?$ ]]; then
day="${BASH_REMATCH[1]}"
if [[ "$day" -lt "$CUTOFF_DATE" ]]; then
TO_DELETE+=("$obj")
if [[ ! "$fname" =~ \.sha256$ ]]; then
TO_DELETE+=("${obj}.sha256")
fi
else
TO_KEEP+=("$obj")
fi
fi
done
# 去重
if [[ "${#TO_DELETE[@]}" -gt 0 ]]; then
mapfile -t TO_DELETE < <(printf '%s\n' "${TO_DELETE[@]}" | awk '!a[$0]++')
fi
DEL_OK=0
DEL_FAIL=0
if [[ "${#TO_DELETE[@]}" -eq 0 ]]; then
log "INFO" "没有过期对象需要清理。"
else
log "INFO" "准备删除 ${#TO_DELETE[@]} 个对象(含 sha256"
for o in "${TO_DELETE[@]}"; do
if obsutil rm "$o" -f >/dev/null 2>&1; then
((DEL_OK++))
log "INFO" "删除:$o"
else
((DEL_FAIL++))
log "WARN" "删除失败:$o"
fi
done
fi
# 通知与退出码
send_telegram "success"
if (( DEL_FAIL > 0 )); then
exit 6
fi
exit 0