#!/bin/bash # 站点恢复脚本 - 支持选择恢复单个站点或全部 # 用法:bash restore-sites.sh [备份时间戳] # 无参数则自动检测最新备份 set -e BACKUP_DIR="/Users/niuniu/programs/docker/db-backups" REMOTE_BASE="/root/docker" log() { echo "[$(date '+%H:%M:%S')] $1"; } warn() { echo "[$(date '+%H:%M:%S')] [警告] $1"; } error() { echo "[$(date '+%H:%M:%S')] [错误] $1"; exit 1; } # ============================================================ # 1. 选择备份时间戳 # ============================================================ if [[ -n "$1" ]]; then TIMESTAMP="$1" else echo "" echo "==========================================" echo " 可用的备份时间戳" echo "==========================================" echo "" # 列出可用的时间戳 timestamps=$(ls ${BACKUP_DIR}/assets-*.tar.gz 2>/dev/null | sed 's/.*assets-\(.*\)\.tar\.gz/\1/' | sort -r | head -10) if [[ -z "$timestamps" ]]; then error "未找到任何备份文件" fi index=1 while IFS= read -r ts; do echo " $index) $ts" ts_list[$index]="$ts" ((index++)) done <<< "$timestamps" echo "" printf "请选择备份时间戳 (1-$((index-1))): " read choice if [[ -z "${ts_list[$choice]}" ]]; then error "无效选择" fi TIMESTAMP="${ts_list[$choice]}" fi # ============================================================ # 2. 检测可用的备份文件 # ============================================================ echo "" log "检测备份文件: ${TIMESTAMP}" LOCAL_BACKUP="${BACKUP_DIR}" CLOUD_BACKUP="txjp:/root/docker/db-backups" # 检查本地备份 check_backup() { local site="$1" local found="" # 检查本地 if [[ -f "${LOCAL_BACKUP}/${site}-${TIMESTAMP}.tar.gz" ]]; then found="local:${LOCAL_BACKUP}/${site}-${TIMESTAMP}.tar.gz" # 检查云端 elif ssh txjp "test -f ${CLOUD_BACKUP}/${site}-${TIMESTAMP}.tar.gz" 2>/dev/null; then found="cloud:${site}-${TIMESTAMP}.tar.gz" fi echo "$found" } # ============================================================ # 3. 选择要恢复的站点 # ============================================================ echo "" echo "==========================================" echo " 可恢复的站点" echo "==========================================" echo "" PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" SITES="shared" for dir in "$PROJECT_ROOT"/*-ai/; do s=$(basename "$dir"); [[ "$s" == "nginx-proxy-ai" ]] && continue [ -f "$dir/docker-compose.yml" ] || continue SITES="$SITES $s" done index=1 declare -A site_status for site in $SITES; do backup=$(check_backup "$site") if [[ -n "$backup" ]]; then if [[ "$backup" == local:* ]]; then status="✅ 本地" else status="✅ 云端" fi echo " $index) ${site} ${status}" site_list[$index]="$site" site_source[$index]="$backup" ((index++)) else echo " $index) ${site} ❌ 未找到" ((index++)) fi done echo "" echo " $index) 全部恢复" echo "" printf "请选择要恢复的内容 (1-$index): " read choice if [[ "$choice" == "$index" ]]; then # 全部恢复 restore_list=("${site_list[@]}") else restore_list=("${site_list[$choice]}") fi # ============================================================ # 4. 下载并解压备份文件 # ============================================================ RESTORE_DIR="/tmp/restore-$$" mkdir -p "$RESTORE_DIR" download_and_extract() { local site="$1" local source="${site_source[$choice]}" local file_path="" if [[ "$source" == local:* ]]; then file_path="${source#local:}" elif [[ "$source" == cloud:* ]]; then local filename="${source#cloud:}" log "从云端下载 ${site} 备份..." scp "${CLOUD_BACKUP}/${filename}" "$RESTORE_DIR/" file_path="$RESTORE_DIR/${filename}" fi # 解压 cd "$RESTORE_DIR" tar xzf "$file_path" } # ============================================================ # 5. 恢复函数 # ============================================================ restore_shared() { log "恢复共享资源..." download_and_extract "shared" # 恢复 docker 网络 if [[ -f "$RESTORE_DIR/backup-shared/docker-network.sh" ]]; then log "创建 Docker 网络..." bash "$RESTORE_DIR/backup-shared/docker-network.sh" fi # 恢复 nginx 配置 if [[ -d "$RESTORE_DIR/backup-shared/nginx" ]]; then log "恢复 nginx 配置..." ssh txjp "mkdir -p $REMOTE_BASE/nginx-proxy-ai/conf.d" scp "$RESTORE_DIR/backup-shared/nginx/"* txjp:$REMOTE_BASE/nginx-proxy-ai/conf.d/ fi # 恢复 SSL 证书 if [[ -d "$RESTORE_DIR/backup-shared/ssl" ]]; then log "恢复 SSL 证书..." ssh txjp "cp -a $RESTORE_DIR/backup-shared/ssl/live /etc/letsencrypt/ 2>/dev/null || true" ssh txjp "cp -a $RESTORE_DIR/backup-shared/ssl/archive /etc/letsencrypt/ 2>/dev/null || true" ssh txjp "cp -a $RESTORE_DIR/backup-shared/ssl/renewal /etc/letsencrypt/ 2>/dev/null || true" fi # 恢复 nginx 容器配置 if [[ -f "$RESTORE_DIR/backup-shared/docker-compose.yml" ]]; then log "恢复 nginx docker-compose..." scp "$RESTORE_DIR/backup-shared/docker-compose.yml" txjp:$REMOTE_BASE/nginx-proxy-ai/ fi } restore_assets() { log "恢复 assets 站点..." download_and_extract "assets" # 创建目录 ssh txjp "mkdir -p $REMOTE_BASE/assets-ai/data" # 恢复数据库 if [[ -f "$RESTORE_DIR/backup-assets/data/assets.db" ]]; then log "恢复 assets 数据库..." scp "$RESTORE_DIR/backup-assets/data/assets.db" txjp:/tmp/assets.db ssh txjp "docker cp /tmp/assets.db assets-ai:/app/data/assets.db && rm /tmp/assets.db" fi # 恢复上传文件 if [[ -d "$RESTORE_DIR/backup-assets/data/uploads" ]]; then log "恢复 assets 上传文件..." scp -r "$RESTORE_DIR/backup-assets/data/uploads/"* txjp:$REMOTE_BASE/assets-ai/data/uploads/ 2>/dev/null || true fi # 恢复配置文件 for f in docker-compose.yml .env Dockerfile; do if [[ -f "$RESTORE_DIR/backup-assets/$f" ]]; then scp "$RESTORE_DIR/backup-assets/$f" txjp:$REMOTE_BASE/assets-ai/ fi done # 重启容器 log "重启 assets 容器..." ssh txjp "cd $REMOTE_BASE/assets-ai && docker compose down 2>/dev/null || true && docker compose up -d" } restore_issue() { log "恢复 issue 站点..." download_and_extract "issue" # 创建目录 ssh txjp "mkdir -p $REMOTE_BASE/issue-ai/data" # 恢复数据库 if [[ -f "$RESTORE_DIR/backup-issue/data/issue.db" ]]; then log "恢复 issue 数据库..." scp "$RESTORE_DIR/backup-issue/data/issue.db" txjp:/tmp/issue.db ssh txjp "docker cp /tmp/issue.db issue-ai:/app/data/issue.db && rm /tmp/issue.db" fi # 恢复上传文件 if [[ -d "$RESTORE_DIR/backup-issue/data/uploads" ]]; then log "恢复 issue 上传文件..." scp -r "$RESTORE_DIR/backup-issue/data/uploads/"* txjp:$REMOTE_BASE/issue-ai/data/uploads/ 2>/dev/null || true fi # 恢复报告文件 if [[ -d "$RESTORE_DIR/backup-issue/data/reports" ]]; then log "恢复 issue 报告文件..." scp -r "$RESTORE_DIR/backup-issue/data/reports/"* txjp:$REMOTE_BASE/issue-ai/data/reports/ 2>/dev/null || true fi # 恢复配置文件 for f in docker-compose.yml .env Dockerfile; do if [[ -f "$RESTORE_DIR/backup-issue/$f" ]]; then scp "$RESTORE_DIR/backup-issue/$f" txjp:$REMOTE_BASE/issue-ai/ fi done # 重启容器 log "重启 issue 容器..." ssh txjp "cd $REMOTE_BASE/issue-ai && docker compose down 2>/dev/null || true && docker compose up -d" } restore_ldap() { log "恢复 ldap 站点..." download_and_extract "ldap" # 创建目录 ssh txjp "mkdir -p $REMOTE_BASE/ldap-ai/data" # 恢复数据库 if [[ -f "$RESTORE_DIR/backup-ldap/data/users.db" ]]; then log "恢复 ldap 数据库..." scp "$RESTORE_DIR/backup-ldap/data/users.db" txjp:/tmp/ldap.db ssh txjp "docker cp /tmp/ldap.db lldap:/data/users.db && rm /tmp/ldap.db" fi # 恢复配置文件 for f in docker-compose.yml .env Dockerfile; do if [[ -f "$RESTORE_DIR/backup-ldap/$f" ]]; then scp "$RESTORE_DIR/backup-ldap/$f" txjp:$REMOTE_BASE/ldap-ai/ fi done # 重启容器 log "重启 ldap 容器..." ssh txjp "cd $REMOTE_BASE/ldap-ai && docker compose down 2>/dev/null || true && docker compose up -d" } restore_gitea() { log "恢复 gitea 站点..." download_and_extract "gitea" # 创建目录 ssh txjp "mkdir -p $REMOTE_BASE/gitea-ai/data" # 恢复数据库 if [[ -f "$RESTORE_DIR/backup-gitea/data/gitea.db" ]]; then log "恢复 gitea 数据库..." scp "$RESTORE_DIR/backup-gitea/data/gitea.db" txjp:/tmp/gitea.db ssh txjp "docker cp /tmp/gitea.db gitea-ai:/data/gitea.db && rm /tmp/gitea.db" fi # 恢复代码仓库 if [[ -d "$RESTORE_DIR/backup-gitea/git" ]]; then log "恢复 gitea 代码仓库..." scp -r "$RESTORE_DIR/backup-gitea/git" txjp:$REMOTE_BASE/gitea-ai/data/ fi # 恢复配置 if [[ -d "$RESTORE_DIR/backup-gitea/gitea-conf" ]]; then log "恢复 gitea 配置..." ssh txjp "mkdir -p $REMOTE_BASE/gitea-ai/data/gitea" scp -r "$RESTORE_DIR/backup-gitea/gitea-conf" txjp:$REMOTE_BASE/gitea-ai/data/gitea/ fi # 恢复 docker-compose if [[ -f "$RESTORE_DIR/backup-gitea/docker-compose.yml" ]]; then scp "$RESTORE_DIR/backup-gitea/docker-compose.yml" txjp:$REMOTE_BASE/gitea-ai/ fi # 重启容器 log "重启 gitea 容器..." ssh txjp "cd $REMOTE_BASE/gitea-ai && docker compose down 2>/dev/null || true && docker compose up -d" } restore_oa() { log "恢复 oa 站点..." download_and_extract "oa" # 恢复配置文件 for f in docker-compose.yml .env; do if [[ -f "$RESTORE_DIR/backup-oa/$f" ]]; then scp "$RESTORE_DIR/backup-oa/$f" txjp:$REMOTE_BASE/oa-ai/ fi done # 重启容器 log "重启 oa 容器..." ssh txjp "cd $REMOTE_BASE/oa-ai && docker compose down 2>/dev/null || true && docker compose up -d" } restore_monitor() { log "恢复 monitor 站点..." download_and_extract "monitor" for f in docker-compose.yml .env; do if [[ -f "$RESTORE_DIR/backup-monitor/$f" ]]; then scp "$RESTORE_DIR/backup-monitor/$f" txjp:$REMOTE_BASE/monitor-ai/ fi done # 恢复数据库 if [[ -f "$RESTORE_DIR/backup-monitor/data/monitor.db" ]]; then ssh txjp "docker cp $RESTORE_DIR/backup-monitor/data/monitor.db monitor-ai:/app/data/monitor.db" fi log "重启 monitor 容器..." ssh txjp "cd $REMOTE_BASE/monitor-ai && docker compose down 2>/dev/null || true && docker compose up -d --build" } # 通用恢复(未知站点,仅恢复配置文件) restore_generic() { local site="$1" log "恢复 ${site} 站点(通用模式)..." download_and_extract "${site#*-}" # monitor-ai → monitor for f in docker-compose.yml .env Dockerfile; do if [[ -f "$RESTORE_DIR/backup-${site#*-}/$f" ]]; then scp "$RESTORE_DIR/backup-${site#*-}/$f" txjp:$REMOTE_BASE/${site#*-}-ai/ fi done # 尝试重启容器 ssh txjp "cd $REMOTE_BASE/${site#*-}-ai && docker compose down 2>/dev/null || true && docker compose up -d" 2>/dev/null || warn "${site} 容器重启失败,请手动检查" } # ============================================================ # 6. 执行恢复 # ============================================================ for site in "${restore_list[@]}"; do case "$site" in shared) restore_shared ;; assets) restore_assets ;; issue) restore_issue ;; ldap) restore_ldap ;; gitea) restore_gitea ;; oa) restore_oa ;; monitor) restore_monitor ;; *) warn "${site}: 无专用恢复逻辑,仅恢复配置"; restore_generic "$site" ;; esac done # ============================================================ # 7. 重载 nginx # ============================================================ log "重载 nginx 配置..." ssh txjp "docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" 2>/dev/null || warn "nginx 重载失败,请手动检查" # ============================================================ # 8. 清理临时文件 # ============================================================ log "清理临时文件..." rm -rf "$RESTORE_DIR" # ============================================================ # 9. 验证恢复结果 # ============================================================ echo "" log "恢复完成!验证容器状态..." ssh txjp "docker ps --format 'table {{.Names}}\t{{.Status}}' | grep -E '(assets|issue|ldap|gitea|oa|nginx)'"