213 lines
9.9 KiB
Bash
213 lines
9.9 KiB
Bash
#!/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
|