#!/bin/bash # OCNGX 恢复脚本 # 用于在不同的 HuggingFace Space 上恢复 OCNGX 系统 set -euo pipefail # 配置变量 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" # 参数解析 BACKUP_FILE="" FORCE_RESTORE=false SKIP_CONFIG=false usage() { echo "用法: $0 [选项] <备份文件>" echo "" echo "选项:" echo " -f, --force 强制恢复,覆盖现有配置" echo " -s, --skip-config 跳过配置文件恢复" echo " -h, --help 显示帮助信息" echo "" echo "示例:" echo " $0 /path/to/ocngx_backup_20240101_120000.tar.gz" echo " $0 --force ocngx_backup_20240101_120000.tar.gz" exit 1 } # 解析命令行参数 while [[ $# -gt 0 ]]; do case $1 in -f|--force) FORCE_RESTORE=true shift ;; -s|--skip-config) SKIP_CONFIG=true shift ;; -h|--help) usage ;; -*) echo "未知选项: $1" usage ;; *) if [[ -z "$BACKUP_FILE" ]]; then BACKUP_FILE="$1" else echo "错误: 只能指定一个备份文件" usage fi shift ;; esac done # 检查备份文件 if [[ -z "$BACKUP_FILE" ]]; then echo "❌ 错误: 必须指定备份文件" usage fi if [[ ! -f "$BACKUP_FILE" ]]; then echo "❌ 错误: 备份文件不存在: $BACKUP_FILE" exit 1 fi # 验证备份文件完整性 verify_backup() { echo "🔍 验证备份文件..." # 检查文件格式 if [[ ! "$BACKUP_FILE" =~ \.tar\.gz$ ]]; then echo "❌ 错误: 备份文件格式不正确,应为 .tar.gz 文件" exit 1 fi # 验证校验和(如果存在) local checksum_file="${BACKUP_FILE}.sha256" if [[ -f "$checksum_file" ]]; then echo "🔐 验证校验和..." if sha256sum -c "$checksum_file" >/dev/null 2>&1; then echo " ✅ 校验和验证通过" else echo " ❌ 校验和验证失败,备份文件可能已损坏" exit 1 fi else echo " ⚠️ 未找到校验和文件,跳过验证" fi # 检查归档内容 echo "📦 检查归档内容..." if ! tar -tzf "$BACKUP_FILE" >/dev/null 2>&1; then echo "❌ 错误: 备份文件无法正常解压" exit 1 fi echo " ✅ 备份文件验证通过" } # 检查环境 check_environment() { echo "🔍 检查恢复环境..." # 检查是否为 HuggingFace Spaces 环境 if [[ -d "/app" && -f "/app/opencode.json" ]]; then echo " 🤗 HuggingFace Spaces 环境检测到" RESTORE_TARGET="/app" else echo " 💻 本地环境,恢复到项目目录" RESTORE_TARGET="$PROJECT_DIR" fi echo " 📂 恢复目标: $RESTORE_TARGET" # 检查磁盘空间 local backup_size=$(du -h "$BACKUP_FILE" | cut -f1) local available_space=$(df -h "$RESTORE_TARGET" | awk 'NR==2 {print $4}') echo " 💾 备份大小: $backup_size, 可用空间: $available_space" } # 预检查 pre_restore_checks() { echo "⚠️ 执行恢复前检查..." # 检查关键文件是否存在 local critical_files=("$RESTORE_TARGET/opencode.json" "$RESTORE_TARGET/docker-start.sh") for file in "${critical_files[@]}"; do if [[ -f "$file" ]] && [[ "$FORCE_RESTORE" != true ]]; then echo " ⚠️ 发现现有配置: $file" echo " 使用 --force 选项强制覆盖" echo " 或使用 --skip-config 选项跳过配置恢复" exit 1 fi done # 检查 OpenCode 是否运行 if pgrep -f "opencode" >/dev/null 2>&1; then echo " ⚠️ 检测到 OpenCode 正在运行" echo " 建议停止服务后再执行恢复" read -p "是否继续? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then echo "恢复已取消" exit 0 fi fi echo " ✅ 预检查完成" } # 创建临时恢复目录 prepare_restore() { echo "📁 准备恢复环境..." TEMP_RESTORE_DIR="/tmp/ocngx_restore_$(date +%s)" mkdir -p "$TEMP_RESTORE_DIR" echo " 📂 临时目录: $TEMP_RESTORE_DIR" # 解压备份文件 echo "📦 解压备份文件..." tar -xzf "$BACKUP_FILE" -C "$TEMP_RESTORE_DIR" # 查找备份根目录 BACKUP_ROOT=$(find "$TEMP_RESTORE_DIR" -name "ocngx_backup_*" -type d | head -1) if [[ -z "$BACKUP_ROOT" ]]; then echo "❌ 错误: 无法找到备份根目录" exit 1 fi echo " ✅ 备份解压完成: $BACKUP_ROOT" } # 显示备份信息 show_backup_info() { echo "📋 备份信息:" local metadata_file="$BACKUP_ROOT/backup_metadata.json" if [[ -f "$metadata_file" ]]; then echo " 🆔 备份ID: $(jq -r '.backup_id' "$metadata_file" 2>/dev/null || echo "未知")" echo " 📅 备份时间: $(jq -r '.backup_date' "$metadata_file" 2>/dev/null || echo "未知")" echo " 🌍 环境: $(jq -r '.environment' "$metadata_file" 2>/dev/null || echo "未知")" echo " 🖥️ 主机名: $(jq -r '.hostname' "$metadata_file" 2>/dev/null || echo "未知")" echo " 🤖 OpenCode版本: $(jq -r '.opencode_version' "$metadata_file" 2>/dev/null || echo "未知")" else echo " ⚠️ 未找到备份元数据" fi echo "" } # 恢复配置文件 restore_configs() { if [[ "$SKIP_CONFIG" == true ]]; then echo "⏭️ 跳过配置文件恢复" return fi echo "⚙️ 恢复配置文件..." local config_source="$BACKUP_ROOT/configs" if [[ ! -d "$config_source" ]]; then echo " ⚠️ 未找到配置文件备份" return fi # 备份现有配置 if [[ "$FORCE_RESTORE" == true ]] && [[ -f "$RESTORE_TARGET/opencode.json" ]]; then echo " 💾 备份现有配置..." mkdir -p "$RESTORE_TARGET/.backup_$(date +%s)" cp -r "$RESTORE_TARGET"/opencode.json "$RESTORE_TARGET"/docker-start.sh "$RESTORE_TARGET/.backup_$(date +%s)/" 2>/dev/null || true fi # 恢复核心配置文件 local files_to_restore=("opencode.json" "AGENT.md" "docker-start.sh" "Dockerfile") for file in "${files_to_restore[@]}"; do if [[ -f "$config_source/$file" ]]; then cp "$config_source/$file" "$RESTORE_TARGET/" echo " ✅ 已恢复: $file" fi done # 恢复 Nginx 配置 if [[ -d "$config_source/nginx" ]]; then cp -r "$config_source/nginx"/* "$RESTORE_TARGET/nginx/" 2>/dev/null || true echo " ✅ 已恢复: Nginx 配置" fi # 恢复 Cron 配置 if [[ -d "$config_source/cron-jobs" ]]; then cp -r "$config_source/cron-jobs"/* "$RESTORE_TARGET/cron-jobs/" 2>/dev/null || true echo " ✅ 已恢复: Cron 配置" fi echo " ✅ 配置文件恢复完成" } # 恢复 OpenCode 数据 restore_opencode_data() { echo "🤖 恢复 OpenCode 数据..." local data_source="$BACKUP_ROOT/opencode_data" if [[ ! -d "$data_source" ]]; then echo " ⚠️ 未找到 OpenCode 数据备份" return fi # 确定 OpenCode 数据目录 local opencode_data_dir="${HOME}/.opencode" mkdir -p "$opencode_data_dir" # 恢复数据目录 for subdir in "projects" "sessions" "config" "cache" "logs"; do if [[ -d "$data_source/$subdir" ]]; then cp -r "$data_source/$subdir" "$opencode_data_dir/" echo " 📦 已恢复: $subdir" fi done # 恢复配置文件 local config_files=("config.json" "preferences.json") for file in "${config_files[@]}"; do if [[ -f "$data_source/$file" ]]; then cp "$data_source/$file" "$opencode_data_dir/" echo " ⚙️ 已恢复: $file" fi done # 设置正确的权限 chmod -R 755 "$opencode_data_dir" 2>/dev/null || true echo " ✅ OpenCode 数据恢复完成" } # 更新环境配置 update_environment_config() { echo "🔄 更新环境配置..." # 检测当前 HuggingFace Space URL if [[ -n "${SPACE_ID:-}" ]]; then local space_url="https://${SPACE_ID}.hf.space" echo " 🌐 检测到 HuggingFace Space URL: $space_url" # 更新 docker-start.sh 中的 URL 配置 local docker_start_file="$RESTORE_TARGET/docker-start.sh" if [[ -f "$docker_start_file" ]]; then sed -i.bak "s|https://[^.]*\.hf\.space|$space_url|g" "$docker_start_file" echo " ✅ 已更新 Space URL 配置" fi fi # 设置正确的文件权限 chmod +x "$RESTORE_TARGET/docker-start.sh" 2>/dev/null || true chmod +x "$RESTORE_TARGET/cron-jobs"/*.sh 2>/dev/null || true echo " ✅ 环境配置更新完成" } # 验证恢复结果 verify_restore() { echo "🔍 验证恢复结果..." local errors=0 # 检查关键文件 local critical_files=( "$RESTORE_TARGET/opencode.json" "$RESTORE_TARGET/docker-start.sh" "$RESTORE_TARGET/Dockerfile" "$RESTORE_TARGET/nginx/conf.d/default.conf" ) for file in "${critical_files[@]}"; do if [[ -f "$file" ]]; then echo " ✅ 存在: $file" else echo " ❌ 缺失: $file" ((errors++)) fi done # 检查数据目录 local opencode_data_dir="${HOME}/.opencode" if [[ -d "$opencode_data_dir" ]]; then echo " ✅ OpenCode 数据目录存在" else echo " ⚠️ OpenCode 数据目录不存在" fi if [[ $errors -eq 0 ]]; then echo " ✅ 恢复验证通过" else echo " ❌ 恢复验证失败,发现 $errors 个错误" return 1 fi } # 清理临时文件 cleanup() { echo "🧹 清理临时文件..." rm -rf "$TEMP_RESTORE_DIR" echo " ✅ 清理完成" } # 显示恢复后说明 show_post_restore_info() { echo "" echo "🎉 恢复完成!" echo "" echo "📋 后续步骤:" echo "1. 🔄 重启服务以应用新配置:" echo " docker-compose down && docker-compose up -d" echo "" echo "2. 🔍 验证服务状态:" echo " curl http://localhost:7860/health" echo "" echo "3. 📖 检查 OpenCode 状态:" echo " curl http://localhost:3000/global/health" echo "" echo "4. 🌐 访问 Web 界面:" echo " http://localhost:7860" echo "" if [[ "$SKIP_CONFIG" == true ]]; then echo "⚠️ 注意: 配置文件恢复已跳过,请手动检查配置" fi if [[ "$FORCE_RESTORE" == true ]]; then echo "💾 原始配置已备份到 .backup_*/ 目录" fi } # 主函数 main() { echo "🚀 OCNGX 恢复开始..." echo "📂 备份文件: $BACKUP_FILE" echo "📅 恢复时间: $(date '+%Y-%m-%d %H:%M:%S')" echo "" verify_backup check_environment pre_restore_checks prepare_restore show_backup_info restore_configs restore_opencode_data update_environment_config if verify_restore; then cleanup show_post_restore_info echo "" echo "✅ 恢复成功完成!" else echo "" echo "❌ 恢复过程中发现问题,请检查上述错误信息" cleanup exit 1 fi } # 错误处理 trap 'echo "❌ 恢复过程中发生错误"; cleanup; exit 1' ERR # 执行主函数 main "$@"