Files
glchat/deploy/verify-backup.sh
grendervill 1cb949a7a4 feat(ops): проверка бэкапа и уборка файлов без записи
- glchat verify-backup: расшифровка age, integrity_check, foreign_key_check,
  схема и состав данных, сверка с манифестом; живые данные не трогает
- чистка удаляет файлы на диске, у которых не осталось записи в базе (так
  оставались вложения удалённых серверов), и пустые подкаталоги; работает
  через os.Root, чтобы символическая ссылка не вывела за каталог файлов
- удаление сервера теперь чистит и содержимое файлов, а не только записи
- install.sh пишет значения .env с пробелами в кавычках: файл подключается
  через `set -a && . .env`, иначе имя главного сервера выполнялось как команда
- тесты: Go на уборку файлов без записи (свежие не трогаются, пустые
  каталоги удаляются)
2026-09-21 23:33:04 +03:00

110 lines
4.8 KiB
Bash

#!/usr/bin/env bash
# glchat: проверка бэкапа без восстановления (AGENT.md 10.8).
#
# Расшифровывает архив, распаковывает во временный каталог, проверяет целостность
# базы (PRAGMA integrity_check), схему и состав данных, сверяет манифест.
# Живые данные не трогает: можно запускать на рабочем инстансе.
set -euo pipefail
GLCHAT_ETC_DIR="${GLCHAT_ETC_DIR:-/opt/glchat}"
GLCHAT_DATA_DIR="${GLCHAT_DATA_DIR:-${GLCHAT_ETC_DIR}/data}"
ENV_FILE="${GLCHAT_ETC_DIR}/.env"
log() { printf '[verify] %s\n' "$*"; }
die() {
printf '[verify] ошибка: %s\n' "$*" >&2
exit 1
}
ARCHIVE="${1:-}"
[ -n "$ARCHIVE" ] || die "использование: glchat verify-backup <файл.tar.gz[.age]>"
[ -f "$ARCHIVE" ] || die "файл не найден: $ARCHIVE"
if [ -f "$ENV_FILE" ]; then
# shellcheck disable=SC1090
set -a && . "$ENV_FILE" && set +a
fi
WORK_DIR="$(mktemp -d)"
cleanup() { rm -rf "$WORK_DIR"; }
trap cleanup EXIT
# 1. Расшифровка: age-архив требует приватный ключ.
case "$ARCHIVE" in
*.age)
command -v age >/dev/null 2>&1 || die "age не установлен"
KEY_FILE="${AGE_KEY_FILE:-${GLCHAT_DATA_DIR}/backups/age-key.txt}"
[ -f "$KEY_FILE" ] || die "не найден приватный age-ключ (${KEY_FILE}); укажите AGE_KEY_FILE"
log "расшифровываем $(basename "$ARCHIVE")"
age -d -i "$KEY_FILE" -o "${WORK_DIR}/backup.tar.gz" "$ARCHIVE" ||
die "не удалось расшифровать бэкап (проверьте ключ)"
;;
*) cp "$ARCHIVE" "${WORK_DIR}/backup.tar.gz" ;;
esac
# 2. Распаковка и обязательные файлы.
log "распаковываем"
tar -xzf "${WORK_DIR}/backup.tar.gz" -C "$WORK_DIR" || die "архив повреждён"
[ -f "${WORK_DIR}/glchat.db" ] || die "в архиве нет glchat.db — это не бэкап glchat"
stats() { printf '%s' "$1"; }
DB="${WORK_DIR}/glchat.db"
# 3. Целостность базы: sqlite3 есть на хосте или в контейнере приложения.
sqlite_query() {
local query="$1"
if command -v sqlite3 >/dev/null 2>&1; then
sqlite3 -readonly "$DB" "$query"
return
fi
if docker ps --format '{{.Names}}' | grep -q '^glchat-app$'; then
docker cp "$DB" glchat-app:/tmp/verify-backup.db >/dev/null
docker exec glchat-app sh -c "sqlite3 -readonly /tmp/verify-backup.db \"$query\""
docker exec glchat-app rm -f /tmp/verify-backup.db >/dev/null 2>&1 || true
return
fi
die "нет sqlite3 ни на хосте, ни в контейнере glchat-app"
}
log "проверяем целостность базы"
integrity="$(sqlite_query 'PRAGMA integrity_check;')"
[ "$integrity" = "ok" ] || die "integrity_check вернул: ${integrity}"
log "integrity_check: ok"
foreign_keys="$(sqlite_query 'PRAGMA foreign_key_check;' || true)"
[ -z "$foreign_keys" ] || die "нарушены внешние ключи: ${foreign_keys}"
log "foreign_key_check: чисто"
schema_version="$(sqlite_query 'SELECT MAX(version_id) FROM goose_db_version;' 2>/dev/null || echo '?')"
users="$(sqlite_query 'SELECT COUNT(*) FROM users WHERE deleted_at IS NULL;')"
guilds="$(sqlite_query 'SELECT COUNT(*) FROM guilds;')"
messages="$(sqlite_query 'SELECT COUNT(*) FROM messages;')"
files="$(sqlite_query 'SELECT COUNT(*) FROM files;')"
# 4. Манифест: подсказывает, с какого инстанса снят бэкап.
manifest="нет"
if [ -f "${WORK_DIR}/MANIFEST.txt" ]; then
manifest="$(tr '\n' ' ' <"${WORK_DIR}/MANIFEST.txt")"
fi
files_on_disk=0
if [ -d "${WORK_DIR}/files" ]; then
files_on_disk="$(find "${WORK_DIR}/files" -type f | wc -l | tr -d ' ')"
fi
printf '\n=== проверка бэкапа ===\n'
printf ' архив: %s\n' "$(basename "$ARCHIVE")"
printf ' схема: %s\n' "$schema_version"
printf ' пользователи: %s\n' "$users"
printf ' серверы: %s\n' "$guilds"
printf ' сообщения: %s\n' "$messages"
printf ' файлов в БД: %s (на диске: %s)\n' "$files" "$files_on_disk"
printf ' манифест: %s\n' "$manifest"
# 5. Файлы: если в базе есть записи о файлах, каталог не должен быть пустым.
if [ "${files:-0}" -gt 0 ] && [ "${files_on_disk:-0}" -eq 0 ]; then
die "в базе ${files} файлов, а в архиве нет каталога files — бэкап неполный"
fi
log "бэкап пригоден к восстановлению"