File size: 11,789 Bytes
7f2e1a6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
#!/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 "$@"