diff --git a/backup/backup-db.sh b/backup/backup-db.sh deleted file mode 100755 index b182f12..0000000 --- a/backup/backup-db.sh +++ /dev/null @@ -1,98 +0,0 @@ -#!/bin/bash -# 云端数据库一键备份脚本 -# 用法:bash backup-db.sh - -set -e - -TIMESTAMP=$(date +%Y%m%d_%H%M%S) -LOCAL_DIR="/Users/niuniu/programs/docker/db-backups" - -log() { echo "[$(date '+%H:%M:%S')] $1"; } - -# 自动发现含数据库的站点 -PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -DB_SITES="" -for dir in "$PROJECT_ROOT"/*-ai/; do - site=$(basename "$dir") - [[ "$site" == "nginx-proxy-ai" ]] && continue - [ -f "$dir/docker-compose.yml" ] || continue - grep -qE "\.db|DATABASE_PATH|sqlite" "$dir/docker-compose.yml" 2>/dev/null || continue - DB_SITES="$DB_SITES $site" -done - -log "发现数据库站点:${DB_SITES}" -log "开始备份云端数据库..." - -# 站点 → 容器名 + DB路径 映射(自动推导::/app/data/.db) -# LLDAP/gitea 容器名特殊,显式声明 -declare -A DB_CONTAINERS=([ldap]="lldap:/data/users.db" [gitea]="gitea-ai:/data/gitea.db" [oa]="") - -# 并行 WAL checkpoint -for site in $DB_SITES; do - [[ "$site" == "oa" ]] && continue # OA 无独立 DB - if [[ -n "${DB_CONTAINERS[$site]}" ]]; then - cname="${DB_CONTAINERS[$site]%%:*}" - else - cname="${site}" # assets-ai / issue-ai / monitor-ai - fi - log "WAL checkpoint: ${site} (${cname})..." - ssh txjp "docker exec ${cname} sqlite3 /app/data/*.db 'PRAGMA wal_checkpoint(TRUNCATE);' 2>/dev/null || true" & -done -wait - -# 云端打包 -ssh txjp "mkdir -p /tmp/db-backup" -for site in $DB_SITES; do - [[ "$site" == "oa" ]] && continue - if [[ -n "${DB_CONTAINERS[$site]}" ]]; then - cname="${DB_CONTAINERS[$site]%%:*}" - dbpath="${DB_CONTAINERS[$site]##*:}" - else - cname="${site}" - short="${site%-ai}" # assets-ai → assets - dbpath="/app/data/${short}.db" - fi - log "备份 ${site} (${cname}:${dbpath})..." - ssh txjp "docker cp ${cname}:${dbpath} /tmp/db-backup/${site}-${TIMESTAMP}.db" -done - -# 确保本地备份目录存在 -mkdir -p "${LOCAL_DIR}" - -# 云端留一份持久备份 -CLOUD_DIR="/root/docker/db-backups" -ssh txjp "mkdir -p ${CLOUD_DIR}" -for site in $DB_SITES; do - [[ "$site" == "oa" ]] && continue - ssh txjp "cp /tmp/db-backup/${site}-${TIMESTAMP}.db ${CLOUD_DIR}/" -done -log "云端备份已保存至: ${CLOUD_DIR}" - -# 复制到本地 -log "复制到本地..." -mkdir -p "${LOCAL_DIR}" -for site in $DB_SITES; do - [[ "$site" == "oa" ]] && continue - scp "txjp:/tmp/db-backup/${site}-${TIMESTAMP}.db" "${LOCAL_DIR}/" -done - -# 验证备份文件为有效 SQLite 数据库 -log "验证备份完整性..." -for site in $DB_SITES; do - [[ "$site" == "oa" ]] && continue - f="${LOCAL_DIR}/${site}-${TIMESTAMP}.db" - if ! sqlite3 "$f" "SELECT count(*) FROM sqlite_master" > /dev/null 2>&1; then - echo "[错误] 备份文件无效或损坏: $f" - exit 1 - fi -done -log "备份文件验证通过" - -# 清理云端临时文件 -ssh txjp "rm -rf /tmp/db-backup" - -log "备份完成:${TIMESTAMP}" -echo " 云端: ${CLOUD_DIR}/" -ssh txjp "ls -lh ${CLOUD_DIR}/*-${TIMESTAMP}.db 2>/dev/null" -echo " 本地:" -ls -lh "${LOCAL_DIR}"/*-${TIMESTAMP}.db 2>/dev/null diff --git a/backup/backup-full.sh b/backup/backup-full.sh deleted file mode 100755 index 63973bd..0000000 --- a/backup/backup-full.sh +++ /dev/null @@ -1,140 +0,0 @@ -#!/bin/bash -# 完整灾难恢复备份脚本 -# 包含:数据库、代码仓库、用户数据、配置文件、SSL证书 -# 用法:bash backup-full.sh - -set -e - -TIMESTAMP=$(date +%Y%m%d_%H%M%S) -LOCAL_DIR="/Users/niuniu/programs/docker/db-backups" -BACKUP_NAME="full-backup-${TIMESTAMP}" - -log() { echo "[$(date '+%H:%M:%S')] $1"; } -warn() { echo "[$(date '+%H:%M:%S')] [警告] $1"; } - -log "开始完整灾难恢复备份..." - -# ============================================================ -# 1. 数据库备份(WAL checkpoint + docker cp) -# ============================================================ -# 自动发现数据库站点 -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 - -log "执行 WAL checkpoint..." -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 || warn "ldap checkpoint 失败" ;; - *) ssh txjp "docker exec ${s} sqlite3 /app/data/*.db 'PRAGMA wal_checkpoint(TRUNCATE);' 2>/dev/null || true" & ;; - esac -done; wait - -log "导出数据库..." -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 - -# ============================================================ -# 2. Gitea 代码仓库 + 配置 -# ============================================================ -log "备份 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" - -# ============================================================ -# 3. 用户上传文件 -# ============================================================ -log "备份用户上传文件..." -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" - -# ============================================================ -# 4. Nginx 配置 -# ============================================================ -log "备份 Nginx 配置..." -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" - -# ============================================================ -# 5. 各站点 docker-compose.yml + .env -# ============================================================ -log "备份站点配置文件..." -ssh txjp "mkdir -p /tmp/full-backup/configs && \ - for site in assets-ai issue-ai oa-ai ldap-ai gitea-ai; do \ - mkdir -p /tmp/full-backup/configs/\$site && \ - cp /root/docker/\$site/docker-compose.yml /tmp/full-backup/configs/\$site/ 2>/dev/null || true && \ - cp /root/docker/\$site/.env /tmp/full-backup/configs/\$site/ 2>/dev/null || true && \ - cp /root/docker/\$site/Dockerfile /tmp/full-backup/configs/\$site/ 2>/dev/null || true; \ - done" - -# ============================================================ -# 6. SSL 证书 -# ============================================================ -log "备份 SSL 证书..." -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" - -# ============================================================ -# 7. 打包压缩 -# ============================================================ -log "打包压缩..." -ssh txjp "cd /tmp && tar czf ${BACKUP_NAME}.tar.gz full-backup/ && rm -rf /tmp/full-backup" - -# ============================================================ -# 8. 复制到本地 -# ============================================================ -log "复制到本地..." -mkdir -p "${LOCAL_DIR}" -scp "txjp:/tmp/${BACKUP_NAME}.tar.gz" "${LOCAL_DIR}/" - -# ============================================================ -# 9. 云端保留一份 -# ============================================================ -CLOUD_DIR="/root/docker/db-backups" -ssh txjp "mkdir -p ${CLOUD_DIR} && cp /tmp/${BACKUP_NAME}.tar.gz ${CLOUD_DIR}/" -log "云端备份已保存至: ${CLOUD_DIR}" - -# ============================================================ -# 10. 清理云端临时文件 -# ============================================================ -ssh txjp "rm -f /tmp/${BACKUP_NAME}.tar.gz" - -# ============================================================ -# 11. 验证备份 -# ============================================================ -log "验证备份完整性..." -cd "${LOCAL_DIR}" -if tar tzf "${BACKUP_NAME}.tar.gz" > /dev/null 2>&1; then - log "备份文件验证通过" -else - echo "[错误] 备份文件损坏" - exit 1 -fi - -# 显示备份内容 -log "备份内容清单:" -tar tzf "${BACKUP_NAME}.tar.gz" | head -30 - -echo "" -log "备份完成:${TIMESTAMP}" -echo " 文件: ${LOCAL_DIR}/${BACKUP_NAME}.tar.gz" -echo " 大小: $(du -h ${LOCAL_DIR}/${BACKUP_NAME}.tar.gz | cut -f1)" -echo "" -echo " 云端: ${CLOUD_DIR}/${BACKUP_NAME}.tar.gz" -ssh txjp "ls -lh ${CLOUD_DIR}/${BACKUP_NAME}.tar.gz 2>/dev/null" diff --git a/backup/backup-sites.sh b/backup/backup-sites.sh deleted file mode 100755 index 24be88b..0000000 --- a/backup/backup-sites.sh +++ /dev/null @@ -1,209 +0,0 @@ -#!/bin/bash -# 站点独立备份脚本 -# 每个站点单独一个备份文件,共享资源(nginx/ssl/network)单独备份 -# 用法:bash backup-sites.sh [站点名] -# 无参数:备份所有站点 -# 指定站点:仅备份指定站点 - -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"; } - -# 自动发现含数据库的站点(按目录约定:*-ai/docker-compose.yml 含 .db 或 DATABASE_PATH) -PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -ALL_SITES="" -for dir in "$PROJECT_ROOT"/*-ai/; do - site=$(basename "$dir") - [[ "$site" == "nginx-proxy-ai" ]] && continue - [ -f "$dir/docker-compose.yml" ] || continue - grep -qE "\.db|DATABASE_PATH|sqlite" "$dir/docker-compose.yml" 2>/dev/null || continue - ALL_SITES="$ALL_SITES $site" -done - -# ============================================================ -# 备份单个站点 -# ============================================================ -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) - # WAL checkpoint - 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) - # WAL checkpoint - 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) - # WAL checkpoint - 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) - # 数据库(gitea 不允许以 root 运行 dump,直接复制数据库文件) - 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" - # docker-compose - 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" - # OA 没有独立数据库,数据存在 assets/issue 中 - ;; - 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" - ;; - *) - # 未知站点:仅备份配置文件(保守策略,确保新增站点不会因缺 case 而崩溃) - 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" - ssh txjp "cp /root/docker/${site}/Dockerfile /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}" -} - -# ============================================================ -# 备份共享资源(nginx + ssl + docker网络) -# ============================================================ -backup_shared() { - local backup_name="shared-${TIMESTAMP}.tar.gz" - - log "备份共享资源..." - - ssh txjp "rm -rf /tmp/backup-shared && mkdir -p /tmp/backup-shared" - - # Nginx 配置 - 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" - - # SSL 证书 - 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" - - # Docker 网络配置 - ssh txjp "cat > /tmp/backup-shared/docker-network.sh << 'NETEOF' -#!/bin/bash -# 恢复 docker 网络 -if ! docker network inspect webnet >/dev/null 2>&1; then - docker network create --driver bridge --subnet 172.18.0.0/16 webnet - echo 'Docker 网络 webnet 已创建' -else - echo 'Docker 网络 webnet 已存在' -fi -NETEOF" - - # Nginx docker-compose(用于恢复 nginx 容器) - 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}" -} - -# ============================================================ -# 主流程 -# ============================================================ -log "开始站点独立备份..." - -# 解析参数 -TARGET_SITES="${1:-all}" - -if [[ "$TARGET_SITES" == "all" ]]; then - # 备份所有站点 - for site in $ALL_SITES; do - backup_site "$site" - done - backup_shared -else - # 备份指定站点 - for site in $TARGET_SITES; do - if echo "$ALL_SITES" | grep -qw "$site"; then - backup_site "$site" - else - warn "未知站点: $site" - fi - done -fi - -# ============================================================ -# 显示备份结果 -# ============================================================ -echo "" -log "备份完成!" -echo "" -echo "本地备份文件:" -ls -lh ${LOCAL_DIR}/*-${TIMESTAMP}.tar.gz 2>/dev/null -echo "" -echo "云端备份文件:" -ssh txjp "ls -lh ${CLOUD_DIR}/*-${TIMESTAMP}.tar.gz 2>/dev/null" diff --git a/backup/backup.sh b/backup/backup.sh index 3532204..f2abb7f 100644 --- a/backup/backup.sh +++ b/backup/backup.sh @@ -22,10 +22,19 @@ 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="${s%-ai}" # assets-ai → assets [[ "$s" == "oa" ]] && continue DB_SITES="$DB_SITES $s" done +# 站点 → 容器名映射 +container_for() { + case "$1" in + ldap) echo "lldap" ;; + *) echo "${1}-ai" ;; + esac +} + # === 模式 1:站点完整备份(默认)=== backup_site() { local site="$1" @@ -107,25 +116,23 @@ backup_shared() { 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" & + c=$(container_for "$s") + if [[ "$s" == "ldap" ]]; then + ssh txjp "docker exec ${c} sqlite3 /data/users.db '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" & + ssh txjp "docker exec ${c} sqlite3 /app/data/${s}.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 + c=$(container_for "$s") + dbpath="/app/data/${s}.db" + [[ "$s" == "ldap" ]] && dbpath="/data/users.db" + [[ "$s" == "gitea" ]] && dbpath="/data/gitea.db" log "备份 ${s}..." - ssh txjp "docker cp ${cname}:${dbpath} /tmp/db-backup/${s}-${TIMESTAMP}.db" + ssh txjp "docker cp ${c}:${dbpath} /tmp/db-backup/${s}-${TIMESTAMP}.db" done mkdir -p "${LOCAL_DIR}" @@ -146,19 +153,18 @@ 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 + c=$(container_for "$s") + [[ "$s" == "ldap" ]] && ssh txjp "docker exec ${c} sqlite3 /data/users.db 'PRAGMA wal_checkpoint(TRUNCATE);'" 2>/dev/null || true + [[ "$s" != "ldap" ]] && ssh txjp "docker exec ${c} sqlite3 /app/data/${s}.db 'PRAGMA wal_checkpoint(TRUNCATE);' 2>/dev/null || true" & 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 + c=$(container_for "$s") + dbpath="/app/data/${s}.db" + [[ "$s" == "ldap" ]] && dbpath="/data/users.db" + [[ "$s" == "gitea" ]] && dbpath="/data/gitea.db" + ssh txjp "docker cp ${c}:${dbpath} /tmp/full-backup/databases/${s}.db" done # Gitea 代码仓库 diff --git a/backup/restore-db.sh b/backup/restore-db.sh deleted file mode 100755 index ddbc1f0..0000000 --- a/backup/restore-db.sh +++ /dev/null @@ -1,296 +0,0 @@ -#!/bin/bash -# ============================================ -# 数据库恢复脚本 -# -# 用法: -# 恢复本地数据库: bash scripts/restore-db.sh local <服务名> <备份文件名> -# 恢复云端数据库: bash scripts/restore-db.sh cloud <服务名> <备份文件名> -# -# 服务名: assets / issue / ldap / gitea / monitor -# -# 示例: -# bash scripts/restore-db.sh local issue issue-2026-04-29_1753.db -# bash scripts/restore-db.sh cloud assets assets-2026-04-29_1753.db -# bash scripts/restore-db.sh cloud monitor monitor-2026-07-08_1200.db -# ============================================ - -set -e - -# ---- 参数检查 ---- -if [ $# -lt 3 ]; then - echo "用法: $0 <备份文件名>" - echo "" - echo "支持格式: .db 或 .tar.gz(自动解压)" - echo "" - echo "示例:" - echo " $0 local issue issue-2026-04-29_1753.db" - echo " $0 local issue issue-20260622_101858.tar.gz" - echo " $0 cloud ldap ldap-2026-05-15_1200.db" - echo " $0 cloud assets assets-2026-04-29_1753.db" - echo "" - echo "可用备份文件:" - echo " 本地:" - ls -1 /Users/niuniu/programs/docker/db-backups/ 2>/dev/null || echo " (无)" - echo " 云端:" - ssh txjp "ls -1 /root/docker/db-backups/" 2>/dev/null || echo " (无)" - exit 1 -fi - -TARGET=$1 -SERVICE=$2 -BACKUP_FILE=$3 - -# ---- 路径定义 ---- -LOCAL_BACKUP_DIR="/Users/niuniu/programs/docker/db-backups" -CLOUD_BACKUP_DIR="/root/docker/db-backups" - -case $SERVICE in - issue) - PROJECT_DIR="issue-ai" - CONTAINER="issue-ai" - DB_PATH_IN_CONTAINER="/app/data/issue.db" - LOCAL_DB_PATH="/Users/niuniu/programs/docker/issue-ai/data/issue.db" - ;; - assets) - PROJECT_DIR="assets-ai" - CONTAINER="assets-ai" - DB_PATH_IN_CONTAINER="/app/data/assets.db" - LOCAL_DB_PATH="/Users/niuniu/programs/docker/assets-ai/data/assets.db" - ;; - ldap) - PROJECT_DIR="ldap-ai" - CONTAINER="lldap" - DB_PATH_IN_CONTAINER="/data/users.db" - LOCAL_DB_PATH="/Users/niuniu/programs/docker/ldap-ai/data/lldap/users.db" - ;; - gitea) - PROJECT_DIR="gitea-ai" - CONTAINER="gitea-ai" - DB_PATH_IN_CONTAINER="/data/gitea.db" - LOCAL_DB_PATH="/Users/niuniu/programs/docker/gitea-ai/data/gitea/gitea.db" - ;; - monitor) - PROJECT_DIR="monitor-ai" - CONTAINER="monitor-ai" - DB_PATH_IN_CONTAINER="/app/data/monitor.db" - LOCAL_DB_PATH="/Users/niuniu/programs/docker/monitor-ai/data/monitor.db" - ;; - *) - echo "错误: 服务名必须是 assets、issue、ldap、gitea 或 monitor" - exit 1 - ;; -esac - -# ============================================ -# 本地恢复 -# ============================================ -restore_local() { - local BACKUP_PATH="${LOCAL_BACKUP_DIR}/${BACKUP_FILE}" - local DB_FILE="$BACKUP_PATH" - - echo "========================================" - echo " 本地数据库恢复" - echo "========================================" - echo " 服务: ${SERVICE} (${CONTAINER})" - echo " 备份文件: ${BACKUP_PATH}" - echo " 目标数据库: ${LOCAL_DB_PATH}" - echo "========================================" - echo "" - - # 1. 检查备份文件 - if [ ! -f "$BACKUP_PATH" ]; then - echo "[错误] 备份文件不存在: ${BACKUP_PATH}" - exit 1 - fi - - # 2. 如果是 .tar.gz 文件,解压获取 .db 文件 - if [[ "$BACKUP_PATH" == *.tar.gz ]]; then - echo "[信息] 检测到 tar.gz 压缩包,正在解压..." - TEMP_DIR=$(mktemp -d) - tar xzf "$BACKUP_PATH" -C "$TEMP_DIR" - - # 查找 .db 文件 - DB_FILE=$(find "$TEMP_DIR" -name "*.db" -type f | head -1) - if [ -z "$DB_FILE" ]; then - echo "[错误] 压缩包中未找到 .db 文件" - rm -rf "$TEMP_DIR" - exit 1 - fi - echo "[信息] 解压得到数据库文件: $(basename $DB_FILE)" - fi - - # 3. 验证备份文件是有效的 SQLite 数据库 - if ! sqlite3 "$DB_FILE" "SELECT count(*) FROM sqlite_master" > /dev/null 2>&1; then - echo "[错误] 备份文件不是有效的 SQLite 数据库" - [ -n "$TEMP_DIR" ] && rm -rf "$TEMP_DIR" - exit 1 - fi - - # 4. 确认操作 - echo "[警告] 这将覆盖本地 ${CONTAINER} 数据库!" - echo " 当前数据库: ${LOCAL_DB_PATH}" - echo " 将替换为: ${DB_FILE}" - echo "" - read -p "确认恢复? (输入 yes 继续): " CONFIRM - if [ "$CONFIRM" != "yes" ]; then - echo "已取消" - [ -n "$TEMP_DIR" ] && rm -rf "$TEMP_DIR" - exit 0 - fi - - # 5. 备份当前数据库(安全起见) - local SAFE_BACKUP="${LOCAL_DB_PATH}.before-restore-$(date +%Y%m%d_%H%M%S)" - if [ -f "$LOCAL_DB_PATH" ]; then - cp "$LOCAL_DB_PATH" "$SAFE_BACKUP" - echo "[备份] 当前数据库已备份至: ${SAFE_BACKUP}" - fi - - # 6. WAL checkpoint on backup (确保数据完整) - sqlite3 "$DB_FILE" "PRAGMA wal_checkpoint(TRUNCATE)" 2>/dev/null || true - - # 7. 停止本地容器(如果在运行) - if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then - echo "[操作] 停止容器 ${CONTAINER}..." - docker stop "$CONTAINER" - fi - - # 8. 替换数据库 - cp "$DB_FILE" "$LOCAL_DB_PATH" - rm -f "${LOCAL_DB_PATH}-shm" "${LOCAL_DB_PATH}-wal" - echo "[完成] 数据库已替换" - - # 9. 清理临时文件 - [ -n "$TEMP_DIR" ] && rm -rf "$TEMP_DIR" - - # 10. 启动容器 - echo "[操作] 启动容器 ${CONTAINER}..." - docker start "$CONTAINER" - - # 11. 验证 - sleep 2 - echo "" - echo "[验证] 恢复后数据:" - if [ "$SERVICE" = "issue" ]; then - echo " 工单数: $(sqlite3 "$LOCAL_DB_PATH" "SELECT COUNT(*) FROM tickets")" - echo " 配件名称: $(sqlite3 "$LOCAL_DB_PATH" "SELECT COUNT(*) FROM tickets WHERE parts_name IS NOT NULL AND parts_name != ''") 条" - elif [ "$SERVICE" = "ldap" ]; then - echo " 用户数: $(sqlite3 "$LOCAL_DB_PATH" "SELECT COUNT(*) FROM users")" - else - echo " 设备数: $(sqlite3 "$LOCAL_DB_PATH" "SELECT COUNT(*) FROM assets")" - fi - echo "" - echo "恢复完成!" -} - -# ============================================ -# 云端恢复 -# ============================================ -restore_cloud() { - local BACKUP_PATH="${CLOUD_BACKUP_DIR}/${BACKUP_FILE}" - - echo "========================================" - echo " 云端数据库恢复 (txjp)" - echo "========================================" - echo " 服务: ${SERVICE} (${CONTAINER})" - echo " 备份文件: ${BACKUP_PATH}" - echo "========================================" - echo "" - - # 1. 检查备份文件 - if ! ssh txjp "test -f ${BACKUP_PATH}"; then - echo "[错误] 云端备份文件不存在: ${BACKUP_PATH}" - exit 1 - fi - - # 2. 验证备份文件(通过容器内 better-sqlite3,避免依赖宿主机或容器内 sqlite3 CLI) - if ! ssh txjp "docker cp ${BACKUP_PATH} ${CONTAINER}:/tmp/restore-check.db 2>/dev/null && \ - docker exec ${CONTAINER} node -e \"const D=require('better-sqlite3');const db=new D('/tmp/restore-check.db',{readonly:true});console.log(db.prepare('SELECT count(*) as c FROM sqlite_master').get().c);db.close();\" > /dev/null 2>&1 && \ - docker exec ${CONTAINER} rm /tmp/restore-check.db"; then - echo "[错误] 备份文件不是有效的 SQLite 数据库" - exit 1 - fi - - # 3. 显示备份和当前状态 - echo "[信息] 备份文件信息:" - ssh txjp "ls -lh ${BACKUP_PATH}" - echo "" - echo "[信息] 当前云端数据库状态:" - if [ "$SERVICE" = "issue" ]; then - ssh txjp "docker exec ${CONTAINER} node -e \" - const D=require('better-sqlite3'); - const db=new D('/app/data/issue.db',{readonly:true}); - console.log(' 工单数:', db.prepare('SELECT COUNT(*) as c FROM tickets').get().c); - db.close(); - \"" - else - ssh txjp "docker exec ${CONTAINER} node -e \" - const D=require('better-sqlite3'); - const db=new D('/app/data/assets.db',{readonly:true}); - console.log(' 设备数:', db.prepare('SELECT COUNT(*) as c FROM assets').get().c); - db.close(); - \"" - fi - - # 4. 确认 - echo "" - echo "[警告] 这将覆盖云端 ${CONTAINER} 数据库!" - read -p "确认恢复? (输入 yes 继续): " CONFIRM - if [ "$CONFIRM" != "yes" ]; then - echo "已取消" - exit 0 - fi - - # 5. 停止容器(确保数据库不被写入,安全替换) - echo "[操作] 停止容器 ${CONTAINER}..." - ssh txjp "cd /root/docker/${PROJECT_DIR} && docker compose stop ${CONTAINER}" - - # 6. 备份当前数据库(容器已停止,安全复制) - local SAFE_NAME="before-restore-$(date +%Y%m%d_%H%M%S).db" - echo "[操作] 备份云端当前数据库..." - ssh txjp "docker cp ${CONTAINER}:${DB_PATH_IN_CONTAINER} /root/docker/db-backups/${SAFE_NAME}" - echo "[备份] 当前数据库已保存至云端: /root/docker/db-backups/${SAFE_NAME}" - - # 7. 替换数据库 - echo "[操作] 复制备份文件到容器..." - ssh txjp "docker cp ${BACKUP_PATH} ${CONTAINER}:${DB_PATH_IN_CONTAINER}" - - # 8. 删除残留 WAL/SHM 文件,防止旧日志与新数据库不匹配导致损坏 - echo "[操作] 清理残留 WAL/SHM 文件..." - ssh txjp "docker exec ${CONTAINER} rm -f ${DB_PATH_IN_CONTAINER}-shm ${DB_PATH_IN_CONTAINER}-wal" 2>/dev/null || true - - # 9. 启动容器 - echo "[操作] 启动容器 ${CONTAINER}..." - ssh txjp "cd /root/docker/${PROJECT_DIR} && docker compose start ${CONTAINER}" - - # 10. 验证 - sleep 3 - echo "" - echo "[验证] 恢复后数据:" - if [ "$SERVICE" = "issue" ]; then - ssh txjp "docker exec ${CONTAINER} node -e \" - const D=require('better-sqlite3'); - const db=new D('/app/data/issue.db',{readonly:true}); - console.log(' 工单数:', db.prepare('SELECT COUNT(*) as c FROM tickets').get().c); - db.close(); - \"" - else - ssh txjp "docker exec ${CONTAINER} node -e \" - const D=require('better-sqlite3'); - const db=new D('/app/data/assets.db',{readonly:true}); - console.log(' 设备数:', db.prepare('SELECT COUNT(*) as c FROM assets').get().c); - db.close(); - \"" - fi - echo "" - echo "恢复完成!" -} - -# ---- 执行 ---- -case $TARGET in - local) restore_local ;; - cloud) restore_cloud ;; - *) - echo "错误: 目标必须是 local 或 cloud" - exit 1 - ;; -esac diff --git a/backup/restore-full.sh b/backup/restore-full.sh deleted file mode 100755 index 4ba01da..0000000 --- a/backup/restore-full.sh +++ /dev/null @@ -1,289 +0,0 @@ -#!/bin/bash -# 灾难恢复脚本 - 从完整备份恢复站点 -# 用法:bash restore-full.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 - BACKUP_FILE="$1" -else - echo "" - echo "==========================================" - echo " 可用的完整备份文件" - echo "==========================================" - echo "" - - # 列出本地备份 - if [[ -d "$BACKUP_DIR" ]]; then - local_files=$(ls -t ${BACKUP_DIR}/full-backup-*.tar.gz 2>/dev/null | head -10) - if [[ -n "$local_files" ]]; then - echo "本地备份:" - index=1 - while IFS= read -r file; do - filename=$(basename "$file") - filesize=$(du -h "$file" | cut -f1) - echo " $index) $filename ($filesize)" - files[$index]="$file" - ((index++)) - done <<< "$local_files" - echo "" - fi - fi - - # 列出云端备份 - echo "云端备份:" - cloud_files=$(ssh txjp "ls -t /root/docker/db-backups/full-backup-*.tar.gz 2>/dev/null | head -10" 2>/dev/null || true) - if [[ -n "$cloud_files" ]]; then - while IFS= read -r file; do - filename=$(basename "$file") - echo " $index) [云端] $filename" - files[$index]="cloud:$file" - ((index++)) - done <<< "$cloud_files" - echo "" - fi - - if [[ ${#files[@]} -eq 0 ]]; then - error "未找到任何备份文件" - fi - - printf "请选择备份文件 (1-$((index-1))): " - read choice - - if [[ -z "${files[$choice]}" ]]; then - error "无效选择" - fi - - BACKUP_FILE="${files[$choice]}" -fi - -# ============================================================ -# 2. 下载云端备份到临时目录 -# ============================================================ -RESTORE_DIR="/tmp/restore-$$" -mkdir -p "$RESTORE_DIR" - -if [[ "$BACKUP_FILE" == cloud:* ]]; then - cloud_path="${BACKUP_FILE#cloud:}" - log "从云端下载备份..." - scp "txjp:$cloud_path" "$RESTORE_DIR/" - BACKUP_FILE="$RESTORE_DIR/$(basename $cloud_path)" -fi - -# ============================================================ -# 3. 解压备份 -# ============================================================ -log "解压备份文件..." -cd "$RESTORE_DIR" -tar xzf "$BACKUP_FILE" - -# 检测备份内容 -BACKUP_CONTENT="$RESTORE_DIR/full-backup" -if [[ ! -d "$BACKUP_CONTENT" ]]; then - error "备份文件格式错误" -fi - -log "备份内容:" -ls -la "$BACKUP_CONTENT/" - -# ============================================================ -# 4. 选择要恢复的站点 -# ============================================================ -echo "" -echo "==========================================" -echo " 选择要恢复的内容" -echo "==========================================" -echo "" -echo " 1) assets - 资产管理系统" -echo " 2) issue - 工单系统" -echo " 3) ldap - 用户目录服务" -echo " 4) gitea - 代码仓库" -echo " 5) monitor - 告警监控中心" -echo " 6) nginx - 反向代理配置" -echo " 7) 全部恢复" -echo "" -printf "请选择 (1-7): " -read site_choice - -# ============================================================ -# 5. 执行恢复 -# ============================================================ -restore_assets() { - log "恢复 assets 站点..." - - # 恢复数据库 - if [[ -f "$BACKUP_CONTENT/databases/assets.db" ]]; then - log "恢复 assets 数据库..." - scp "$BACKUP_CONTENT/databases/assets.db" txjp:/tmp/assets.db - ssh txjp "docker cp /tmp/assets.db assets-ai:/app/data/assets.db && rm /tmp/assets.db" - ssh txjp "docker restart assets-ai" - log "assets 数据库恢复完成" - fi - - # 恢复配置 - if [[ -d "$BACKUP_CONTENT/configs/assets-ai" ]]; then - log "恢复 assets 配置..." - ssh txjp "mkdir -p $REMOTE_BASE/assets-ai" - scp "$BACKUP_CONTENT/configs/assets-ai/"* txjp:$REMOTE_BASE/assets-ai/ 2>/dev/null || true - fi - - # 恢复上传文件 - if [[ -d "$BACKUP_CONTENT/uploads/assets-uploads" ]]; then - log "恢复 assets 上传文件..." - ssh txjp "mkdir -p $REMOTE_BASE/assets-ai/data" - scp -r "$BACKUP_CONTENT/uploads/assets-uploads/"* txjp:$REMOTE_BASE/assets-ai/data/uploads/ 2>/dev/null || true - fi -} - -restore_issue() { - log "恢复 issue 站点..." - - # 恢复数据库 - if [[ -f "$BACKUP_CONTENT/databases/issue.db" ]]; then - log "恢复 issue 数据库..." - scp "$BACKUP_CONTENT/databases/issue.db" txjp:/tmp/issue.db - ssh txjp "docker cp /tmp/issue.db issue-ai:/app/data/issue.db && rm /tmp/issue.db" - ssh txjp "docker restart issue-ai" - log "issue 数据库恢复完成" - fi - - # 恢复配置 - if [[ -d "$BACKUP_CONTENT/configs/issue-ai" ]]; then - log "恢复 issue 配置..." - ssh txjp "mkdir -p $REMOTE_BASE/issue-ai" - scp "$BACKUP_CONTENT/configs/issue-ai/"* txjp:$REMOTE_BASE/issue-ai/ 2>/dev/null || true - fi - - # 恢复上传文件 - if [[ -d "$BACKUP_CONTENT/uploads/issue-uploads" ]]; then - log "恢复 issue 上传文件..." - ssh txjp "mkdir -p $REMOTE_BASE/issue-ai/data" - scp -r "$BACKUP_CONTENT/uploads/issue-uploads/"* txjp:$REMOTE_BASE/issue-ai/data/uploads/ 2>/dev/null || true - fi - - # 恢复报告文件 - if [[ -d "$BACKUP_CONTENT/uploads/issue-reports" ]]; then - log "恢复 issue 报告文件..." - ssh txjp "mkdir -p $REMOTE_BASE/issue-ai/data" - scp -r "$BACKUP_CONTENT/uploads/issue-reports/"* txjp:$REMOTE_BASE/issue-ai/data/reports/ 2>/dev/null || true - fi -} - -restore_ldap() { - log "恢复 ldap 站点..." - - # 恢复数据库 - if [[ -f "$BACKUP_CONTENT/databases/ldap.db" ]]; then - log "恢复 ldap 数据库..." - scp "$BACKUP_CONTENT/databases/ldap.db" txjp:/tmp/ldap.db - ssh txjp "docker cp /tmp/ldap.db lldap:/data/users.db && rm /tmp/ldap.db" - ssh txjp "docker restart lldap" - log "ldap 数据库恢复完成" - fi - - # 恢复配置 - if [[ -d "$BACKUP_CONTENT/configs/ldap-ai" ]]; then - log "恢复 ldap 配置..." - ssh txjp "mkdir -p $REMOTE_BASE/ldap-ai" - scp "$BACKUP_CONTENT/configs/ldap-ai/"* txjp:$REMOTE_BASE/ldap-ai/ 2>/dev/null || true - fi -} - -restore_gitea() { - log "恢复 gitea 站点..." - - # 恢复数据库 - if [[ -f "$BACKUP_CONTENT/databases/gitea.db" ]]; then - log "恢复 gitea 数据库..." - scp "$BACKUP_CONTENT/databases/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 "$BACKUP_CONTENT/gitea/git" ]]; then - log "恢复 gitea 代码仓库..." - ssh txjp "mkdir -p $REMOTE_BASE/gitea-ai/data" - scp -r "$BACKUP_CONTENT/gitea/git" txjp:$REMOTE_BASE/gitea-ai/data/ - fi - - # 恢复配置 - if [[ -d "$BACKUP_CONTENT/gitea/conf" ]]; then - log "恢复 gitea 配置..." - ssh txjp "mkdir -p $REMOTE_BASE/gitea-ai/data/gitea" - scp -r "$BACKUP_CONTENT/gitea/conf" txjp:$REMOTE_BASE/gitea-ai/data/gitea/ - fi - - # 恢复 docker-compose - if [[ -d "$BACKUP_CONTENT/configs/gitea-ai" ]]; then - log "恢复 gitea docker-compose..." - scp "$BACKUP_CONTENT/configs/gitea-ai/"* txjp:$REMOTE_BASE/gitea-ai/ 2>/dev/null || true - fi - - # 重启容器 - ssh txjp "cd $REMOTE_BASE/gitea-ai && docker compose down && docker compose up -d" - log "gitea 恢复完成" -} - -restore_nginx() { - log "恢复 nginx 配置..." - - if [[ -d "$BACKUP_CONTENT/nginx" ]]; then - ssh txjp "mkdir -p $REMOTE_BASE/nginx-proxy-ai/conf.d" - scp "$BACKUP_CONTENT/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 "nginx 配置恢复完成" - fi -} - -restore_monitor() { - log "恢复 monitor 站点..." - - if [[ -f "$BACKUP_CONTENT/databases/monitor.db" ]]; then - ssh txjp "docker cp $BACKUP_CONTENT/databases/monitor.db monitor-ai:/app/data/monitor.db" - log "monitor 数据库恢复完成" - else - warn "未找到 monitor 数据库备份" - fi - - ssh txjp "cd $REMOTE_BASE/monitor-ai && docker compose down && docker compose up -d --build" - log "monitor 恢复完成" -} - -# 执行恢复 -case "$site_choice" in - 1) restore_assets ;; - 2) restore_issue ;; - 3) restore_ldap ;; - 4) restore_gitea ;; - 5) restore_monitor ;; - 6) restore_nginx ;; - 7) - restore_assets - restore_issue - restore_ldap - restore_gitea - restore_monitor - restore_nginx - ;; - *) error "无效选择" ;; -esac - -# ============================================================ -# 6. 清理临时文件 -# ============================================================ -log "清理临时文件..." -rm -rf "$RESTORE_DIR" - -echo "" -log "恢复完成!" diff --git a/backup/restore-sites.sh b/backup/restore-sites.sh deleted file mode 100755 index 86a34e2..0000000 --- a/backup/restore-sites.sh +++ /dev/null @@ -1,412 +0,0 @@ -#!/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)'" diff --git a/backup/restore.sh b/backup/restore.sh index 529f0f6..a219d63 100644 --- a/backup/restore.sh +++ b/backup/restore.sh @@ -22,6 +22,7 @@ ALL_SITES="shared" for dir in "$PROJECT_ROOT"/*-ai/; do s=$(basename "$dir"); [[ "$s" == "nginx-proxy-ai" ]] && continue [ -f "$dir/docker-compose.yml" ] || continue + s="${s%-ai}" # assets-ai → assets ALL_SITES="$ALL_SITES $s" done