Нормализовать переводы строк в LF

Решение по TD-STYLE-ANALYZERS: LF — инструменты проекта (Python/Node) пишут LF,
CRLF-.sh не работают на Linux CI (sh scripts/ci.sh), большинство файлов уже были
LF. Добавлен .gitattributes (* text=auto eol=lf, бинарные исключения),
.editorconfig переведён на lf, 1029 файлов конвертированы, git add --renormalize.
Из индекса убраны закравшиеся archive/**/__pycache__/*.pyc.
This commit is contained in:
Rustam Khalimov
2026-09-11 19:01:42 +03:00
parent 39c9bdc1b7
commit 713d554dc2
751 changed files with 94507 additions and 94486 deletions
+183 -183
View File
@@ -1,183 +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"
#!/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"