#!/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="${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" 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 "开始数据库备份..." for s in $DB_SITES; do 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 ${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 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 ${c}:${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 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 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 代码仓库 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