Files
Deal/scripts/deal-backup-lib.sh
T
Rustam Khalimov 713d554dc2 Нормализовать переводы строк в 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.
2026-09-11 19:01:42 +03:00

185 lines
10 KiB
Bash

#!/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
}