Инициализировать репозиторий «Дейл»
Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ, инструкция пользователя, техдокументация, код-стайл), бэклог, скрипты развёртывания и архив прототипа LeadRadar.
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user