#!/usr/bin/env bash # glchat: восстановление из бэкапа (AGENT.md 10.8). 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" COMPOSE_FILE="${GLCHAT_ETC_DIR}/compose.yaml" log() { printf '[restore] %s\n' "$*"; } # env_get читает значение из .env инстанса (пусто, если ключа нет). env_get() { [ -f "$ENV_FILE" ] || return 0 sed -n "s/^$1=//p" "$ENV_FILE" | tail -1 } # app_check_url печатает адрес /readyz с учётом привязки приложения: во внешнем # режиме приложение слушает WireGuard-адрес, и проверка localhost не работает. app_check_url() { local port="$1" host="${2:-}" case "$host" in 0.0.0.0 | "" | "::") host="127.0.0.1" ;; esac printf 'http://%s:%s/readyz' "$host" "$port" } die() { printf '[restore] ошибка: %s\n' "$*" >&2 exit 1 } ARCHIVE="${1:-}" [ -n "$ARCHIVE" ] || die "использование: glchat restore <файл.tar.gz[.age]>" [ -f "$ARCHIVE" ] || die "файл не найден: $ARCHIVE" [ -f "$ENV_FILE" ] || die "не найден ${ENV_FILE}" [ -f "$COMPOSE_FILE" ] || die "не найден ${COMPOSE_FILE}" # shellcheck disable=SC1090 set -a && . "$ENV_FILE" && set +a WORK_DIR="$(mktemp -d)" cleanup() { rm -rf "$WORK_DIR"; } trap cleanup EXIT 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 "расшифровываем ${ARCHIVE}" age -d -i "$KEY_FILE" -o "${WORK_DIR}/backup.tar.gz" "$ARCHIVE" || die "не удалось расшифровать бэкап" ;; *) cp "$ARCHIVE" "${WORK_DIR}/backup.tar.gz" ;; esac log "распаковываем" tar -xzf "${WORK_DIR}/backup.tar.gz" -C "$WORK_DIR" [ -f "${WORK_DIR}/glchat.db" ] || die "в архиве нет glchat.db — это не бэкап glchat" log "текущее состояние" (cd "$GLCHAT_ETC_DIR" && docker compose ps) || true log "создаём страховочный бэкап текущих данных" if [ "${GLCHAT_SKIP_PREBACKUP:-0}" != "1" ]; then GLCHAT_ETC_DIR="$GLCHAT_ETC_DIR" bash "${GLCHAT_ETC_DIR}/backup.sh" || die "страховочный бэкап не удался; восстановление остановлено" fi printf 'Восстановление заменит текущую БД и файлы. Продолжить? (yes/no): ' read -r reply [ "$reply" = "yes" ] || { log "отменено" exit 0 } log "останавливаем приложение" (cd "$GLCHAT_ETC_DIR" && docker compose stop app) || true rollback_dir="${GLCHAT_DATA_DIR}/pre-restore-$(date -u +%Y%m%dT%H%M%SZ)" mkdir -p "$rollback_dir" cp -a "${GLCHAT_DATA_DIR}/db" "$rollback_dir/" 2>/dev/null || true log "текущие данные скопированы в ${rollback_dir}" log "заменяем базу" install -m 640 "${WORK_DIR}/glchat.db" "${GLCHAT_DATA_DIR}/db/glchat.db" rm -f "${GLCHAT_DATA_DIR}/db/glchat.db-wal" "${GLCHAT_DATA_DIR}/db/glchat.db-shm" if [ -d "${WORK_DIR}/files" ]; then log "заменяем файлы" rm -rf "${GLCHAT_DATA_DIR}/files" mkdir -p "${GLCHAT_DATA_DIR}/files" cp -a "${WORK_DIR}/files/." "${GLCHAT_DATA_DIR}/files/" fi log "запускаем приложение" (cd "$GLCHAT_ETC_DIR" && docker compose start app) || (cd "$GLCHAT_ETC_DIR" && docker compose up -d app) for _ in $(seq 1 30); do if curl -fsS "$(app_check_url "${APP_PORT:-8080}" "$(env_get GLCHAT_APP_BIND)")" >/dev/null 2>&1; then log "приложение готово" GLCHAT_ETC_DIR="$GLCHAT_ETC_DIR" bash "${GLCHAT_ETC_DIR}/smoke.sh" || true log "восстановление завершено" exit 0 fi sleep 2 done die "приложение не поднялось после восстановления; откат: ${rollback_dir}"