159 lines
5.6 KiB
Bash
Executable File
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 "=== Готово ==="
|