110 lines
4.8 KiB
Bash
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 "бэкап пригоден к восстановлению"
|