website-scripts/backup/backup.sh

213 lines
9.9 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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