|
|
#!/bin/bash |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 "🔍 检查恢复环境..." |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
if [[ -d "$config_source/nginx" ]]; then |
|
|
cp -r "$config_source/nginx"/* "$RESTORE_TARGET/nginx/" 2>/dev/null || true |
|
|
echo " ✅ 已恢复: Nginx 配置" |
|
|
fi |
|
|
|
|
|
|
|
|
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 " ✅ 配置文件恢复完成" |
|
|
} |
|
|
|
|
|
|
|
|
restore_opencode_data() { |
|
|
echo "🤖 恢复 OpenCode 数据..." |
|
|
|
|
|
local data_source="$BACKUP_ROOT/opencode_data" |
|
|
if [[ ! -d "$data_source" ]]; then |
|
|
echo " ⚠️ 未找到 OpenCode 数据备份" |
|
|
return |
|
|
fi |
|
|
|
|
|
|
|
|
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 "🔄 更新环境配置..." |
|
|
|
|
|
|
|
|
if [[ -n "${SPACE_ID:-}" ]]; then |
|
|
local space_url="https://${SPACE_ID}.hf.space" |
|
|
echo " 🌐 检测到 HuggingFace Space URL: $space_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 "$@" |