Files
Deal/scripts/restore.sh
T
Rustam Khalimov 9e07568ddd Инициализировать репозиторий «Дейл»
Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы
ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ,
инструкция пользователя, техдокументация, код-стайл), бэклог,
скрипты развёртывания и архив прототипа LeadRadar.
2026-09-11 02:50:17 +03:00

184 lines
10 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# restore.sh — восстановление «Дейла» из снапшотов scripts/backup.sh (этап 7 Task 15, Ruling 8).
#
# ВАЖНО — порядок применения (БД/тома не должны быть заняты; скрипт сервисы НЕ останавливает):
# 1) остановите core и сервисы:
# dev: docker compose -f deploy/compose.dev.yml stop core telegram-service ml-service
# prod: docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml stop
# 2) bash scripts/restore.sh [all|pg|minio|data] [TS]
# TS — метка времени снапшота вида 20260908-021500 (из имени файла backup-<TS>…);
# без TS берётся самый свежий снапшот Postgres, остальные шаги — тот же TS;
# 3) поднимите сервисы обратно (docker compose … start core telegram-service ml-service).
#
# Шаги (зеркально backup.sh, те же env — см. scripts/deal-backup-lib.sh и техдок §13.9):
# pg — БД deal целиком (public + tenant_*): docker-режим — dropdb+createdb, затем pg_restore
# из $BACKUP_DIR/pg/backup-<TS>.dump (docker exec deal-postgres); при DEAL_PG_HOST —
# прямое pg_restore --clean --if-exists поверх существующей БД;
# minio — mc mirror $BACKUP_DIR/minio/backup-<TS>/ → бакет deal-files (восстановление = загрузка
# В MinIO); DEAL_MINIO_MIRROR_REMOVE=1 добавляет --remove (бакет = снимок 1:1);
# data — распаковка $BACKUP_DIR/data/backup-<TS>.tar.gz в DEAL_DATA_DIR; при DEAL_TAR_VOLUMES —
# архивы backup-<TS>.<volume>.tar.gz распаковываются в соответствующие docker volume'ы
# (busybox-контейнер).
#
# ⚠ Восстановление — OVERLAY (дописывание поверх текущих данных), лишние объекты НЕ удаляются:
# файл/объект, которого нет в снапшоте, останется. Для бакета MinIO строгий снимок 1:1 —
# DEAL_MINIO_MIRROR_REMOVE=1 (mc mirror --remove); для data-каталогов/томов при необходимости
# очистите целевой каталог/том вручную ПЕРЕД распаковкой.
#
# Код возврата: 0 — все запрошенные шаги ok; 1 — сбой. Лог — консоль и
# $BACKUP_DIR/logs/restore-YYYYMM.log. Секреты в лог не попадают.
# Реальный прогон и тест восстановления — ⚠ Manual (нужен поднятый docker-стек).
set -euo pipefail
# Общие env-дефолты и хелперы (BACKUP_DIR, resolve_pg_container, select_mc_mode, mc_cmd…)
# shellcheck source=deal-backup-lib.sh
. "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/deal-backup-lib.sh"
# --- Аргументы и валидация -------------------------------------------------------------
say_usage() {
say "использование: restore.sh [all|pg|minio|data] [TS]"
say " TS — метка снапшота YYYYMMDD-HHMMSS (по умолчанию — самый свежий)"
}
STEP=${1:-all}
TS_ARG=${2:-""}
case "$STEP" in
all | pg | minio | data) ;;
*) say_usage; die "неизвестный шаг '$STEP'" ;;
esac
case "$TS_ARG" in
'') ;;
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) ;;
*) say_usage; die "TS должен быть вида YYYYMMDD-HHMMSS (сейчас: '$TS_ARG')" ;;
esac
mkdir -p "$BACKUP_DIR/logs"
latest_pg_ts() { # самая свежая метка среди дампов pg
ls -1 "$BACKUP_DIR"/pg/backup-*.dump 2>/dev/null \
| sort | tail -n 1 | sed 's#.*/backup-##; s/\.dump$//'
}
resolve_pg_ts() { # выбирает TS_ARG: из аргумента или самый свежий pg-снапшот
if [ -n "$TS_ARG" ]; then return 0; fi
TS_ARG=$(latest_pg_ts)
if [ -z "$TS_ARG" ]; then
die $BACKUP_DIR/pg нет дампов backup-*.dump — сначала выполните scripts/backup.sh"
fi
log "Выбран снапшот: $TS_ARG"
}
# --- Шаги --------------------------------------------------------------------------------
restore_pg() { # БД deal целиком (public + tenant_*)
local dump
resolve_pg_ts
dump="$BACKUP_DIR/pg/backup-$TS_ARG.dump"
[ -f "$dump" ] || die "дамп не найден: $dump"
if [ -n "$DEAL_PG_HOST" ]; then
have pg_restore || die "задан DEAL_PG_HOST, но pg_restore не найден в PATH"
log "Postgres: прямое pg_restore --clean --if-exists → $DEAL_PG_HOST:$DEAL_PG_PORT/$DEAL_PG_DB"
PGPASSWORD="$DEAL_PG_PASSWORD" pg_restore --clean --if-exists --no-owner --exit-on-error \
-h "$DEAL_PG_HOST" -p "$DEAL_PG_PORT" -U "$DEAL_PG_USER" -d "$DEAL_PG_DB" "$dump" \
|| die "pg_restore завершился с ошибкой"
else
docker_ok || die "Docker недоступен и DEAL_PG_HOST не задан — укажите DEAL_PG_HOST… для прямого pg_restore"
resolve_pg_container \
|| die "контейнер Postgres не найден (искал deal-postgres и compose-сервис 'postgres'); задайте DEAL_PG_CONTAINER или DEAL_PG_HOST"
log "Postgres: docker cp $dump$PG_CONTAINER, dropdb + createdb + pg_restore"
docker cp "$(host_docker_path "$dump")" "$PG_CONTAINER:/tmp/deal-restore.dump" || die "docker cp дампа не удался"
# БД пересоздаётся целиком — снимок всех схем консистентен (core/сервисы должны быть остановлены!)
docker exec "$PG_CONTAINER" dropdb -U "$DEAL_PG_USER" --if-exists "$DEAL_PG_DB" \
|| die "dropdb не удался (есть активные подключения? остановите core/сервисы)"
docker exec "$PG_CONTAINER" createdb -U "$DEAL_PG_USER" -O "$DEAL_PG_USER" "$DEAL_PG_DB" \
|| die "createdb не удался"
docker exec "$PG_CONTAINER" pg_restore -U "$DEAL_PG_USER" -d "$DEAL_PG_DB" --no-owner --exit-on-error \
/tmp/deal-restore.dump || die "pg_restore завершился с ошибкой"
docker exec "$PG_CONTAINER" rm -f /tmp/deal-restore.dump || true
fi
log "Postgres: OK (БД $DEAL_PG_DB из $dump)"
}
restore_minio() { # mc mirror снапшота → бакет deal-files
local snap
select_mc_mode
[ "$MC_MODE" = "skip" ] && return 0
if [ -n "$TS_ARG" ]; then
snap="$BACKUP_DIR/minio/backup-$TS_ARG"
[ -d "$snap" ] || die "снапшот MinIO не найден: $snap"
else
snap=$(ls -1d "$BACKUP_DIR"/minio/backup-* 2>/dev/null | sort | tail -n 1)
[ -n "$snap" ] || die $BACKUP_DIR/minio нет снапшотов backup-*"
fi
log "MinIO: mc mirror $snap → бакет $DEAL_MINIO_BUCKET (режим mc: $MC_MODE)"
if [ "$DEAL_MINIO_MIRROR_REMOVE" = "1" ] || [ "$DEAL_MINIO_MIRROR_REMOVE" = "true" ]; then
mc_cmd mirror --overwrite --remove "$snap" "deal/$DEAL_MINIO_BUCKET" \
|| die "mc mirror (restore) завершился с ошибкой"
else
mc_cmd mirror --overwrite "$snap" "deal/$DEAL_MINIO_BUCKET" \
|| die "mc mirror (restore) завершился с ошибкой"
fi
log "MinIO: OK ($snap$DEAL_MINIO_BUCKET)"
}
restore_data() { # распаковка архивов данных: docker volume'ы (DEAL_TAR_VOLUMES) или DEAL_DATA_DIR
local vol arc
if [ -n "$DEAL_TAR_VOLUMES" ]; then
docker_ok || die "задан DEAL_TAR_VOLUMES, но Docker недоступен"
for vol in $DEAL_TAR_VOLUMES; do
docker volume inspect "$vol" >/dev/null 2>&1 \
|| die "docker volume '$vol' не существует (docker volume ls)"
if [ -n "$TS_ARG" ]; then
arc="$BACKUP_DIR/data/backup-$TS_ARG.$vol.tar.gz"
[ -f "$arc" ] || arc=""
else
arc=$(ls -1 "$BACKUP_DIR"/data/backup-*."$vol".tar.gz 2>/dev/null | sort | tail -n 1)
fi
if [ -z "$arc" ] || [ ! -f "$arc" ]; then
log "Data: WARNING — архив тома '$vol' не найден, пропускаю"
continue
fi
log "Data: распаковка $(basename "$arc") → docker volume $vol (busybox)"
docker run --rm \
-v "$(host_docker_path "$BACKUP_DIR/data"):/in:ro" -v "$vol:/data" \
busybox tar -xzf "/in/$(basename "$arc")" -C /data \
|| die "распаковка в volume '$vol' завершилась с ошибкой"
log "Data: OK ($(basename "$arc")$vol)"
done
return 0
fi
[ -d "$DEAL_DATA_DIR" ] || die "DEAL_DATA_DIR='$DEAL_DATA_DIR' не существует (создайте каталог)"
if [ -n "$TS_ARG" ]; then
arc="$BACKUP_DIR/data/backup-$TS_ARG.tar.gz"
else
# только host-архивы вида backup-YYYYMMDD-HHMMSS.tar.gz (архивы томов содержат имя volume)
arc=$(ls -1 "$BACKUP_DIR"/data/backup-*.tar.gz 2>/dev/null \
| grep -E 'backup-[0-9]{8}-[0-9]{6}\.tar\.gz$' | sort | tail -n 1)
fi
[ -n "$arc" ] && [ -f "$arc" ] || die "архив данных не найден в $BACKUP_DIR/data (backup-<TS>.tar.gz)"
log "Data: распаковка $(basename "$arc")$DEAL_DATA_DIR"
tar -xzf "$arc" -C "$DEAL_DATA_DIR" || die "распаковка архива данных завершилась с ошибкой"
log "Data: OK ($(basename "$arc")$DEAL_DATA_DIR)"
}
# --- Основной поток ------------------------------------------------------------------------
main() {
log "=== Восстановление «Дейла»: старт (шаг: $STEP, BACKUP_DIR=$BACKUP_DIR) ==="
case "$STEP" in
all)
restore_pg || return 1
restore_minio || return 1
restore_data || return 1
;;
pg) restore_pg || return 1 ;;
minio) restore_minio || return 1 ;;
data) restore_data || return 1 ;;
esac
log "=== Восстановление завершено (шаг: $STEP, TS: ${TS_ARG:-последний}) ==="
}
LOG_FILE="$BACKUP_DIR/logs/restore-$(date +%Y%m).log"
main "$@" 2>&1 | tee -a "$LOG_FILE"
status=$?
exit "$status"