#!/usr/bin/env bash # scripts/ops/pg-backup.sh # Sprint 5.2: PostgreSQL 自動備份腳本 # 部署: cron 0 */6 * * * on 188 (ollama user) # 備份目標: awoooi_prod + momo_analytics # 保留策略: 7 天 # 2026-04-09 Claude Sonnet 4.6 Asia/Taipei set -euo pipefail BACKUP_DIR="${BACKUP_DIR:-/home/ollama/backups}" SECRETS_FILE="${SECRETS_FILE:-/home/ollama/awoooi-ops-secrets/secrets.env}" RETAIN_DAYS="${RETAIN_DAYS:-7}" AWOOOI_API_URL="${AWOOOI_API_URL:-https://awoooi.wooo.work}" # 載入 secrets(含 Telegram token for fallback) [[ -f "$SECRETS_FILE" ]] && source "$SECRETS_FILE" TIMESTAMP=$(date '+%Y%m%d_%H%M%S') LOG_PREFIX="[$(date '+%Y-%m-%d %H:%M:%S %z')]" log() { echo "${LOG_PREFIX} $*"; } notify_telegram() { local msg="$1" local chat_id="${TELEGRAM_ALERT_CHAT_ID:-${SRE_GROUP_CHAT_ID:--1003711974679}}" if [[ -n "${TELEGRAM_BOT_TOKEN:-}" && -n "$chat_id" ]]; then curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -H "Content-Type: application/json" \ -d "{\"chat_id\":\"${chat_id}\",\"text\":\"${msg}\",\"parse_mode\":\"HTML\"}" \ > /dev/null 2>&1 || true fi } backup_db() { local label="$1" # awoooi_prod | momo_analytics local host="$2" # 127.0.0.1 local user="$3" local password="$4" local dbname="$5" local outfile="${BACKUP_DIR}/${label}_${TIMESTAMP}.sql.gz" log "開始備份 ${label} → ${outfile}" if PGPASSWORD="$password" pg_dump \ -h "$host" -U "$user" -d "$dbname" \ --no-owner --no-acl \ 2>/dev/null | gzip > "$outfile"; then local size size=$(du -sh "$outfile" | cut -f1) log "✅ ${label} 備份完成 (${size})" echo "success:${label}:${size}" else log "❌ ${label} 備份失敗" echo "failed:${label}" fi } cleanup_old_backups() { local label="$1" local count count=$(find "$BACKUP_DIR" -name "${label}_*.sql.gz" -mtime "+${RETAIN_DAYS}" | wc -l) if (( count > 0 )); then find "$BACKUP_DIR" -name "${label}_*.sql.gz" -mtime "+${RETAIN_DAYS}" -delete log "🗑️ 清理 ${label} 舊備份 ${count} 個 (>${RETAIN_DAYS}天)" fi } main() { mkdir -p "$BACKUP_DIR" log "=== pg-backup 開始 (retain=${RETAIN_DAYS}d) ===" local results=() # awoooi_prod (host PostgreSQL, TCP) results+=("$(backup_db "awoooi_prod" "127.0.0.1" "awoooi" "awoooi_prod_2026" "awoooi_prod")") # momo_analytics (momo-db 容器,無對外 port,用 docker exec 直接 dump) local outfile_momo="${BACKUP_DIR}/momo_analytics_${TIMESTAMP}.sql.gz" log "開始備份 momo_analytics (docker exec) → ${outfile_momo}" if docker exec momo-db pg_dump -U momo momo_analytics 2>/dev/null | gzip > "$outfile_momo"; then local size_momo size_momo=$(du -sh "$outfile_momo" | cut -f1) log "✅ momo_analytics 備份完成 (${size_momo})" results+=("success:momo_analytics:${size_momo}") else log "❌ momo_analytics 備份失敗" rm -f "$outfile_momo" results+=("failed:momo_analytics") fi # 清理舊備份 cleanup_old_backups "awoooi_prod" cleanup_old_backups "momo_analytics" log "=== pg-backup 完成 ===" # 組裝 Telegram 通知 local success_count=0 fail_count=0 details="" for r in "${results[@]}"; do IFS=':' read -r status label size_or_empty <<< "$r" case "$status" in success) ((success_count++)) || true; details+="✅ ${label} (${size_or_empty})\n" ;; failed) ((fail_count++)) || true; details+="❌ ${label} 失敗\n" ;; skipped) details+="⏭️ ${label} 跳過\n" ;; esac done local icon="✅" [[ $fail_count -gt 0 ]] && icon="⚠️" notify_telegram "${icon} AWOOOI DB 備份 ├ 時間: $(date '+%Y-%m-%d %H:%M') +0800 ├ 成功: ${success_count} | 失敗: ${fail_count} └ ${details}" [[ $fail_count -gt 0 ]] && exit 1 return 0 } main "$@"