From 95b44f1e5510b1e4efec49e3a3f90d51a1980e1a Mon Sep 17 00:00:00 2001 From: Warren Date: Mon, 30 Mar 2026 04:11:02 +0800 Subject: [PATCH] fix: backup monitoring and PATH environment issues - Fix backup_monitor.sh find command to sort by modification time - Fix grep -oP syntax error (change to grep -oE) - Adjust tier rotation threshold from -mtime +7 to +6 - Add backup_all.sh script with PATH fixes for crontab - Add mysql-client/bin to PATH for mysqldump command - Fix backup status check for v2 naming patterns --- monitor/storage/backup_monitor.sh | 6 +- scripts/backup_all.sh | 821 ++++++++++++++++++++++++++++++ 2 files changed, 824 insertions(+), 3 deletions(-) create mode 100755 scripts/backup_all.sh diff --git a/monitor/storage/backup_monitor.sh b/monitor/storage/backup_monitor.sh index 511a5e9..e54037e 100755 --- a/monitor/storage/backup_monitor.sh +++ b/monitor/storage/backup_monitor.sh @@ -92,7 +92,7 @@ check_backup_status() { if [ -d "$service_backup_dir" ]; then file_count=$(find "$service_backup_dir" -type f 2>/dev/null | wc -l) size=$(du -sb "$service_backup_dir" 2>/dev/null | cut -f1) - latest_file=$(find "$service_backup_dir" -type f \( -name "*.tar.gz" -o -name "*.sql.gz" -o -name "*.rdb" \) 2>/dev/null | head -1) + latest_file=$(find "$service_backup_dir" -type f \( -name "*.tar.gz" -o -name "*.sql.gz" -o -name "*.rdb" \) -printf "%T@ %p\n" 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-) # 處理 size 為空或 0 的情況 if [ -z "$size" ] || [ "$size" = "0" ]; then @@ -271,12 +271,12 @@ tier_backups() { # 7天前: daily -> weekly # 命名格式: {service}_{type}_{YYYYMMDD}_{HHMMSS}.{ext} - find "$BACKUP_BASE/daily" -type f -mtime +7 | while read -r file; do + find "$BACKUP_BASE/daily" -type f -mtime +6 | while read -r file; do service=$(basename "$(dirname "$file")") # 解析時間戳 filename=$(basename "$file") - timestamp=$(echo "$filename" | grep -oP '\d{8}_\d{6}' || echo "") + timestamp=$(echo "$filename" | grep -oE '[0-9]{8}_[0-9]{6}' || echo "") if [ -n "$timestamp" ]; then year=${timestamp:0:4} diff --git a/scripts/backup_all.sh b/scripts/backup_all.sh new file mode 100755 index 0000000..6caf274 --- /dev/null +++ b/scripts/backup_all.sh @@ -0,0 +1,821 @@ +#!/bin/bash +export PATH="/usr/local/bin:/opt/homebrew/bin:/opt/homebrew/opt/postgresql@18/bin:/usr/bin:/bin:/sbin:/opt/homebrew/opt/mysql-client/bin:$PATH" + +#=============================================================================== +# Momentry 統一備份腳本 +# 路徑: /Users/accusys/momentry/scripts/backup_all.sh +# +# 命名規範 (v2): +# {service}_{type}_v2_{YYYYMMDD}_{HHMMSS}.{ext} +# +# 版本說明: +# v1: 初始備份架構(不包含新架構組件) +# v2: 新架構備份(包含 monitor_jobs, processor_results, Output 目錄) +# +# 使用方式: +# ./backup_all.sh [service|all] [type] [timestamp] +# +# 參數: +# service - 特定服務 (postgresql, redis, mariadb, wordpress, n8n, qdrant, gitea, ollama, caddy, sftpgo, mongodb, php, momentry_output) +# all - 備份所有服務 (默認) +# type - 備份類型 (full, db, cfg, data) +# timestamp - 指定時間戳 (格式: YYYYMMDD_HHMMSS) +# +# 示例: +# ./backup_all.sh # 備份所有服務 (v2) +# ./backup_all.sh postgresql # 只備份 PostgreSQL +# ./backup_all.sh all full # 完整備份所有服務 (v2) +# ./backup_all.sh mariadb db # 只備份 MariaDB 數據庫 +# ./backup_all.sh restore 20260316_101215 # 恢復到指定斷點 +# +# ⚠️ v2 版本差異: +# - 新增 monitor_jobs, processor_results 表 +# - 新增 Output 目錄備份 +# - MongoDB 路徑修正 +# +# 排程範例 (crontab): +# # 每天凌晨 3 點執行所有備份 +# 0 3 * * * /Users/accusys/momentry/scripts/backup_all.sh >> /Users/accusys/momentry/log/backup.log 2>&1 +# +# # 每週日凌晨 3 點執行完整備份 +# 0 3 * * 0 /Users/accusys/momentry/scripts/backup_all.sh all full >> /Users/accusys/momentry/log/backup.log 2>&1 +#=============================================================================== + +set -e + +# 載入密碼配置 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/load_credentials.sh" ]; then + source "$SCRIPT_DIR/load_credentials.sh" +fi + +# 確保路徑正確(Crontab 環境可能缺少 PATH) +export PATH="/usr/local/bin:/opt/homebrew/bin:/opt/homebrew/opt/postgresql@18/bin:/sbin:/usr/sbin:/usr/bin:/bin:/opt/homebrew/opt/mysql-client/bin" + +# 顏色定義 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# 路徑配置 +BACKUP_ROOT="/Users/accusys/momentry/backup/daily" +LOG_DIR="/Users/accusys/momentry/log" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# 備份版本 (v2 = 新架構) +BACKUP_VERSION="v2" + +# 時間戳 (v2 格式: v2_YYYYMMDD_HHMMSS) +if [ -n "$3" ]; then + TIMESTAMP="$3" +else + TIMESTAMP="${BACKUP_VERSION}_$(date +%Y%m%d_%H%M%S)" +fi + +# 服務列表 (v2 新增 momentry_output) +SERVICES=("postgresql" "redis" "mariadb" "wordpress" "n8n" "qdrant" "gitea" "ollama" "caddy" "sftpgo" "mongodb" "php" "momentry_output") + +#=============================================================================== +# 日誌函數 +#=============================================================================== +log() { + echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_DIR/backup.log" +} + +log_success() { + echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] ✅ $1${NC}" | tee -a "$LOG_DIR/backup.log" +} + +log_error() { + echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] ❌ $1${NC}" | tee -a "$LOG_DIR/backup.log" +} + +log_warn() { + echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️ $1${NC}" | tee -a "$LOG_DIR/backup.log" +} + +#=============================================================================== +# 通用函數 +#=============================================================================== +ensure_backup_dir() { + local service=$1 + mkdir -p "$BACKUP_ROOT/$service" +} + +backup_file() { + local service=$1 + local type=$2 + local file=$3 + + ensure_backup_dir "$service" + + if [ -f "$file" ]; then + local filename=$(basename "$file") + local dest="$BACKUP_ROOT/$service/${service}_${type}_${TIMESTAMP}_${filename}" + cp "$file" "$dest" + + # 壓縮 + if [[ "$filename" == *.sql ]]; then + gzip "$dest" + dest="${dest}.gz" + fi + + # SHA256 + sha256sum "$dest" >"${dest}.sha256" + + log_success "$service $type: $(basename "$dest")" + return 0 + fi + return 1 +} + +backup_directory() { + local service=$1 + local type=$2 + local dir=$3 + + ensure_backup_dir "$service" + + if [ -d "$dir" ]; then + local dest="$BACKUP_ROOT/$service/${service}_${type}_${TIMESTAMP}.tar.gz" + tar -czf "$dest" -C "$(dirname "$dir")" "$(basename "$dir")" 2>/dev/null || true + + # SHA256 + sha256sum "$dest" >"${dest}.sha256" + + log_success "$service $type: $(basename "$dest")" + return 0 + fi + return 1 +} + +#=============================================================================== +# 服務備份函數 +#=============================================================================== + +# PostgreSQL +backup_postgresql() { + local type=${1:-db} + log "開始 PostgreSQL 備份..." + + # momentry 數據庫 + PGPASSWORD="$PG_PASSWORD" pg_dump -U "$PG_USER" -d momentry | gzip >"$BACKUP_ROOT/postgresql/postgresql_db_momentry_${TIMESTAMP}.sql.gz" + sha256sum "$BACKUP_ROOT/postgresql/postgresql_db_momentry_${TIMESTAMP}.sql.gz" >"$BACKUP_ROOT/postgresql/postgresql_db_${TIMESTAMP}.sha256" + + # video_register 數據庫 + PGPASSWORD="$PG_PASSWORD" pg_dump -U "$PG_USER" -d video_register | gzip >"$BACKUP_ROOT/postgresql/postgresql_db_video_register_${TIMESTAMP}.sql.gz" + sha256sum "$BACKUP_ROOT/postgresql/postgresql_db_video_register_${TIMESTAMP}.sql.gz" >>"$BACKUP_ROOT/postgresql/postgresql_db_${TIMESTAMP}.sha256" + + log_success "PostgreSQL: 數據庫備份完成" +} + +# Redis +backup_redis() { + local type=${1:-rdb} + log "開始 Redis 備份..." + + redis-cli -a "$REDIS_PASSWORD" SAVE >/dev/null 2>&1 + cp /opt/homebrew/var/db/redis/dump.rdb "$BACKUP_ROOT/redis/redis_rdb_${TIMESTAMP}.rdb" + sha256sum "$BACKUP_ROOT/redis/redis_rdb_${TIMESTAMP}.rdb" >"$BACKUP_ROOT/redis/redis_rdb_${TIMESTAMP}.sha256" + + log_success "Redis: RDB 備份完成" +} + +# MariaDB (包含 WordPress) +backup_mariadb() { + local type=${1:-db} + log "開始 MariaDB 備份..." + + # 所有數據庫 + mysqldump -u "$MARIADB_USER" -p"$MARIADB_PASSWORD" --all-databases | gzip > \ + "$BACKUP_ROOT/mariadb/mariadb_db_all_${TIMESTAMP}.sql.gz" + sha256sum "$BACKUP_ROOT/mariadb/mariadb_db_all_${TIMESTAMP}.sql.gz" >"$BACKUP_ROOT/mariadb/mariadb_db_${TIMESTAMP}.sha256" + + # WordPress 數據庫 + mysqldump -u "$MARIADB_USER" -p"$MARIADB_PASSWORD" wordpress | gzip > \ + "$BACKUP_ROOT/mariadb/mariadb_db_wordpress_${TIMESTAMP}.sql.gz" + sha256sum "$BACKUP_ROOT/mariadb/mariadb_db_wordpress_${TIMESTAMP}.sql.gz" >>"$BACKUP_ROOT/mariadb/mariadb_db_${TIMESTAMP}.sha256" + + log_success "MariaDB: 數據庫備份完成 (包含 WordPress)" +} + +# WordPress 文件 +backup_wordpress_files() { + local wordpress_dir="/Users/accusys/wordpress/web" + local backup_dir="$BACKUP_ROOT/wordpress" + + log "開始 WordPress 文件備份..." + + # 確保備份目錄存在 + mkdir -p "$backup_dir" + + # 排除不必要的目錄 + if [ -d "$wordpress_dir" ]; then + tar --exclude='wp-content/cache/*' \ + --exclude='wp-content/uploads/cache/*' \ + --exclude='.git/*' \ + -czf "$backup_dir/wordpress_files_${TIMESTAMP}.tar.gz" \ + -C /Users/accusys/wordpress web/ + + sha256sum "$backup_dir/wordpress_files_${TIMESTAMP}.tar.gz" >>"$backup_dir/wordpress_${TIMESTAMP}.sha256" 2>/dev/null || + sha256sum "$backup_dir/wordpress_files_${TIMESTAMP}.tar.gz" >"$backup_dir/wordpress_${TIMESTAMP}.sha256" + + log_success "WordPress: 文件備份完成" + else + log_error "WordPress 目錄不存在: $wordpress_dir" + fi +} + +# n8n +backup_n8n() { + local type=${1:-full} + log "開始 n8n 備份..." + + # 數據庫 + PGPASSWORD="$PG_PASSWORD" pg_dump -U "$PG_USER" -d n8n | gzip >"$BACKUP_ROOT/n8n/n8n_db_${TIMESTAMP}.sql.gz" + + # 數據目錄 + if [ -d "/Users/accusys/momentry/var/n8n" ]; then + tar -czf "$BACKUP_ROOT/n8n/n8n_data_${TIMESTAMP}.tar.gz" -C /Users/accusys/momentry/var n8n/ + fi + + # SHA256 + sha256sum "$BACKUP_ROOT/n8n"/n8n_* >"$BACKUP_ROOT/n8n/n8n_${TIMESTAMP}.sha256" + + log_success "n8n: 完整備份完成" +} + +# Qdrant +backup_qdrant() { + local type=${1:-full} + log "開始 Qdrant 備份..." + + # 嘗試使用 Snapshots API + COLLECTIONS=$(curl -s -H "api-key: $QDRANT_API_KEY" \ + http://localhost:6333/collections | jq -r '.result[].name' 2>/dev/null || echo "") + + if [ -n "$COLLECTIONS" ] && [ "$COLLECTIONS" != "null" ]; then + for COLLECTION in $COLLECTIONS; do + curl -X POST -H "api-key: $QDRANT_API_KEY" \ + "http://localhost:6333/collections/${COLLECTION}/snapshots" \ + -o "$BACKUP_ROOT/qdrant/qdrant_snapshot_${COLLECTION}_${TIMESTAMP}.tar.gz" 2>/dev/null || true + done + else + # 數據目錄備份 + tar -czf "$BACKUP_ROOT/qdrant/qdrant_data_${TIMESTAMP}.tar.gz" \ + -C /Users/accusys/momentry/var qdrant/ 2>/dev/null || true + fi + + # SHA256 + sha256sum "$BACKUP_ROOT/qdrant"/qdrant_* >"$BACKUP_ROOT/qdrant/qdrant_${TIMESTAMP}.sha256" + + log_success "Qdrant: 備份完成" +} + +# Gitea +backup_gitea() { + local type=${1:-full} + log "開始 Gitea 備份..." + + # 數據目錄 + if [ -d "/Users/accusys/momentry/var/gitea" ]; then + tar -czf "$BACKUP_ROOT/gitea/gitea_data_${TIMESTAMP}.tar.gz" \ + -C /Users/accusys/momentry/var gitea/ + fi + + # 配置目錄 + if [ -d "/Users/accusys/momentry/etc/gitea" ]; then + tar -czf "$BACKUP_ROOT/gitea/gitea_cfg_${TIMESTAMP}.tar.gz" \ + -C /Users/accusys/momentry/etc gitea/ + fi + + # SHA256 + sha256sum "$BACKUP_ROOT/gitea"/gitea_* >"$BACKUP_ROOT/gitea/gitea_${TIMESTAMP}.sha256" + + log_success "Gitea: 完整備份完成" +} + +# Ollama +backup_ollama() { + local type=${1:-cfg} + log "開始 Ollama 備份..." + + # 配置目錄 + if [ -d "/Users/accusys/momentry/etc/ollama" ]; then + tar -czf "$BACKUP_ROOT/ollama/ollama_cfg_${TIMESTAMP}.tar.gz" \ + -C /Users/accusys/momentry/etc ollama/ + fi + + # 環境變數 + if [ -f "/Users/accusys/momentry/var/ollama/environment.txt" ]; then + cp /Users/accusys/momentry/var/ollama/environment.txt "$BACKUP_ROOT/ollama/ollama_env_${TIMESTAMP}.txt" + fi + + # SHA256 + sha256sum "$BACKUP_ROOT/ollama"/ollama_* >"$BACKUP_ROOT/ollama/ollama_${TIMESTAMP}.sha256" + + log_success "Ollama: 配置備份完成" +} + +# Caddy +backup_caddy() { + local type=${1:-cfg} + log "開始 Caddy 備份..." + + # 配置 + if [ -f "/Users/accusys/momentry/etc/Caddyfile" ]; then + tar -czf "$BACKUP_ROOT/caddy/caddy_cfg_${TIMESTAMP}.tar.gz" \ + -C /Users/accusys/momentry/etc Caddyfile + fi + + # SHA256 + sha256sum "$BACKUP_ROOT/caddy"/caddy_* >"$BACKUP_ROOT/caddy/caddy_${TIMESTAMP}.sha256" + + log_success "Caddy: 配置備份完成" +} + +# SftpGo +backup_sftpgo() { + local type=${1:-cfg} + log "開始 SftpGo 備份..." + + # 配置 + if [ -d "/Users/accusys/momentry/etc/sftpgo" ]; then + tar -czf "$BACKUP_ROOT/sftpgo/sftpgo_cfg_${TIMESTAMP}.tar.gz" \ + -C /Users/accusys/momentry/etc sftpgo/ + fi + + # PostgreSQL 數據庫 (SFTPGo 已遷移到 PostgreSQL) + PGPASSWORD="$SFTPGO_PASSWORD" pg_dump -U "$SFTPGO_USER" -h localhost -d sftpgo | gzip >"$BACKUP_ROOT/sftpgo/sftpgo_db_${TIMESTAMP}.sql.gz" + + # SHA256 + sha256sum "$BACKUP_ROOT/sftpgo"/sftpgo_* >"$BACKUP_ROOT/sftpgo/sftpgo_${TIMESTAMP}.sha256" + + log_success "SftpGo: 配置和數據庫備份完成" +} + +# MongoDB +backup_mongodb() { + local type=${1:-full} + log "開始 MongoDB 備份..." + + # 使用 mongodump 備份 (避免文件鎖問題) + local MONGO_BACKUP_DIR="/tmp/mongodb_backup_${TIMESTAMP}" + mkdir -p "$MONGO_BACKUP_DIR" + + # mongodump 需要認證 + if [ -n "$MONGODB_PASSWORD" ]; then + mongodump --uri="mongodb://localhost:27017" \ + --username="$MONGODB_USER" \ + --password="$MONGODB_PASSWORD" \ + --authenticationDatabase=admin \ + --out="$MONGO_BACKUP_DIR" 2>/dev/null || true + else + mongodump --uri="mongodb://localhost:27017" \ + --out="$MONGO_BACKUP_DIR" 2>/dev/null || true + fi + + # 打包 + if [ -d "$MONGO_BACKUP_DIR" ] && [ "$(ls -A $MONGO_BACKUP_DIR 2>/dev/null)" ]; then + tar -czf "$BACKUP_ROOT/mongodb/mongodb_data_${TIMESTAMP}.tar.gz" \ + -C "$MONGO_BACKUP_DIR" . + rm -rf "$MONGO_BACKUP_DIR" + log "MongoDB: mongodump 備份完成" + else + log_warn "MongoDB: mongodump 備份失敗或數據庫為空" + rm -rf "$MONGO_BACKUP_DIR" + fi + + # SHA256 + sha256sum "$BACKUP_ROOT/mongodb"/mongodb_* >"$BACKUP_ROOT/mongodb/mongodb_${TIMESTAMP}.sha256" + + log_success "MongoDB: 備份完成" +} + +# PHP +backup_php() { + local type=${1:-cfg} + log "開始 PHP 備份..." + + # 配置 + if [ -d "/Users/accusys/momentry/etc/php/8.5" ]; then + tar -czf "$BACKUP_ROOT/php/php_cfg_${TIMESTAMP}.tar.gz" \ + -C /Users/accusys/momentry/etc php/8.5 + fi + + # SHA256 + sha256sum "$BACKUP_ROOT/php"/php_* >"$BACKUP_ROOT/php/php_${TIMESTAMP}.sha256" + + log_success "PHP: 配置備份完成" +} + +# Momentry Output 目錄 (v2 新增) +backup_momentry_output() { + local type=${1:-data} + log "開始 Momentry Output 備份..." + + # Output 目錄 + local OUTPUT_DIR="/Users/accusys/momentry/output" + + if [ -d "$OUTPUT_DIR" ]; then + tar -czf "$BACKUP_ROOT/momentry/momentry_output_${TIMESTAMP}.tar.gz" \ + -C /Users/accusys/momentry output/ + log "Momentry Output: 備份 $OUTPUT_DIR" + else + log_warn "Momentry Output: 目錄不存在或為空 ($OUTPUT_DIR)" + fi + + # SHA256 + sha256sum "$BACKUP_ROOT/momentry"/momentry_output_* >"$BACKUP_ROOT/momentry/momentry_output_${TIMESTAMP}.sha256" 2>/dev/null || true + + log_success "Momentry Output: 備份完成" +} + +#=============================================================================== +# 恢復函數 +#=============================================================================== + +restore_postgresql() { + local timestamp=$1 + log "恢復 PostgreSQL..." + + # 找到對應的備份文件 + local backup_file=$(ls "$BACKUP_ROOT/postgresql"/postgresql_db_momentry_${timestamp}.sql.gz 2>/dev/null | head -1) + + if [ -n "$backup_file" ]; then + gunzip -c "$backup_file" | PGPASSWORD="$PG_PASSWORD" psql -U "$PG_USER" -d momentry + log_success "PostgreSQL 恢復完成" + else + log_error "找不到 PostgreSQL 備份文件: $timestamp" + fi +} + +restore_redis() { + local timestamp=$1 + log "恢復 Redis..." + + local backup_file=$(ls "$BACKUP_ROOT/redis"/redis_rdb_${timestamp}.rdb 2>/dev/null | head -1) + + if [ -n "$backup_file" ]; then + redis-cli -a "$REDIS_PASSWORD" SHUTDOWN 2>/dev/null || true + cp "$backup_file" /opt/homebrew/var/db/redis/dump.rdb + launchctl load /Library/LaunchDaemons/com.momentry.redis.plist 2>/dev/null || + redis-server --daemonize yes --requirepass "$REDIS_PASSWORD" + log_success "Redis 恢復完成" + else + log_error "找不到 Redis 備份文件: $timestamp" + fi +} + +restore_mariadb() { + local timestamp=$1 + log "恢復 MariaDB (包含 WordPress)..." + + local backup_file=$(ls "$BACKUP_ROOT/mariadb"/mariadb_db_wordpress_${timestamp}.sql.gz 2>/dev/null | head -1) + + if [ -n "$backup_file" ]; then + gunzip -c "$backup_file" | mysql -u momentry_backup -pmomentry_backup_pwd_2026 wordpress + log_success "MariaDB/WordPress 恢復完成" + else + log_error "找不到 MariaDB 備份文件: $timestamp" + fi +} + +restore_n8n() { + local timestamp=$1 + log "恢復 n8n..." + + # 恢復數據庫 + local db_backup=$(ls "$BACKUP_ROOT/n8n"/n8n_db_${timestamp}.sql.gz 2>/dev/null | head -1) + if [ -n "$db_backup" ]; then + gunzip -c "$db_backup" | PGPASSWORD="$PG_PASSWORD" psql -U "$PG_USER" -d n8n + fi + + # 恢復數據目錄 + local data_backup=$(ls "$BACKUP_ROOT/n8n"/n8n_data_${timestamp}.tar.gz 2>/dev/null | head -1) + if [ -n "$data_backup" ]; then + rm -rf /Users/accusys/momentry/var/n8n + tar -xzf "$data_backup" -C /Users/accusys/momentry/var/ + fi + + log_success "n8n 恢復完成" +} + +restore_qdrant() { + local timestamp=$1 + log "恢復 Qdrant..." + + pkill qdrant 2>/dev/null || true + sleep 2 + + local data_backup=$(ls "$BACKUP_ROOT/qdrant"/qdrant_data_${timestamp}.tar.gz 2>/dev/null | head -1) + if [ -n "$data_backup" ]; then + rm -rf /Users/accusys/momentry/var/qdrant + tar -xzf "$data_backup" -C /Users/accusys/momentry/var/ + fi + + launchctl load /Library/LaunchDaemons/com.momentry.qdrant.plist 2>/dev/null || true + log_success "Qdrant 恢復完成" +} + +restore_gitea() { + local timestamp=$1 + log "恢復 Gitea..." + + # 停止 Gitea + pkill gitea 2>/dev/null || true + + # 恢復數據 + local data_backup=$(ls "$BACKUP_ROOT/gitea"/gitea_data_${timestamp}.tar.gz 2>/dev/null | head -1) + if [ -n "$data_backup" ]; then + rm -rf /Users/accusys/momentry/var/gitea + tar -xzf "$data_backup" -C /Users/accusys/momentry/var/ + fi + + # 恢復配置 + local cfg_backup=$(ls "$BACKUP_ROOT/gitea"/gitea_cfg_${timestamp}.tar.gz 2>/dev/null | head -1) + if [ -n "$cfg_backup" ]; then + rm -rf /Users/accusys/momentry/etc/gitea + tar -xzf "$cfg_backup" -C /Users/accusys/momentry/etc/ + fi + + log_success "Gitea 恢復完成" +} + +restore_ollama() { + local timestamp=$1 + log "恢復 Ollama..." + + # 恢復配置 + local cfg_backup=$(ls "$BACKUP_ROOT/ollama"/ollama_cfg_${timestamp}.tar.gz 2>/dev/null | head -1) + if [ -n "$cfg_backup" ]; then + rm -rf /Users/accusys/momentry/etc/ollama + tar -xzf "$cfg_backup" -C /Users/accusys/momentry/etc/ + fi + + log_success "Ollama 恢復完成" +} + +restore_caddy() { + local timestamp=$1 + log "恢復 Caddy..." + + local cfg_backup=$(ls "$BACKUP_ROOT/caddy"/caddy_cfg_${timestamp}.tar.gz 2>/dev/null | head -1) + if [ -n "$cfg_backup" ]; then + tar -xzf "$cfg_backup" -C /Users/accusys/momentry/etc/ + caddy reload --config /Users/accusys/momentry/etc/Caddyfile + fi + + log_success "Caddy 恢復完成" +} + +restore_sftpgo() { + local timestamp=$1 + log "恢復 SftpGo..." + + # 停止 SFTPGo + pkill -f sftpgo || true + sleep 2 + + # 恢復配置 + local cfg_backup=$(ls "$BACKUP_ROOT/sftpgo"/sftpgo_cfg_${timestamp}.tar.gz 2>/dev/null | head -1) + if [ -n "$cfg_backup" ]; then + rm -rf /Users/accusys/momentry/etc/sftpgo + tar -xzf "$cfg_backup" -C /Users/accusys/momentry/etc/ + fi + + # 恢復 PostgreSQL 數據庫 + local db_backup=$(ls "$BACKUP_ROOT/sftpgo"/sftpgo_db_${timestamp}.sql.gz 2>/dev/null | head -1) + if [ -n "$db_backup" ]; then + # 確保數據庫存在 + PGPASSWORD="$PG_PASSWORD" psql -U "$PG_USER" -h localhost -d postgres -c "DROP DATABASE IF EXISTS sftpgo;" 2>/dev/null + PGPASSWORD="$PG_PASSWORD" psql -U "$PG_USER" -h localhost -d postgres -c "CREATE DATABASE sftpgo OWNER $SFTPGO_USER;" 2>/dev/null + gunzip -c "$db_backup" | PGPASSWORD="$SFTPGO_PASSWORD" psql -U "$SFTPGO_USER" -h localhost -d sftpgo 2>/dev/null + fi + + # 重啟 SFTPGo + cd /Users/accusys/momentry/var/sftpgo + /opt/homebrew/opt/sftpgo/bin/sftpgo serve --config-file /Users/accusys/momentry/etc/sftpgo/sftpgo.json & + + log_success "SftpGo 恢復完成" +} + +restore_mongodb() { + local timestamp=$1 + log "恢復 MongoDB..." + + # 解壓縮到臨時目錄 + local MONGO_RESTORE_DIR="/tmp/mongodb_restore_${timestamp}" + mkdir -p "$MONGO_RESTORE_DIR" + + local data_backup=$(ls "$BACKUP_ROOT/mongodb"/mongodb_data_${timestamp}.tar.gz 2>/dev/null | head -1) + if [ -n "$data_backup" ]; then + tar -xzf "$data_backup" -C "$MONGO_RESTORE_DIR/" + + # 使用 mongorestore 恢復 + if [ -n "$MONGODB_PASSWORD" ]; then + mongorestore --uri="mongodb://localhost:27017" \ + --username="$MONGODB_USER" \ + --password="$MONGODB_PASSWORD" \ + --authenticationDatabase=admin \ + --drop \ + --dir="$MONGO_RESTORE_DIR" 2>/dev/null || true + else + mongorestore --uri="mongodb://localhost:27017" \ + --drop \ + --dir="$MONGO_RESTORE_DIR" 2>/dev/null || true + fi + + rm -rf "$MONGO_RESTORE_DIR" + else + log_warn "MongoDB: 未找到備份文件" + fi + + log_success "MongoDB 恢復完成" +} + +restore_php() { + local timestamp=$1 + log "恢復 PHP..." + + local cfg_backup=$(ls "$BACKUP_ROOT/php"/php_cfg_${timestamp}.tar.gz 2>/dev/null | head -1) + if [ -n "$cfg_backup" ]; then + rm -rf /Users/accusys/momentry/etc/php/8.5 + tar -xzf "$cfg_backup" -C /Users/accusys/momentry/etc/php/ + fi + + log_success "PHP 恢復完成" +} + +restore_momentry_output() { + local timestamp=$1 + log "恢復 Momentry Output..." + + # v2: Output 目錄可能有多個版本,嘗試 v2 版本再回退到舊版本 + local output_backup="" + + # 嘗試 v2 版本 + output_backup=$(ls "$BACKUP_ROOT/momentry"/momentry_output_v2_${timestamp}.tar.gz 2>/dev/null | head -1) + + # 如果沒有 v2 版本,嘗試舊格式 + if [ -z "$output_backup" ]; then + output_backup=$(ls "$BACKUP_ROOT/momentry"/momentry_output_${timestamp}.tar.gz 2>/dev/null | head -1) + fi + + if [ -n "$output_backup" ]; then + rm -rf /Users/accusys/momentry/output + mkdir -p /Users/accusys/momentry + tar -xzf "$output_backup" -C /Users/accusys/momentry/ + log "Momentry Output: 恢復 $(basename $output_backup)" + else + log_warn "Momentry Output: 未找到備份檔案" + fi + + log_success "Momentry Output 恢復完成" +} + +#=============================================================================== +# 主程序 +#=============================================================================== + +main() { + local command=${1:-all} + local service=${2:-} + local type=${3:-} + + # 確保日誌目錄存在 + mkdir -p "$LOG_DIR" + + echo "" + log "==========================================" + log "Momentry 備份系統" + log "時間戳: $TIMESTAMP" + log "==========================================" + + case $command in + restore | rollback) + if [ -z "$service" ]; then + log_error "請指定恢復時間戳 (YYYYMMDD_HHMMSS 或 v2_YYYYMMDD_HHMMSS)" + echo "示例: $0 restore v2_20260325_030000" + exit 1 + fi + + log "開始恢復到斷點: $service" + + for svc in "${SERVICES[@]}"; do + case $svc in + postgresql) restore_postgresql "$service" ;; + redis) restore_redis "$service" ;; + mariadb) restore_mariadb "$service" ;; + n8n) restore_n8n "$service" ;; + qdrant) restore_qdrant "$service" ;; + gitea) restore_gitea "$service" ;; + ollama) restore_ollama "$service" ;; + caddy) restore_caddy "$service" ;; + sftpgo) restore_sftpgo "$service" ;; + mongodb) restore_mongodb "$service" ;; + php) restore_php "$service" ;; + momentry_output) restore_momentry_output "$service" ;; + esac + done + + log "==========================================" + log_success "恢復完成!" + log "==========================================" + ;; + + list) + log "可用時間點:" + for dir in "$BACKUP_ROOT"/*/; do + local svc=$(basename "$dir") + echo " $svc:" + ls -1 "$dir"*.tar.gz "$dir"*.sql.gz "$dir"*.rdb 2>/dev/null | + sed 's/.*\([0-9]\{8\}\_[0-9]\{6\}\).*/\1/' | sort -u | sed 's/^/ /' + done + ;; + + status) + log "備份狀態:" + echo "" + for svc in "${SERVICES[@]}"; do + local date_part="${TIMESTAMP#*_}" # Remove v2_ prefix + date_part="${date_part:0:8}" # Extract YYYYMMDD + local latest=$(find "$BACKUP_ROOT/$svc" \( -name "*_${date_part}_*" -o -name "*_v2_${date_part}_*" \) -type f 2>/dev/null | head -1) + if [ -n "$latest" ]; then + local size=$(du -h "$latest" | cut -f1) + echo -e " $svc: ${GREEN}✓${NC} $size" + else + echo -e " $svc: ${RED}✗${NC}" + fi + done + ;; + + all) + # 備份所有服務 + for svc in "${SERVICES[@]}"; do + case $svc in + postgresql) backup_postgresql "$type" ;; + redis) backup_redis "$type" ;; + mariadb) backup_mariadb "$type" ;; + wordpress) backup_wordpress_files ;; + n8n) backup_n8n "$type" ;; + qdrant) backup_qdrant "$type" ;; + gitea) backup_gitea "$type" ;; + ollama) backup_ollama "$type" ;; + caddy) backup_caddy "$type" ;; + sftpgo) backup_sftpgo "$type" ;; + mongodb) backup_mongodb "$type" ;; + php) backup_php "$type" ;; + momentry_output) backup_momentry_output "$type" ;; + esac + done + + log "==========================================" + log_success "所有備份完成! 時間戳: $TIMESTAMP" + log "==========================================" + ;; + + *) + # 備份特定服務 + if [ -n "$service" ]; then + case $service in + postgresql) backup_postgresql "$type" ;; + redis) backup_redis "$type" ;; + mariadb) backup_mariadb "$type" ;; + wordpress) backup_wordpress_files ;; + n8n) backup_n8n "$type" ;; + qdrant) backup_qdrant "$type" ;; + gitea) backup_gitea "$type" ;; + ollama) backup_ollama "$type" ;; + caddy) backup_caddy "$type" ;; + sftpgo) backup_sftpgo "$type" ;; + mongodb) backup_mongodb "$type" ;; + php) backup_php "$type" ;; + momentry_output) backup_momentry_output "$type" ;; + *) + log_error "未知服務: $service" + echo "可用服務: ${SERVICES[*]}" + exit 1 + ;; + esac + else + log_error "請指定命令或服務" + echo "用法: $0 [命令] [服務] [類型]" + echo "" + echo "命令:" + echo " all - 備份所有服務 (默認)" + echo " - 備份特定服務" + echo " restore - 恢復到指定斷點" + echo " list - 列出可用時間點" + echo " status - 顯示備份狀態" + echo "" + echo "服務: ${SERVICES[*]}" + exit 1 + fi + ;; + esac +} + +main "$@"