Files
ZBrain/scripts/backup.sh
T
zuevav f4bca8449e main
2026-05-20 19:33:02 +03:00

159 lines
5.6 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# ZBrain Backup
# ==============
# Делает дамп всех Postgres БД (brainhub + все gbrain_*) и конфигов.
# Шифрует архивы через age (нужен age и публичный ключ).
#
# Запуск из cron, например:
# 0 3 * * * root /opt/zbrain/scripts/backup.sh
set -euo pipefail
# ============================================================
# Конфигурация
# ============================================================
BACKUP_DIR="${BACKUP_DIR:-/var/backups/zbrain}"
RETENTION_DAYS="${RETENTION_DAYS:-14}"
AGE_RECIPIENT_FILE="${AGE_RECIPIENT_FILE:-/etc/zbrain/backup.age.pub}"
LOG_FILE="${LOG_FILE:-/var/log/zbrain/backup.log}"
ZBRAIN_CONFIG_DIR="/etc/zbrain"
ZBRAIN_DATA_DIR="/var/lib/zbrain"
DATE=$(date +%Y%m%d-%H%M%S)
BACKUP_PATH="${BACKUP_DIR}/${DATE}"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
err() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" | tee -a "$LOG_FILE" >&2
}
# ============================================================
# Проверки
# ============================================================
[[ $EUID -eq 0 ]] || { err "Требуется root"; exit 1; }
if ! command -v age &>/dev/null; then
err "age не установлен. apt install age"
exit 1
fi
if [[ ! -f "$AGE_RECIPIENT_FILE" ]]; then
err "Файл получателя age не найден: $AGE_RECIPIENT_FILE"
err "Создай ключ: age-keygen -o /etc/zbrain/backup.age"
err "И публичную часть в $AGE_RECIPIENT_FILE"
exit 1
fi
mkdir -p "$BACKUP_PATH" "$(dirname "$LOG_FILE")"
chmod 700 "$BACKUP_DIR"
log "=== ZBrain backup: $DATE ==="
# ============================================================
# Postgres dump (каждая БД отдельно)
# ============================================================
log "Дамп Postgres БД..."
# Список всех ZBrain БД (brainhub + gbrain_*)
DATABASES=$(sudo -u postgres psql -tAc "
SELECT datname FROM pg_database
WHERE datname = 'brainhub' OR datname LIKE 'gbrain_%'
ORDER BY datname
")
for db in $DATABASES; do
log " ${db}"
dump_file="${BACKUP_PATH}/${db}.sql"
sudo -u postgres pg_dump --format=custom --compress=9 --file="${dump_file}.dump" "$db"
# Шифруем
age -R "$AGE_RECIPIENT_FILE" -o "${dump_file}.dump.age" "${dump_file}.dump"
rm "${dump_file}.dump"
log "${db}.sql.dump.age ($(du -h "${dump_file}.dump.age" | cut -f1))"
done
# pg_dumpall для глобальных объектов (roles, tablespaces)
log "Дамп глобальных Postgres объектов..."
sudo -u postgres pg_dumpall --globals-only > "${BACKUP_PATH}/_globals.sql"
age -R "$AGE_RECIPIENT_FILE" -o "${BACKUP_PATH}/_globals.sql.age" "${BACKUP_PATH}/_globals.sql"
rm "${BACKUP_PATH}/_globals.sql"
# ============================================================
# Конфиги
# ============================================================
log "Архив конфигов..."
tar czf "${BACKUP_PATH}/config.tar.gz" \
-C / \
etc/zbrain \
etc/postgresql/16/main/postgresql.conf \
etc/postgresql/16/main/conf.d \
etc/postgresql/16/main/pg_hba.conf \
etc/systemd/system/zbrain-*.service \
etc/nginx 2>/dev/null || true
age -R "$AGE_RECIPIENT_FILE" -o "${BACKUP_PATH}/config.tar.gz.age" "${BACKUP_PATH}/config.tar.gz"
rm "${BACKUP_PATH}/config.tar.gz"
log "✓ config.tar.gz.age"
# ============================================================
# gbrain data dir (config.json per-brain содержит пароли)
# ============================================================
log "Архив brain configs..."
if [[ -d "$ZBRAIN_DATA_DIR/brains" ]]; then
tar czf "${BACKUP_PATH}/brains-meta.tar.gz" \
-C "$ZBRAIN_DATA_DIR" \
--exclude='brains/*/data' \
brains/
age -R "$AGE_RECIPIENT_FILE" -o "${BACKUP_PATH}/brains-meta.tar.gz.age" "${BACKUP_PATH}/brains-meta.tar.gz"
rm "${BACKUP_PATH}/brains-meta.tar.gz"
log "✓ brains-meta.tar.gz.age"
fi
# ============================================================
# Манифест
# ============================================================
cat > "${BACKUP_PATH}/MANIFEST.txt" <<EOF
ZBrain Backup
=============
Date: $(date -Iseconds)
Hostname: $(hostname -f)
Postgres version: $(sudo -u postgres psql -tAc 'SHOW server_version')
Databases:
$(echo "$DATABASES" | sed 's/^/ - /')
Files:
$(ls -lah "$BACKUP_PATH" | tail -n +2 | awk '{print " - " $NF " (" $5 ")"}')
Восстановление:
bash /opt/zbrain/scripts/restore.sh ${DATE}
EOF
# ============================================================
# Размер итогового бэкапа
# ============================================================
TOTAL_SIZE=$(du -sh "$BACKUP_PATH" | cut -f1)
log "✓ Backup завершён: $BACKUP_PATH ($TOTAL_SIZE)"
# ============================================================
# Retention - удаляем старые бэкапы
# ============================================================
log "Чистка бэкапов старше $RETENTION_DAYS дней..."
find "$BACKUP_DIR" -maxdepth 1 -type d -name '????????-??????' -mtime +$RETENTION_DAYS -exec rm -rf {} \;
log "✓ Чистка завершена"
# ============================================================
# (опционально) push на удалённое хранилище
# ============================================================
# rsync -avz "$BACKUP_PATH" backup@remote-host:/backups/zbrain/
# или rclone copy "$BACKUP_PATH" "remote:zbrain-backups/${DATE}"
log "=== Готово ==="