mirror of
https://github.com/wolfydw/backup_OBS.git
synced 2026-05-08 06:37:02 +08:00
199 lines
5.6 KiB
Bash
199 lines
5.6 KiB
Bash
#!/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
|