#!/usr/bin/env bash # # deal-backup-lib.sh — общие для scripts/backup.sh и scripts/restore.sh env-дефолты и хелперы # (этап 7 Task 15, Ruling 8). Скрипт НЕ исполняется напрямую: подключается из backup.sh/restore.sh # . "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/deal-backup-lib.sh" # # Параметры — окружение (секреты скрипты НЕ логируют; в лог — только имена файлов и размеры): # # BACKUP_DIR / DEAL_BACKUP_DIR корень бэкапов (по умолчанию <репозиторий>/data/backups) # RETENTION_DAYS / DEAL_RETENTION_DAYS # сколько дней хранить снапшоты (удаление по дате YYYYMMDD в имени; 14) # DEAL_DATA_DIR корень host-каталогов данных (по умолчанию <репозиторий>/data) # DEAL_TAR_DIRS относительные к DEAL_DATA_DIR каталоги для тара (по умолчанию # "attachments ml telegram_sessions"; контейнерный путь каталога # сессий telegram-service — /data/sessions) # DEAL_TAR_VOLUMES если задан (список docker volume'ов через пробел) — файловые данные # берутся из томов busybox-контейнером (Ruling 8), DEAL_TAR_DIRS # игнорируется # # Postgres (docker-режим по умолчанию — exec в контейнер; прямой pg_dump — при заданном DEAL_PG_HOST): # DEAL_PG_HOST / DEAL_PG_PORT (5432) / DEAL_PG_USER (deal) / DEAL_PG_PASSWORD / DEAL_PG_DB (deal) # DEAL_PG_CONTAINER имя контейнера (авто: deal-postgres → compose-сервис 'postgres') # # MinIO / mc (бэкап = выгрузка ИЗ MinIO в BACKUP_DIR; restore — обратно): # DEAL_MINIO_ENDPOINT пусто → host-mc: http://localhost:9000 (dev: опубликованный порт); # docker-mc: http://minio:9000 (алиас compose-сервиса — работает и в # compose.dev, и в compose.prod). prod (compose.prod) порты MinIO НЕ # публикует — хостовый mc его не достанет: нужен docker-режим (дефолт, # endpoint minio:9000) либо достижимый DEAL_MINIO_ENDPOINT. # DEAL_MINIO_ACCESS_KEY (fallback: MINIO_ROOT_USER; иначе deal_minio) # DEAL_MINIO_SECRET_KEY (fallback: MINIO_ROOT_PASSWORD; иначе deal_minio_secret) # DEAL_MINIO_BUCKET (deal-files) # DEAL_MINIO_SKIP 1 → шаг MinIO пропускается (Local-хранилище без MinIO) # DEAL_MINIO_MIRROR_REMOVE restore: 1 → mc mirror --remove (бакет = снимок 1:1) # DEAL_MC_IMAGE docker-режим mc (minio/mc; в проде зафиксируйте тег, напр. # minio/mc:RELEASE.2025-04-16T15-23-31Z) # DEAL_MC_CONTAINER контейнер MinIO для сети (авто: deal-minio → compose-сервис 'minio') set -u # --- Корень репозитория и каталоги ----------------------------------------------- SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) REPO_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd) BACKUP_DIR=${BACKUP_DIR:-${DEAL_BACKUP_DIR:-"$REPO_DIR/data/backups"}} RETENTION_DAYS=${RETENTION_DAYS:-${DEAL_RETENTION_DAYS:-14}} DEAL_DATA_DIR=${DEAL_DATA_DIR:-"$REPO_DIR/data"} DEAL_TAR_DIRS=${DEAL_TAR_DIRS:-"attachments ml telegram_sessions"} DEAL_TAR_VOLUMES=${DEAL_TAR_VOLUMES:-""} # --- Postgres ---------------------------------------------------------------------- DEAL_PG_DB=${DEAL_PG_DB:-deal} DEAL_PG_USER=${DEAL_PG_USER:-deal} DEAL_PG_HOST=${DEAL_PG_HOST:-""} DEAL_PG_PORT=${DEAL_PG_PORT:-5432} DEAL_PG_PASSWORD=${DEAL_PG_PASSWORD:-""} DEAL_PG_CONTAINER=${DEAL_PG_CONTAINER:-""} # --- MinIO / mc -------------------------------------------------------------------- DEAL_MINIO_BUCKET=${DEAL_MINIO_BUCKET:-deal-files} DEAL_MINIO_ENDPOINT=${DEAL_MINIO_ENDPOINT:-""} DEAL_MINIO_ACCESS_KEY=${DEAL_MINIO_ACCESS_KEY:-${MINIO_ROOT_USER:-deal_minio}} DEAL_MINIO_SECRET_KEY=${DEAL_MINIO_SECRET_KEY:-${MINIO_ROOT_PASSWORD:-deal_minio_secret}} DEAL_MINIO_SKIP=${DEAL_MINIO_SKIP:-0} DEAL_MINIO_MIRROR_REMOVE=${DEAL_MINIO_MIRROR_REMOVE:-0} DEAL_MC_IMAGE=${DEAL_MC_IMAGE:-minio/mc} DEAL_MC_CONTAINER=${DEAL_MC_CONTAINER:-""} # Найденные рантайм-значения (заполняют функции ниже) PG_CONTAINER="" MINIO_CONTAINER="" MC_MODE="" # skip | host (клиент mc) | docker (разовый контейнер minio/mc) MC_ENDPOINT="$DEAL_MINIO_ENDPOINT" # --- Утилиты ----------------------------------------------------------------------- say() { printf '%s\n' "$*"; } log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*"; } die() { log "ОШИБКА: $*"; exit 1; } have() { command -v "$1" >/dev/null 2>&1; } docker_ok() { docker info >/dev/null 2>&1; } # --- Windows (Git Bash/MSYS): docker не понимает /c/... пути и ломает контейнерные # /out, /in (MSYS конвертирует их в C:/Program Files/Git/...). Отключаем конвертацию # аргументов и переводим host-пути в Windows-вид (C:/...) для -v/-cp. На Linux хелпер # возвращает путь как есть — поведение не меняется. if uname -s 2>/dev/null | grep -qiE 'mingw|msys|cygwin'; then export MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' host_docker_path() { cygpath -m "$1"; } else host_docker_path() { printf '%s' "$1"; } fi container_running() { # имя контейнера → 0, если запущен [ -n "${1:-}" ] && [ "$(docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null)" = "true" ] } compose_container() { # метка com.docker.compose.service → имя running-контейнера (или пусто) docker ps --filter "label=com.docker.compose.service=$1" --format '{{.Names}}' 2>/dev/null | head -n 1 || true } # --- Поиск контейнеров Postgres / MinIO -------------------------------------------- resolve_pg_container() { local c if [ -n "$DEAL_PG_CONTAINER" ]; then container_running "$DEAL_PG_CONTAINER" \ || die "контейнер DEAL_PG_CONTAINER='$DEAL_PG_CONTAINER' не найден/не запущен" PG_CONTAINER="$DEAL_PG_CONTAINER" return 0 fi if container_running deal-postgres; then PG_CONTAINER=deal-postgres; return 0; fi c=$(compose_container postgres) if [ -n "$c" ] && container_running "$c"; then PG_CONTAINER="$c"; return 0; fi return 1 } resolve_minio_container() { local c if [ -n "$DEAL_MC_CONTAINER" ]; then container_running "$DEAL_MC_CONTAINER" \ || die "контейнер DEAL_MC_CONTAINER='$DEAL_MC_CONTAINER' не найден/не запущен" MINIO_CONTAINER="$DEAL_MC_CONTAINER" return 0 fi if container_running deal-minio; then MINIO_CONTAINER=deal-minio; return 0; fi c=$(compose_container minio) if [ -n "$c" ] && container_running "$c"; then MINIO_CONTAINER="$c"; return 0; fi return 1 } minio_network() { # первая docker-сеть контейнера MinIO (для разового контейнера mc) docker inspect -f '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}' \ "$MINIO_CONTAINER" 2>/dev/null | awk '{ print $1 }' } # --- mc: выбор режима и запуск ------------------------------------------------------- select_mc_mode() { # заполняет MC_MODE / MC_ENDPOINT if [ "$DEAL_MINIO_SKIP" = "1" ] || [ "$DEAL_MINIO_SKIP" = "true" ]; then MC_MODE=skip elif have mc; then MC_MODE=host [ -n "$MC_ENDPOINT" ] || MC_ENDPOINT=http://localhost:9000 elif docker_ok; then MC_MODE=docker # minio — алиас compose-сервиса: резолвится и в compose.dev (container_name deal-minio), и в # compose.prod (container_name нет, сервис 'minio'); deal-minio:9000 в prod-сети не существует. [ -n "$MC_ENDPOINT" ] || MC_ENDPOINT=http://minio:9000 else die "MinIO: клиент mc не найден и docker недоступен — установите mc или задайте DEAL_MINIO_SKIP=1" fi } mc_cmd() { # выполняет «mc <аргументы…>» в выбранном режиме; alias 'deal' — только env MC_HOST_deal # (секрет в конфиг mc на диск НЕ пишется; значение env нигде не логируется) # MC_HOST_deal — единый URL http(s)://user:pass@host:port: схему берём из MC_ENDPOINT (если задана), # иначе http. Убираем схему из host:port — иначе URL получает двойную схему и mc его отвергает. local host_value endpoint scheme endpoint="$MC_ENDPOINT" scheme=http case "$endpoint" in https://*) scheme=https; endpoint=${endpoint#https://} ;; http://*) endpoint=${endpoint#http://} ;; esac host_value="$scheme://$DEAL_MINIO_ACCESS_KEY:$DEAL_MINIO_SECRET_KEY@$endpoint" case "$MC_MODE" in skip) log "MinIO: шаг пропущен (DEAL_MINIO_SKIP=1)" return 0 ;; host) MC_HOST_deal="$host_value" mc "$@" ;; docker) resolve_minio_container \ || die "MinIO: контейнер не найден (искал deal-minio и compose-сервис 'minio') — поднимите стек, задайте DEAL_MINIO_ENDPOINT или DEAL_MINIO_SKIP=1" log "MinIO: разовый контейнер $DEAL_MC_IMAGE (сеть: $(minio_network), бакет $DEAL_MINIO_BUCKET)" docker run --rm -i --network "$(minio_network)" \ -e "MC_HOST_deal=$host_value" \ -v "$(host_docker_path "$BACKUP_DIR"):$BACKUP_DIR" \ "$DEAL_MC_IMAGE" "$@" ;; *) die "MinIO: режим mc не выбран (MC_MODE='$MC_MODE') — вызовите select_mc_mode" ;; esac }