refactor: 6→2 — backup.sh + restore.sh 统一备份/恢复脚本

- backup.sh: --db-only / --full / 默认站点完整备份
- restore.sh: --db / --full / 默认交互式站点恢复
- auto-discovery 减少到 1 份(旧方案 6 份)
- 删除旧脚本 backup-sites/db/full.sh + restore-sites/db/full.sh
- 更新 README
This commit is contained in:
aiyimickey 2026-07-08 12:30:12 +08:00
parent ec6d2c8c4a
commit ad2305305b
4 changed files with 554 additions and 43 deletions

View File

@ -20,12 +20,8 @@
| 脚本 | 用途 | 说明 |
|------|------|------|
| `backup/backup-sites.sh` | 站点独立备份 | **推荐**:按站点独立打包 |
| `backup/backup-db.sh` | 仅数据库备份 | 轻量快速 |
| `backup/backup-full.sh` | 完整灾难恢复备份 | 包含所有数据 |
| `backup/restore-sites.sh` | 站点恢复 | 交互式选择恢复 |
| `backup/restore-db.sh` | 数据库恢复 | 恢复单个数据库 |
| `backup/restore-full.sh` | 完整恢复 | 灾难恢复使用 |
| `backup/backup.sh` | 统一备份 | --db-only / --full |
| `backup/restore.sh` | 统一恢复 | --db / --full |
> 详细文档见 [backup/README.md](backup/README.md)
@ -88,19 +84,19 @@ LDAP 部署特殊处理:上传 Dockerfile + docker-compose.yml自动创建
## backup/ — 备份与恢复
```bash
# 站点独立备份(推荐)
bash scripts/backup/backup-sites.sh # 备份所有站点
bash scripts/backup/backup-sites.sh assets issue # 备份指定站点
# 站点完整备份(推荐)
bash scripts/backup/backup.sh # 备份所有
bash scripts/backup/backup.sh assets monitor # 指定站点
bash scripts/backup/backup.sh --db-only # 仅数据库
bash scripts/backup/backup.sh --full # 灾难备份
# 恢复
bash scripts/backup/restore-sites.sh # 交互式恢复
bash scripts/backup/restore-sites.sh 20260622_101858 # 指定时间戳
# 数据库单独备份
bash scripts/backup/backup-db.sh
bash scripts/backup/restore.sh # 交互式
bash scripts/backup/restore.sh --db issue file.db # 数据库恢复
bash scripts/backup/restore.sh --full backup.tar # 灾难恢复
```
支持站点:`assets` / `issue` / `ldap` / `gitea` / `oa` / `monitor`(自动发现,无需手动添加)
支持站点:自动发现所有含 DB 的 `*-ai` 目录,无需手动添加
备份内容:数据库 + 上传文件 + 配置文件 + nginx + SSL 证书

View File

@ -6,51 +6,46 @@
```
scripts/backup/
├── backup-db.sh # 数据库备份(轻量)
├── backup-full.sh # 完整灾难恢复备份
├── backup-sites.sh # 站点独立备份(推荐)
├── restore-db.sh # 数据库恢复
├── restore-full.sh # 完整恢复
└── restore-sites.sh # 站点恢复
├── backup.sh # 统一备份(默认=站点完整,--db-only=仅DB--full=灾难)
└── restore.sh # 统一恢复(默认=交互式,--db=数据库,--full=灾难)
```
## 备份策略对比
## 备份策略
| 脚本 | 内容 | 大小 | 用途 |
| 命令 | 内容 | 大小 | 用途 |
|------|------|------|------|
| backup-db.sh | 仅数据库 | ~3M | 日常快速备份 |
| backup-sites.sh | 数据库+配置+文件 | ~135M | **推荐**:按站点独立备份 |
| backup-full.sh | 全部数据 | ~135M | 完整灾难恢复 |
| `backup.sh` | 数据库+配置+文件 | ~135M | **推荐**:按站点独立备份 |
| `backup.sh --db-only` | 仅数据库 | ~3M | 日常快速备份 |
| `backup.sh --full` | 全部数据 | ~135M | 完整灾难恢复 |
## 使用方法
### 站点独立备份(推荐)
### 备份
```bash
# 备份所有站点
bash scripts/backup/backup-sites.sh
# 站点完整备份(默认,推荐)
bash scripts/backup/backup.sh
bash scripts/backup/backup.sh assets monitor # 指定站点
# 仅备份指定站点
bash scripts/backup/backup-sites.sh assets issue
# 仅数据库
bash scripts/backup/backup.sh --db-only
# 可用站点(自动发现含 DB 的 *-ai 目录assets, issue, ldap, gitea, oa, monitor
# 灾难备份
bash scripts/backup/backup.sh --full
```
### 恢复站点
### 恢复
```bash
# 交互式恢复(自动检测备份,选择站点)
bash scripts/backup/restore-sites.sh
# 交互式恢复(默认,自动检测备份)
bash scripts/backup/restore.sh
bash scripts/backup/restore.sh 20260708_120000 # 指定时间戳
# 指定时间戳恢复
bash scripts/backup/restore-sites.sh 20260622_101858
```
# 数据库恢复
bash scripts/backup/restore.sh --db monitor monitor-20260708.db
### 数据库单独备份
```bash
# 仅备份数据库(快速)
bash scripts/backup/backup-db.sh
# 灾难恢复
bash scripts/backup/restore.sh --full full-backup-20260708.tar.gz
```
## 备份内容

206
backup/backup.sh Normal file
View File

@ -0,0 +1,206 @@
#!/bin/bash
# 统一备份脚本
# 用法:
# bash backup.sh # 站点完整备份(默认,推荐)
# bash backup.sh assets monitor # 备份指定站点
# bash backup.sh --db-only # 仅数据库备份
# bash backup.sh --full # 灾难恢复完整备份
set -e
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
LOCAL_DIR="/Users/niuniu/programs/docker/db-backups"
REMOTE_BASE="/root/docker"
CLOUD_DIR="/root/docker/db-backups"
log() { echo "[$(date '+%H:%M:%S')] $1"; }
warn() { echo "[$(date '+%H:%M:%S')] [警告] $1"; }
# === 自动发现含数据库的站点 ===
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DB_SITES=""
for dir in "$PROJECT_ROOT"/*-ai/; do
s=$(basename "$dir"); [[ "$s" == "nginx-proxy-ai" ]] && continue
[ -f "$dir/docker-compose.yml" ] || continue
grep -qE "\.db|DATABASE_PATH|sqlite" "$dir/docker-compose.yml" 2>/dev/null || continue
[[ "$s" == "oa" ]] && continue
DB_SITES="$DB_SITES $s"
done
# === 模式 1站点完整备份默认===
backup_site() {
local site="$1"
local backup_name="${site}-${TIMESTAMP}.tar.gz"
log "备份 ${site}..."
ssh txjp "rm -rf /tmp/backup-${site} && mkdir -p /tmp/backup-${site}/data"
case "$site" in
assets)
ssh txjp "docker exec assets-ai node -e \"const D=require('better-sqlite3');const db=new D('/app/data/assets.db');db.pragma('wal_checkpoint(TRUNCATE)');db.close();\"" 2>/dev/null || true
ssh txjp "docker cp assets-ai:/app/data/assets.db /tmp/backup-${site}/data/"
ssh txjp "cp -a /root/docker/assets-ai/data/uploads /tmp/backup-${site}/data/ 2>/dev/null || true"
ssh txjp "cp /root/docker/assets-ai/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true"
ssh txjp "cp /root/docker/assets-ai/.env /tmp/backup-${site}/ 2>/dev/null || true"
ssh txjp "cp /root/docker/assets-ai/Dockerfile /tmp/backup-${site}/ 2>/dev/null || true"
;;
issue)
ssh txjp "docker exec issue-ai node -e \"const D=require('better-sqlite3');const db=new D('/app/data/issue.db');db.pragma('wal_checkpoint(TRUNCATE)');db.close();\"" 2>/dev/null || true
ssh txjp "docker cp issue-ai:/app/data/issue.db /tmp/backup-${site}/data/"
ssh txjp "cp -a /root/docker/issue-ai/data/uploads /tmp/backup-${site}/data/ 2>/dev/null || true"
ssh txjp "cp -a /root/docker/issue-ai/data/reports /tmp/backup-${site}/data/ 2>/dev/null || true"
ssh txjp "cp /root/docker/issue-ai/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true"
ssh txjp "cp /root/docker/issue-ai/.env /tmp/backup-${site}/ 2>/dev/null || true"
ssh txjp "cp /root/docker/issue-ai/Dockerfile /tmp/backup-${site}/ 2>/dev/null || true"
;;
ldap)
ssh txjp "docker exec lldap sqlite3 /data/users.db 'PRAGMA wal_checkpoint(TRUNCATE);'" 2>/dev/null || true
ssh txjp "docker cp lldap:/data/users.db /tmp/backup-${site}/data/"
ssh txjp "cp /root/docker/ldap-ai/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true"
ssh txjp "cp /root/docker/ldap-ai/.env /tmp/backup-${site}/ 2>/dev/null || true"
ssh txjp "cp /root/docker/ldap-ai/Dockerfile /tmp/backup-${site}/ 2>/dev/null || true"
;;
gitea)
ssh txjp "docker cp gitea-ai:/data/gitea.db /tmp/backup-${site}/data/"
ssh txjp "cp -a /root/docker/gitea-ai/data/git /tmp/backup-${site}/ 2>/dev/null || true"
ssh txjp "cp -a /root/docker/gitea-ai/data/gitea/conf /tmp/backup-${site}/gitea-conf 2>/dev/null || true"
ssh txjp "cp /root/docker/gitea-ai/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true"
;;
oa)
ssh txjp "cp /root/docker/oa-ai/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true"
ssh txjp "cp /root/docker/oa-ai/.env /tmp/backup-${site}/ 2>/dev/null || true"
;;
monitor)
ssh txjp "docker exec monitor-ai sqlite3 /app/data/monitor.db 'PRAGMA wal_checkpoint(TRUNCATE);'" 2>/dev/null || true
ssh txjp "docker cp monitor-ai:/app/data/monitor.db /tmp/backup-${site}/data/"
ssh txjp "cp /root/docker/monitor-ai/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true"
ssh txjp "cp /root/docker/monitor-ai/.env /tmp/backup-${site}/ 2>/dev/null || true"
;;
*)
warn "${site}: 无专用备份逻辑,仅备份配置文件"
ssh txjp "cp /root/docker/${site}/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true"
ssh txjp "cp /root/docker/${site}/.env /tmp/backup-${site}/ 2>/dev/null || true"
;;
esac
ssh txjp "cd /tmp && tar czf ${backup_name} backup-${site}/"
mkdir -p "${LOCAL_DIR}"; scp "txjp:/tmp/${backup_name}" "${LOCAL_DIR}/"
ssh txjp "mkdir -p ${CLOUD_DIR} && cp /tmp/${backup_name} ${CLOUD_DIR}/"
ssh txjp "rm -rf /tmp/backup-${site} /tmp/${backup_name}"
log "${site} 备份完成: ${backup_name}"
}
backup_shared() {
local backup_name="shared-${TIMESTAMP}.tar.gz"
log "备份共享资源..."
ssh txjp "rm -rf /tmp/backup-shared && mkdir -p /tmp/backup-shared"
ssh txjp "mkdir -p /tmp/backup-shared/nginx && cp -a /root/docker/nginx-proxy-ai/conf.d/* /tmp/backup-shared/nginx/ 2>/dev/null || true"
ssh txjp "mkdir -p /tmp/backup-shared/ssl && cp -a /etc/letsencrypt/live /tmp/backup-shared/ssl/ 2>/dev/null || true && cp -a /etc/letsencrypt/archive /tmp/backup-shared/ssl/ 2>/dev/null || true && cp -a /etc/letsencrypt/renewal /tmp/backup-shared/ssl/ 2>/dev/null || true"
ssh txjp "cp /root/docker/nginx-proxy-ai/docker-compose.yml /tmp/backup-shared/ 2>/dev/null || true"
ssh txjp "cd /tmp && tar czf ${backup_name} backup-shared/"
mkdir -p "${LOCAL_DIR}"; scp "txjp:/tmp/${backup_name}" "${LOCAL_DIR}/"
ssh txjp "mkdir -p ${CLOUD_DIR} && cp /tmp/${backup_name} ${CLOUD_DIR}/"
ssh txjp "rm -rf /tmp/backup-shared /tmp/${backup_name}"
log "共享资源备份完成: ${backup_name}"
}
# === 模式 2仅数据库备份 ===
backup_db_only() {
log "开始数据库备份..."
declare -A DB_CONTAINERS=([ldap]="lldap:/data/users.db" [gitea]="gitea-ai:/data/gitea.db")
for s in $DB_SITES; do
if [[ -n "${DB_CONTAINERS[$s]}" ]]; then
ssh txjp "docker exec ${DB_CONTAINERS[$s]%%:*} sqlite3 ${DB_CONTAINERS[$s]##*:} 'PRAGMA wal_checkpoint(TRUNCATE);' 2>/dev/null || true" &
else
ssh txjp "docker exec ${s} sqlite3 /app/data/*.db 'PRAGMA wal_checkpoint(TRUNCATE);' 2>/dev/null || true" &
fi
done; wait
ssh txjp "mkdir -p /tmp/db-backup"
for s in $DB_SITES; do
if [[ -n "${DB_CONTAINERS[$s]}" ]]; then
cname="${DB_CONTAINERS[$s]%%:*}"; dbpath="${DB_CONTAINERS[$s]##*:}"
else
cname="${s}"; short="${s%-ai}"; dbpath="/app/data/${short}.db"
fi
log "备份 ${s}..."
ssh txjp "docker cp ${cname}:${dbpath} /tmp/db-backup/${s}-${TIMESTAMP}.db"
done
mkdir -p "${LOCAL_DIR}"
ssh txjp "mkdir -p ${CLOUD_DIR}"
for s in $DB_SITES; do
ssh txjp "cp /tmp/db-backup/${s}-${TIMESTAMP}.db ${CLOUD_DIR}/"
scp "txjp:/tmp/db-backup/${s}-${TIMESTAMP}.db" "${LOCAL_DIR}/"
f="${LOCAL_DIR}/${s}-${TIMESTAMP}.db"
sqlite3 "$f" "SELECT count(*) FROM sqlite_master" > /dev/null 2>&1 || warn "${s} 备份文件无效"
done
ssh txjp "rm -rf /tmp/db-backup"
log "数据库备份完成"
}
# === 模式 3灾难恢复完整备份 ===
backup_full() {
log "开始完整灾难恢复备份..."
for s in $DB_SITES; do
case "$s" in
ldap) ssh txjp "docker exec lldap sqlite3 /data/users.db 'PRAGMA wal_checkpoint(TRUNCATE);'" 2>/dev/null || true ;;
*) ssh txjp "docker exec ${s} sqlite3 /app/data/*.db 'PRAGMA wal_checkpoint(TRUNCATE);' 2>/dev/null || true" & ;;
esac
done; wait
ssh txjp "mkdir -p /tmp/full-backup/databases"
for s in $DB_SITES; do
case "$s" in
ldap) ssh txjp "docker cp lldap:/data/users.db /tmp/full-backup/databases/ldap.db" ;;
gitea) ssh txjp "docker cp gitea-ai:/data/gitea.db /tmp/full-backup/databases/gitea.db" ;;
*) short="${s%-ai}"; ssh txjp "docker cp ${s}:/app/data/${short}.db /tmp/full-backup/databases/${short}.db" ;;
esac
done
# Gitea 代码仓库
ssh txjp "mkdir -p /tmp/full-backup/gitea && cp -a /root/docker/gitea-ai/data/git /tmp/full-backup/gitea/ && cp -a /root/docker/gitea-ai/data/gitea/conf /tmp/full-backup/gitea/ 2>/dev/null || true"
# 上传文件
ssh txjp "mkdir -p /tmp/full-backup/uploads && cp -a /root/docker/assets-ai/data/uploads /tmp/full-backup/uploads/assets-uploads 2>/dev/null || true && cp -a /root/docker/issue-ai/data/uploads /tmp/full-backup/uploads/issue-uploads 2>/dev/null || true && cp -a /root/docker/issue-ai/data/reports /tmp/full-backup/uploads/issue-reports 2>/dev/null || true"
# Nginx + SSL
ssh txjp "mkdir -p /tmp/full-backup/nginx && cp -a /root/docker/nginx-proxy-ai/conf.d/* /tmp/full-backup/nginx/ 2>/dev/null || true"
ssh txjp "mkdir -p /tmp/full-backup/ssl && cp -a /etc/letsencrypt/live /tmp/full-backup/ssl/ 2>/dev/null || true && cp -a /etc/letsencrypt/archive /tmp/full-backup/ssl/ 2>/dev/null || true && cp -a /etc/letsencrypt/renewal /tmp/full-backup/ssl/ 2>/dev/null || true"
local name="full-backup-${TIMESTAMP}"; ssh txjp "cd /tmp && tar czf ${name}.tar.gz full-backup/"
mkdir -p "${LOCAL_DIR}"; scp "txjp:/tmp/${name}.tar.gz" "${LOCAL_DIR}/"
ssh txjp "mkdir -p ${CLOUD_DIR} && cp /tmp/${name}.tar.gz ${CLOUD_DIR}/"
ssh txjp "rm -rf /tmp/full-backup /tmp/${name}.tar.gz"
log "完整备份完成: ${name}.tar.gz"
}
# === 主流程 ===
case "${1:-}" in
--db-only)
backup_db_only ;;
--full)
backup_full ;;
--help|-h)
echo "用法: bash backup.sh [--db-only|--full] [站点名...]"
echo " 无参数 站点完整备份(全部含 DB 站点)"
echo " --db-only 仅数据库备份"
echo " --full 灾难恢复完整备份"
echo " 站点名... 备份指定站点"
;;
*)
TARGETS="${@:-all}"
log "站点完整备份: ${TARGETS}"
if [[ "$TARGETS" == "all" ]]; then
ALL="$(for s in $DB_SITES; do echo -n "$s "; done) oa"
for site in $ALL; do backup_site "$site"; done
backup_shared
else
for site in $TARGETS; do backup_site "$site"; done
fi
;;
esac
echo ""; log "备份完成!"
ls -lh ${LOCAL_DIR}/*-${TIMESTAMP}* 2>/dev/null

314
backup/restore.sh Normal file
View File

@ -0,0 +1,314 @@
#!/bin/bash
# 统一恢复脚本
# 用法:
# bash restore.sh # 交互式站点恢复(默认)
# bash restore.sh 20260708_120000 # 指定时间戳恢复
# bash restore.sh --db issue issue.db # 数据库恢复
# bash restore.sh --full full-backup.tar.gz # 灾难恢复
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; }
RESTORE_DIR="$(mktemp -d)"
# === 自动发现可恢复站点 ===
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
ALL_SITES="shared"
for dir in "$PROJECT_ROOT"/*-ai/; do
s=$(basename "$dir"); [[ "$s" == "nginx-proxy-ai" ]] && continue
[ -f "$dir/docker-compose.yml" ] || continue
ALL_SITES="$ALL_SITES $s"
done
# === 工具函数 ===
check_backup() {
local site="$1" ts="$2"
[[ -f "${BACKUP_DIR}/${site}-${ts}.tar.gz" ]] && echo "local:${BACKUP_DIR}/${site}-${ts}.tar.gz" && return
ssh txjp "test -f /root/docker/db-backups/${site}-${ts}.tar.gz" 2>/dev/null && echo "cloud:${site}-${ts}.tar.gz" && return
echo ""
}
download_and_extract() {
local site="$1" ts="$2"
local backup_info=$(check_backup "$site" "$ts")
[[ -z "$backup_info" ]] && error "${site} 备份文件不存在: ${ts}"
local src="${backup_info#*:}"
rm -rf "$RESTORE_DIR/backup-${site}"; mkdir -p "$RESTORE_DIR/backup-${site}"
if [[ "$backup_info" == local:* ]]; then
log "从本地解压: ${BACKUP_DIR}/${site}-${ts}.tar.gz"
tar xzf "${BACKUP_DIR}/${site}-${ts}.tar.gz" -C "$RESTORE_DIR/backup-${site}" 2>/dev/null || tar xzf "${BACKUP_DIR}/${site}-${ts}.tar.gz" -C "$RESTORE_DIR/backup-${site}" --strip-components=1 2>/dev/null || tar tzf "${BACKUP_DIR}/${site}-${ts}.tar.gz" --show-transformed-names 2>/dev/null | head -1
tar xzf "${BACKUP_DIR}/${site}-${ts}.tar.gz" -C "$RESTORE_DIR/backup-${site}"
else
log "从云端下载: ${ts}"
scp "txjp:${src}" "$RESTORE_DIR/"
local f="$RESTORE_DIR/${site}-${ts}.tar.gz"
mkdir -p "$RESTORE_DIR/backup-${site}"
tar xzf "$f" -C "$RESTORE_DIR/backup-${site}"
fi
}
# === 站点恢复函数 ===
restore_shared() {
local ts="$TIMESTAMP"
local src="${RESTORE_DIR}/backup-shared"
[[ -f "$src/nginx"/*.conf ]] && { mkdir -p "$REMOTE_BASE/nginx-proxy-ai/conf.d"; scp "$src/nginx/"* txjp:"$REMOTE_BASE/nginx-proxy-ai/conf.d/"; }
ssh txjp "docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" 2>/dev/null || warn "nginx 重载失败"
log "共享配置恢复完成"
}
restore_assets() {
log "恢复 assets..."
download_and_extract "assets" "$TIMESTAMP"
local src="${RESTORE_DIR}/backup-assets/backup-assets"
[[ ! -d "$src" ]] && src="${RESTORE_DIR}/backup-assets"
for f in docker-compose.yml .env Dockerfile; do
[[ -f "$src/$f" ]] && scp "$src/$f" txjp:$REMOTE_BASE/assets-ai/
done
[[ -d "$src/data/uploads" ]] && ssh txjp "rm -rf /root/docker/assets-ai/data/uploads && cp -a $src/data/uploads /root/docker/assets-ai/data/"
[[ -f "$src/data/assets.db" ]] && ssh txjp "docker cp $src/data/assets.db assets-ai:/app/data/assets.db"
ssh txjp "cd $REMOTE_BASE/assets-ai && docker compose down 2>/dev/null || true && docker compose up -d"
log "assets 恢复完成"
}
restore_issue() {
log "恢复 issue..."
download_and_extract "issue" "$TIMESTAMP"
local src="${RESTORE_DIR}/backup-issue/backup-issue"
[[ ! -d "$src" ]] && src="${RESTORE_DIR}/backup-issue"
for f in docker-compose.yml .env Dockerfile; do
[[ -f "$src/$f" ]] && scp "$src/$f" txjp:$REMOTE_BASE/issue-ai/
done
[[ -d "$src/data/uploads" ]] && ssh txjp "rm -rf /root/docker/issue-ai/data/uploads && cp -a $src/data/uploads /root/docker/issue-ai/data/"
[[ -d "$src/data/reports" ]] && ssh txjp "rm -rf /root/docker/issue-ai/data/reports && cp -a $src/data/reports /root/docker/issue-ai/data/"
[[ -f "$src/data/issue.db" ]] && ssh txjp "docker cp $src/data/issue.db issue-ai:/app/data/issue.db"
ssh txjp "cd $REMOTE_BASE/issue-ai && docker compose down 2>/dev/null || true && docker compose up -d"
log "issue 恢复完成"
}
restore_ldap() {
log "恢复 ldap..."
download_and_extract "ldap" "$TIMESTAMP"
local src="${RESTORE_DIR}/backup-ldap/backup-ldap"
[[ ! -d "$src" ]] && src="${RESTORE_DIR}/backup-ldap"
for f in docker-compose.yml .env Dockerfile; do
[[ -f "$src/$f" ]] && scp "$src/$f" txjp:$REMOTE_BASE/ldap-ai/
done
[[ -f "$src/data/users.db" ]] && ssh txjp "docker cp $src/data/users.db lldap:/data/users.db"
ssh txjp "cd $REMOTE_BASE/ldap-ai && docker compose down 2>/dev/null || true && docker compose up -d"
log "ldap 恢复完成"
}
restore_gitea() {
log "恢复 gitea..."
download_and_extract "gitea" "$TIMESTAMP"
local src="${RESTORE_DIR}/backup-gitea/backup-gitea"
[[ ! -d "$src" ]] && src="${RESTORE_DIR}/backup-gitea"
[[ -f "$src/docker-compose.yml" ]] && scp "$src/docker-compose.yml" txjp:$REMOTE_BASE/gitea-ai/
[[ -f "$src/data/gitea.db" ]] && ssh txjp "docker cp $src/data/gitea.db gitea-ai:/data/gitea.db"
ssh txjp "cd $REMOTE_BASE/gitea-ai && docker compose down && docker compose up -d"
log "gitea 恢复完成"
}
restore_oa() {
log "恢复 oa..."
download_and_extract "oa" "$TIMESTAMP"
local src="${RESTORE_DIR}/backup-oa/backup-oa"
[[ ! -d "$src" ]] && src="${RESTORE_DIR}/backup-oa"
for f in docker-compose.yml .env; do
[[ -f "$src/$f" ]] && scp "$src/$f" txjp:$REMOTE_BASE/oa-ai/
done
ssh txjp "cd $REMOTE_BASE/oa-ai && docker compose down 2>/dev/null || true && docker compose up -d"
log "oa 恢复完成"
}
restore_monitor() {
log "恢复 monitor..."
download_and_extract "monitor" "$TIMESTAMP"
local src="${RESTORE_DIR}/backup-monitor/backup-monitor"
[[ ! -d "$src" ]] && src="${RESTORE_DIR}/backup-monitor"
for f in docker-compose.yml .env; do
[[ -f "$src/$f" ]] && scp "$src/$f" txjp:$REMOTE_BASE/monitor-ai/
done
[[ -f "$src/data/monitor.db" ]] && ssh txjp "docker cp $src/data/monitor.db monitor-ai:/app/data/monitor.db"
ssh txjp "cd $REMOTE_BASE/monitor-ai && docker compose down 2>/dev/null || true && docker compose up -d --build"
log "monitor 恢复完成"
}
restore_generic() {
local site="$1"
log "恢复 ${site}(通用模式)..."
download_and_extract "${site}" "$TIMESTAMP"
local src="${RESTORE_DIR}/backup-${site}/backup-${site}"
[[ ! -d "$src" ]] && src="${RESTORE_DIR}/backup-${site}"
for f in docker-compose.yml .env Dockerfile; do
[[ -f "$src/$f" ]] && scp "$src/$f" txjp:$REMOTE_BASE/${site}/
done
ssh txjp "cd $REMOTE_BASE/${site} && docker compose down 2>/dev/null || true && docker compose up -d" 2>/dev/null || warn "${site} 容器重启失败"
}
# === 模式 2数据库恢复 ===
restore_db() {
local service="$1" file="$2"
[[ -z "$service" || -z "$file" ]] && error "用法: bash restore.sh --db <assets|issue|ldap|gitea|monitor> <文件名>"
local container="" dbpath="" project=""
case "$service" in
assets) project="assets-ai"; container="assets-ai"; dbpath="/app/data/assets.db" ;;
issue) project="issue-ai"; container="issue-ai"; dbpath="/app/data/issue.db" ;;
ldap) project="ldap-ai"; container="lldap"; dbpath="/data/users.db" ;;
gitea) project="gitea-ai"; container="gitea-ai"; dbpath="/data/gitea.db" ;;
monitor) project="monitor-ai"; container="monitor-ai"; dbpath="/app/data/monitor.db" ;;
*) error "服务名必须是: assets, issue, ldap, gitea, monitor" ;;
esac
local src="$BACKUP_DIR/$file"
[[ ! -f "$src" ]] && src="$file"
log "恢复 ${service} 数据库: ${src}..."
scp "$src" "txjp:/tmp/db-restore.db"
ssh txjp "docker cp /tmp/db-restore.db ${container}:${dbpath} && rm /tmp/db-restore.db"
ssh txjp "cd $REMOTE_BASE/$project && docker compose restart" 2>/dev/null || true
log "${service} 数据库恢复完成"
}
# === 模式 3灾难恢复 ===
restore_full() {
local file="$1"
[[ -z "$file" ]] && error "用法: bash restore.sh --full <backup.tar.gz>"
local src="$BACKUP_DIR/$file"
[[ ! -f "$src" ]] && src="$file"
[[ ! -f "$src" ]] && error "备份文件不存在: $file"
log "完整恢复: $file"
rm -rf "$RESTORE_DIR/full"; mkdir -p "$RESTORE_DIR/full"
tar xzf "$src" -C "$RESTORE_DIR/full"
local content="$RESTORE_DIR/full/full-backup"
echo ""; echo " 1) assets 2) issue 3) ldap 4) gitea"
echo " 5) monitor 6) nginx 7) 全部恢复"
echo ""; printf "请选择 (1-7): "; read choice
case "$choice" in
1) restore_assets_from_full "$content" ;;
2) restore_issue_from_full "$content" ;;
3) restore_ldap_from_full "$content" ;;
4) restore_gitea_from_full "$content" ;;
5) restore_monitor_from_full "$content" ;;
6) restore_nginx_from_full "$content" ;;
7) restore_all_from_full "$content" ;;
*) error "无效选择" ;;
esac
}
restore_assets_from_full() {
log "恢复 assets..."
[[ -f "$1/databases/assets.db" ]] && ssh txjp "docker cp $1/databases/assets.db assets-ai:/app/data/assets.db"
ssh txjp "cd $REMOTE_BASE/assets-ai && docker compose down && docker compose up -d"; log "完成"
}
restore_issue_from_full() {
log "恢复 issue..."
[[ -f "$1/databases/issue.db" ]] && ssh txjp "docker cp $1/databases/issue.db issue-ai:/app/data/issue.db"
ssh txjp "cd $REMOTE_BASE/issue-ai && docker compose down && docker compose up -d"; log "完成"
}
restore_ldap_from_full() {
log "恢复 ldap..."
[[ -f "$1/databases/ldap.db" ]] && ssh txjp "docker cp $1/databases/ldap.db lldap:/data/users.db"
ssh txjp "cd $REMOTE_BASE/ldap-ai && docker compose down && docker compose up -d"; log "完成"
}
restore_gitea_from_full() {
log "恢复 gitea..."
[[ -f "$1/databases/gitea.db" ]] && ssh txjp "docker cp $1/databases/gitea.db gitea-ai:/data/gitea.db"
ssh txjp "cd $REMOTE_BASE/gitea-ai && docker compose down && docker compose up -d"; log "完成"
}
restore_monitor_from_full() {
log "恢复 monitor..."
[[ -f "$1/databases/monitor.db" ]] && ssh txjp "docker cp $1/databases/monitor.db monitor-ai:/app/data/monitor.db"
ssh txjp "cd $REMOTE_BASE/monitor-ai && docker compose down && docker compose up -d --build"; log "完成"
}
restore_nginx_from_full() {
log "恢复 nginx..."
[[ -d "$1/nginx" ]] && ssh txjp "mkdir -p $REMOTE_BASE/nginx-proxy-ai/conf.d" && scp "$1/nginx/"* txjp:$REMOTE_BASE/nginx-proxy-ai/conf.d/
ssh txjp "docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload"; log "完成"
}
restore_all_from_full() {
local c="$1"
restore_assets_from_full "$c"
restore_issue_from_full "$c"
restore_ldap_from_full "$c"
restore_gitea_from_full "$c"
restore_monitor_from_full "$c"
restore_nginx_from_full "$c"
}
# === 主流程 ===
case "${1:-}" in
--db)
restore_db "$2" "$3" ;;
--full)
restore_full "$2" ;;
--help|-h)
echo "用法: bash restore.sh [--db|--full] [参数...]"
echo " 无参数 交互式站点恢复"
echo " restore.sh TIMESTAMP 指定时间戳恢复"
echo " --db <site> <file> 数据库恢复"
echo " --full <file> 灾难恢复"
;;
*)
# 交互式站点恢复
TIMESTAMP="${1:-}"
if [[ -z "$TIMESTAMP" ]]; then
echo ""; echo "可用的备份时间戳:"; echo ""
timestamps=$(ls ${BACKUP_DIR}/assets-*.tar.gz 2>/dev/null | sed 's/.*assets-\(.*\)\.tar\.gz/\1/' | sort -r | head -10)
[[ -z "$timestamps" ]] && error "未找到任何备份文件"
declare -A ts_list; 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
TIMESTAMP="${ts_list[$choice]}"
[[ -z "$TIMESTAMP" ]] && error "无效选择"
fi
echo ""; echo "可恢复的站点:"; echo ""
declare -A site_status; index=1
for site in $ALL_SITES; do
local bk=$(check_backup "$site" "$TIMESTAMP")
[[ -n "$bk" ]] && echo " $index) $site" && site_list[$index]="$site" || echo " -) $site (无备份)"
((index++))
done
echo ""; echo " a) 全部恢复"; printf "请选择 (1-$((index-1)) 或 a): "; read s_choice
declare -a restore_list
if [[ "$s_choice" == "a" ]]; then
for idx in "${!site_list[@]}"; do restore_list+=("${site_list[$idx]}"); done
else
restore_list=("${site_list[$s_choice]}")
[[ -z "${restore_list[0]}" ]] && error "无效选择或无备份"
fi
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
ssh txjp "docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" 2>/dev/null || true
rm -rf "$RESTORE_DIR"
echo ""; log "恢复完成!"
ssh txjp "docker ps --format 'table {{.Names}}\t{{.Status}}' | grep -E '(assets|issue|ldap|gitea|oa|monitor|nginx|sso)'"
;;
esac