website-scripts/backup/backup.sh

206 lines
10 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
# 统一备份脚本 — 支持远程模式(从本地 Mac 执行)和本地模式(在服务器上直接执行)
# 用法bash backup.sh [--local] [--db-only|--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"
MODE="remote"
log() { echo "[$(date '+%H:%M:%S')] $1"; }
warn() { echo "[$(date '+%H:%M:%S')] [警告] $1"; }
# 参数解析(提取 --local / --db-only / --full其余为站点名
ARGS=(); ACTION="default"
for arg in "$@"; do
case "$arg" in
--local) MODE="local" ;;
--db-only|--full) ACTION="${arg#--}" ;;
--help|-h) ACTION="help" ;;
*) ARGS+=("$arg") ;;
esac
done
# 远程/本地适配
runcmd() { [[ "$MODE" == "local" ]] && bash -c "$*" || ssh txjp "$*"; }
fetch() { [[ "$MODE" == "local" ]] && cp "$1" "$2" || scp "txjp:$1" "$2"; }
# === 自动发现含数据库的站点 ===
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}"; [[ "$s" == "oa" ]] && continue
DB_SITES="$DB_SITES $s"
done
container_for() { [[ "$1" == "ldap" ]] && echo "lldap" || echo "${1}-ai"; }
# === 模式 1站点完整备份默认===
backup_site() {
local site="$1" backup_name="${site}-${TIMESTAMP}.tar.gz"
log "备份 ${site}..."
runcmd "rm -rf /tmp/backup-${site} && mkdir -p /tmp/backup-${site}/data"
case "$site" in
assets)
runcmd "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
runcmd "docker cp assets-ai:/app/data/assets.db /tmp/backup-${site}/data/"
runcmd "cp -a /root/docker/assets-ai/data/uploads /tmp/backup-${site}/data/ 2>/dev/null || true"
runcmd "cp /root/docker/assets-ai/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true"
runcmd "cp /root/docker/assets-ai/.env /tmp/backup-${site}/ 2>/dev/null || true"
runcmd "cp /root/docker/assets-ai/Dockerfile /tmp/backup-${site}/ 2>/dev/null || true"
;;
issue)
runcmd "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
runcmd "docker cp issue-ai:/app/data/issue.db /tmp/backup-${site}/data/"
runcmd "cp -a /root/docker/issue-ai/data/uploads /tmp/backup-${site}/data/ 2>/dev/null || true"
runcmd "cp -a /root/docker/issue-ai/data/reports /tmp/backup-${site}/data/ 2>/dev/null || true"
runcmd "cp /root/docker/issue-ai/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true"
runcmd "cp /root/docker/issue-ai/.env /tmp/backup-${site}/ 2>/dev/null || true"
runcmd "cp /root/docker/issue-ai/Dockerfile /tmp/backup-${site}/ 2>/dev/null || true"
;;
ldap)
runcmd "docker exec lldap sqlite3 /data/users.db 'PRAGMA wal_checkpoint(TRUNCATE);'" 2>/dev/null || true
runcmd "docker cp lldap:/data/users.db /tmp/backup-${site}/data/"
runcmd "cp /root/docker/ldap-ai/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true"
runcmd "cp /root/docker/ldap-ai/.env /tmp/backup-${site}/ 2>/dev/null || true"
runcmd "cp /root/docker/ldap-ai/Dockerfile /tmp/backup-${site}/ 2>/dev/null || true"
;;
gitea)
runcmd "docker cp gitea-ai:/data/gitea.db /tmp/backup-${site}/data/"
runcmd "cp -a /root/docker/gitea-ai/data/git /tmp/backup-${site}/ 2>/dev/null || true"
runcmd "cp -a /root/docker/gitea-ai/data/gitea/conf /tmp/backup-${site}/gitea-conf 2>/dev/null || true"
runcmd "cp /root/docker/gitea-ai/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true"
;;
oa)
runcmd "cp /root/docker/oa-ai/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true"
runcmd "cp /root/docker/oa-ai/.env /tmp/backup-${site}/ 2>/dev/null || true"
;;
monitor)
runcmd "docker exec monitor-ai sqlite3 /app/data/monitor.db 'PRAGMA wal_checkpoint(TRUNCATE);'" 2>/dev/null || true
runcmd "docker cp monitor-ai:/app/data/monitor.db /tmp/backup-${site}/data/"
runcmd "cp /root/docker/monitor-ai/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true"
runcmd "cp /root/docker/monitor-ai/.env /tmp/backup-${site}/ 2>/dev/null || true"
;;
*)
warn "${site}: 无专用备份逻辑,仅备份配置文件"
runcmd "cp /root/docker/${site}-ai/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true"
runcmd "cp /root/docker/${site}-ai/.env /tmp/backup-${site}/ 2>/dev/null || true"
;;
esac
runcmd "cd /tmp && tar czf ${backup_name} backup-${site}/"
mkdir -p "${LOCAL_DIR}"
fetch "/tmp/${backup_name}" "${LOCAL_DIR}/"
runcmd "mkdir -p ${CLOUD_DIR} && cp /tmp/${backup_name} ${CLOUD_DIR}/"
runcmd "rm -rf /tmp/backup-${site} /tmp/${backup_name}"
log "${site} 备份完成: ${backup_name}"
}
backup_shared() {
local backup_name="shared-${TIMESTAMP}.tar.gz"
log "备份共享资源..."
runcmd "rm -rf /tmp/backup-shared && mkdir -p /tmp/backup-shared"
runcmd "mkdir -p /tmp/backup-shared/nginx && cp -a /root/docker/nginx-proxy-ai/conf.d/* /tmp/backup-shared/nginx/ 2>/dev/null || true"
runcmd "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"
runcmd "cp /root/docker/nginx-proxy-ai/docker-compose.yml /tmp/backup-shared/ 2>/dev/null || true"
runcmd "cd /tmp && tar czf ${backup_name} backup-shared/"
mkdir -p "${LOCAL_DIR}"; fetch "/tmp/${backup_name}" "${LOCAL_DIR}/"
runcmd "mkdir -p ${CLOUD_DIR} && cp /tmp/${backup_name} ${CLOUD_DIR}/"
runcmd "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")
[[ "$s" == "ldap" ]] && runcmd "docker exec ${c} sqlite3 /data/users.db 'PRAGMA wal_checkpoint(TRUNCATE);' 2>/dev/null || true" &
[[ "$s" != "ldap" ]] && runcmd "docker exec ${c} sqlite3 /app/data/${s}.db 'PRAGMA wal_checkpoint(TRUNCATE);' 2>/dev/null || true" &
done; wait
runcmd "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}..."
runcmd "docker cp ${c}:${dbpath} /tmp/db-backup/${s}-${TIMESTAMP}.db"
done
mkdir -p "${LOCAL_DIR}"; runcmd "mkdir -p ${CLOUD_DIR}"
for s in $DB_SITES; do
runcmd "cp /tmp/db-backup/${s}-${TIMESTAMP}.db ${CLOUD_DIR}/"
fetch "/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
runcmd "rm -rf /tmp/db-backup"
log "数据库备份完成"
}
# === 模式 3灾难恢复完整备份 ===
backup_full() {
log "开始完整灾难恢复备份..."
for s in $DB_SITES; do
c=$(container_for "$s")
[[ "$s" == "ldap" ]] && runcmd "docker exec ${c} sqlite3 /data/users.db 'PRAGMA wal_checkpoint(TRUNCATE);'" 2>/dev/null || true
[[ "$s" != "ldap" ]] && runcmd "docker exec ${c} sqlite3 /app/data/${s}.db 'PRAGMA wal_checkpoint(TRUNCATE);' 2>/dev/null || true" &
done; wait
runcmd "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"
runcmd "docker cp ${c}:${dbpath} /tmp/full-backup/databases/${s}.db"
done
runcmd "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"
runcmd "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"
runcmd "mkdir -p /tmp/full-backup/nginx && cp -a /root/docker/nginx-proxy-ai/conf.d/* /tmp/full-backup/nginx/ 2>/dev/null || true"
runcmd "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}"
runcmd "cd /tmp && tar czf ${name}.tar.gz full-backup/"
mkdir -p "${LOCAL_DIR}"; fetch "/tmp/${name}.tar.gz" "${LOCAL_DIR}/"
runcmd "mkdir -p ${CLOUD_DIR} && cp /tmp/${name}.tar.gz ${CLOUD_DIR}/"
runcmd "rm -rf /tmp/full-backup /tmp/${name}.tar.gz"
log "完整备份完成: ${name}.tar.gz"
}
# === 主流程 ===
case "$ACTION" in
help)
echo "用法: bash backup.sh [--local] [--db-only|--full] [站点名...]"
echo " --local 本地模式(直接在服务器上运行)"
echo " 无参数 站点完整备份(全部含 DB 站点)"
echo " --db-only 仅数据库备份"
echo " --full 灾难恢复完整备份"
echo " 站点名... 备份指定站点"
;;
db-only) backup_db_only ;;
full) backup_full ;;
*)
TARGETS="${ARGS[*]:-all}"
log "站点完整备份: ${TARGETS} (模式: ${MODE})"
if [[ "$TARGETS" == "all" ]]; then
for site in $DB_SITES oa; 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