413 lines
13 KiB
Bash
Executable File
413 lines
13 KiB
Bash
Executable File
#!/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)'"
|