#!/usr/bin/env bash set -Eeuo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd)" ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" BACKUP_DIR="${BACKUP_DIR:-${ROOT_DIR}/storage/backups}" MYSQL_CLIENT_IMAGE="${MYSQL_CLIENT_IMAGE:-mysql:8.4}" usage() { cat <<'EOF' Использование: ./scripts/db-backup.sh export [--target=all|app|auth|characters] [--out=DIR] ./scripts/db-backup.sh import <файл.sql[.gz]> --target=app|auth|characters ./scripts/db-backup.sh list Опции: --env=PATH путь к .env (по умолчанию ./.env) --target=NAME какая БД: app (laravel), auth (acore_auth), characters (acore_characters), all (по умолчанию для export) --out=DIR куда писать дампы (по умолчанию storage/backups) --gzip сжимать дамп gzip-ом (по умолчанию включено для export) --no-gzip не сжимать --force для import: не спрашивать подтверждение --create-db перед импортом создать БД, если её нет -h, --help показать справку Переменные окружения: ENV_FILE, BACKUP_DIR — переопределяют значения по умолчанию MYSQL_CLIENT_IMAGE — образ для фолбэка (по умолчанию mysql:8.4) Зависимости: Если на хосте установлены mysqldump/mysql — используются они. Иначе скрипт запускает их в одноразовом контейнере MYSQL_CLIENT_IMAGE. Примеры: ./scripts/db-backup.sh export ./scripts/db-backup.sh export --target=auth --out=/tmp ./scripts/db-backup.sh import /tmp/acore_auth_2026-04-26.sql.gz --target=auth ./scripts/db-backup.sh import dump.sql.gz --target=app --create-db --force EOF } log() { printf '\033[1;34m[db-backup]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[db-backup]\033[0m %s\n' "$*" >&2; } err() { printf '\033[1;31m[db-backup]\033[0m %s\n' "$*" >&2; } require_cmd() { command -v "$1" >/dev/null 2>&1 || { err "Не найдена команда: $1"; exit 1; } } # Безопасное чтение значения из .env (поддержка кавычек) env_get() { local key="$1" file="$2" local line value line="$(grep -E "^${key}=" "$file" | tail -n1 || true)" [[ -z "$line" ]] && return 0 value="${line#${key}=}" # снять обрамляющие кавычки if [[ "$value" =~ ^\".*\"$ ]] || [[ "$value" =~ ^\'.*\'$ ]]; then value="${value:1:${#value}-2}" fi printf '%s' "$value" } load_target() { local target="$1" DB_DOCKER_SERVICE="" DB_DOCKER_PORT="" case "$target" in app) DB_HOST="$(env_get DB_HOST "$ENV_FILE")" DB_PORT="$(env_get DB_PORT "$ENV_FILE")" DB_NAME="$(env_get DB_DATABASE "$ENV_FILE")" DB_USER="$(env_get DB_USERNAME "$ENV_FILE")" DB_PASS="$(env_get DB_PASSWORD "$ENV_FILE")" if [[ "$DB_HOST" == "mysql" ]]; then DB_DOCKER_SERVICE="mysql" DB_DOCKER_PORT="${DB_PORT:-3306}" fi ;; auth) DB_HOST="$(env_get AZEROTHCORE_AUTH_DB_HOST "$ENV_FILE")" DB_PORT="$(env_get AZEROTHCORE_AUTH_DB_PORT "$ENV_FILE")" DB_NAME="$(env_get AZEROTHCORE_AUTH_DB_DATABASE "$ENV_FILE")" DB_USER="$(env_get AZEROTHCORE_AUTH_DB_USERNAME "$ENV_FILE")" DB_PASS="$(env_get AZEROTHCORE_AUTH_DB_PASSWORD "$ENV_FILE")" ;; characters) DB_HOST="$(env_get AZEROTHCORE_CHARACTERS_DB_HOST "$ENV_FILE")" DB_PORT="$(env_get AZEROTHCORE_CHARACTERS_DB_PORT "$ENV_FILE")" DB_NAME="$(env_get AZEROTHCORE_CHARACTERS_DB_DATABASE "$ENV_FILE")" DB_USER="$(env_get AZEROTHCORE_CHARACTERS_DB_USERNAME "$ENV_FILE")" DB_PASS="$(env_get AZEROTHCORE_CHARACTERS_DB_PASSWORD "$ENV_FILE")" ;; *) err "Неизвестная цель: $target"; exit 2;; esac # docker-only хосты заменяем на localhost при запуске вне контейнера if [[ "$DB_HOST" == "mysql" || "$DB_HOST" == "host.docker.internal" ]]; then local fwd fwd="$(env_get FORWARD_DB_PORT "$ENV_FILE")" if [[ -n "$fwd" && "$target" == "app" ]]; then DB_PORT="$fwd" fi DB_HOST="127.0.0.1" fi if [[ -z "$DB_NAME" || -z "$DB_USER" ]]; then err "Не удалось определить параметры подключения для target=$target" exit 1 fi } have_native_mysql() { command -v mysqldump >/dev/null 2>&1 && command -v mysql >/dev/null 2>&1 } # Запуск mysqldump/mysql либо на хосте, либо в одноразовом docker-контейнере. # Стдин/стдаут пробрасываются — годится и для дампа, и для импорта по pipe. # $1 — "mysqldump" или "mysql"; остальные — доп.аргументы тулзе. # Подключение читается из глобалей DB_HOST, DB_PORT, DB_USER, DB_PASS, DB_NAME. run_mysql_tool() { local tool="$1"; shift if have_native_mysql; then local defaults defaults="$(mktemp)" chmod 600 "$defaults" { printf '[client]\n' printf 'host=%s\n' "$DB_HOST" printf 'port=%s\n' "${DB_PORT:-3306}" printf 'user=%s\n' "$DB_USER" if [[ -n "${DB_PASS:-}" && "$DB_PASS" != "null" ]]; then printf 'password=%s\n' "$DB_PASS" fi } > "$defaults" local rc=0 "$tool" --defaults-extra-file="$defaults" "$@" || rc=$? rm -f "$defaults" return $rc fi require_cmd docker if [[ -n "${DB_DOCKER_SERVICE:-}" ]]; then local pwd_value="" if [[ -n "${DB_PASS:-}" && "$DB_PASS" != "null" ]]; then pwd_value="$DB_PASS" fi docker compose exec -T \ -e MYSQL_PWD="$pwd_value" \ "$DB_DOCKER_SERVICE" \ "$tool" \ -h127.0.0.1 \ -P"${DB_DOCKER_PORT:-3306}" \ -u"$DB_USER" \ "$@" return fi local target_host="$DB_HOST" if [[ "$target_host" == "127.0.0.1" || "$target_host" == "localhost" ]]; then target_host="host.docker.internal" fi local pwd_value="" if [[ -n "${DB_PASS:-}" && "$DB_PASS" != "null" ]]; then pwd_value="$DB_PASS" fi docker run --rm -i \ --add-host=host.docker.internal:host-gateway \ -e MYSQL_PWD="$pwd_value" \ "$MYSQL_CLIENT_IMAGE" \ "$tool" \ -h"$target_host" \ -P"${DB_PORT:-3306}" \ -u"$DB_USER" \ "$@" } do_export_one() { local target="$1" out_dir="$2" use_gzip="$3" load_target "$target" local ts dump_path ts="$(date +%Y-%m-%d_%H-%M-%S)" dump_path="${out_dir}/${DB_NAME}_${ts}.sql" log "Экспорт ${target} (${DB_NAME}@${DB_HOST}:${DB_PORT:-3306}) → ${dump_path}$([[ $use_gzip -eq 1 ]] && echo .gz)" local -a dump_args=( --single-transaction --quick --routines --triggers --events --set-gtid-purged=OFF --hex-blob --default-character-set=utf8mb4 --no-tablespaces "$DB_NAME" ) if [[ "$use_gzip" -eq 1 ]]; then require_cmd gzip run_mysql_tool mysqldump "${dump_args[@]}" | gzip -c > "${dump_path}.gz" log "Готово: ${dump_path}.gz ($(du -h "${dump_path}.gz" | cut -f1))" else run_mysql_tool mysqldump "${dump_args[@]}" > "$dump_path" log "Готово: ${dump_path} ($(du -h "$dump_path" | cut -f1))" fi } do_export() { local target="$1" out_dir="$2" use_gzip="$3" mkdir -p "$out_dir" if [[ "$target" == "all" ]]; then for t in app auth characters; do do_export_one "$t" "$out_dir" "$use_gzip" done else do_export_one "$target" "$out_dir" "$use_gzip" fi } do_import() { local file="$1" target="$2" force="$3" create_db="$4" [[ -f "$file" ]] || { err "Файл не найден: $file"; exit 1; } [[ -z "$target" ]] && { err "Для import обязателен --target"; exit 2; } load_target "$target" if [[ "$force" -ne 1 ]]; then printf '\033[1;33m[!] Импорт перезапишет данные в %s@%s:%s. Продолжить? [y/N]: \033[0m' \ "$DB_NAME" "$DB_HOST" "${DB_PORT:-3306}" read -r answer [[ "$answer" =~ ^[yYдД]$ ]] || { warn "Отменено"; exit 0; } fi if [[ "$create_db" -eq 1 ]]; then log "Создаю БД ${DB_NAME} при отсутствии" local saved_name="$DB_NAME" DB_NAME="" run_mysql_tool mysql -e \ "CREATE DATABASE IF NOT EXISTS \`$saved_name\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci" DB_NAME="$saved_name" fi log "Импорт ${file} → ${target} (${DB_NAME}@${DB_HOST}:${DB_PORT:-3306})" if [[ "$file" == *.gz ]]; then require_cmd gzip gzip -dc "$file" | run_mysql_tool mysql "$DB_NAME" else run_mysql_tool mysql "$DB_NAME" < "$file" fi log "Импорт завершён" } do_list() { if [[ ! -d "$BACKUP_DIR" ]]; then warn "Директория бэкапов отсутствует: $BACKUP_DIR" return 0 fi log "Бэкапы в $BACKUP_DIR:" ls -lh "$BACKUP_DIR" 2>/dev/null | awk 'NR>1 {print " " $9 " (" $5 ", " $6, $7, $8 ")"}' } main() { [[ $# -lt 1 ]] && { usage; exit 2; } local cmd="$1"; shift local target="" out_dir="$BACKUP_DIR" use_gzip=1 force=0 create_db=0 import_file="" while [[ $# -gt 0 ]]; do case "$1" in --env=*) ENV_FILE="${1#*=}";; --target=*) target="${1#*=}";; --out=*) out_dir="${1#*=}";; --gzip) use_gzip=1;; --no-gzip) use_gzip=0;; --force) force=1;; --create-db) create_db=1;; -h|--help) usage; exit 0;; -*) err "Неизвестная опция: $1"; usage; exit 2;; *) import_file="$1";; esac shift done [[ -f "$ENV_FILE" ]] || { err ".env не найден: $ENV_FILE"; exit 1; } case "$cmd" in export) [[ -z "$target" ]] && target="all" do_export "$target" "$out_dir" "$use_gzip" ;; import) [[ -z "$import_file" ]] && { err "Укажите путь к дампу"; usage; exit 2; } do_import "$import_file" "$target" "$force" "$create_db" ;; list) do_list ;; -h|--help|help) usage ;; *) err "Неизвестная команда: $cmd" usage exit 2 ;; esac } main "$@"