#!/usr/bin/env bash set -Eeuo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd)" PROD_COMPOSE="${ROOT_DIR}/compose.prod.yaml" ENV_FILE="${MOONWELL_ENV_FILE:-${ROOT_DIR}/.env}" BACKUP_WORK_DIR="${ROOT_DIR}/storage/app/private/database-backups" LOCK_FILE="${BACKUP_WORK_DIR}/backup.lock" RUN_DIR='' log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S %Z')" "$*" } fail() { log "ERROR: $*" >&2 exit 1 } env_value() { local key="$1" local value value="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2-)" if [[ "$value" == \"*\" && "$value" == *\" ]]; then value="${value:1:${#value}-2}" elif [[ "$value" == \'*\' && "$value" == *\' ]]; then value="${value:1:${#value}-2}" fi printf '%s' "$value" } validate_database_name() { [[ "$1" =~ ^[A-Za-z0-9_-]+$ ]] || fail "Недопустимое имя базы: $1" } prod() { docker compose -f "$PROD_COMPOSE" "$@" } cleanup() { if [[ -n "$RUN_DIR" && -d "$RUN_DIR" && "$RUN_DIR" == "${BACKUP_WORK_DIR}/"* ]]; then find "$RUN_DIR" -maxdepth 1 -type f -delete rmdir "$RUN_DIR" 2>/dev/null || true fi } dump_web_database() { local database="$1" local output="$2" log "Создаю дамп локальной базы ${database}" prod exec -T mysql sh -c ' export MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysqldump \ --user=root \ --single-transaction \ --quick \ --hex-blob \ --triggers \ --no-tablespaces \ --set-gtid-purged=OFF \ --databases "$1" ' sh "$database" | gzip > "$output" } dump_remote_database() { local database="$1" local output="$2" local host="$3" local port="$4" local username="$5" local password="$6" log "Создаю дамп AzerothCore ${database}" docker run --rm --network host \ --env MYSQL_PWD="$password" \ mysql:8.4 \ mysqldump \ --host="$host" \ --port="$port" \ --user="$username" \ --single-transaction \ --quick \ --hex-blob \ --triggers \ --no-tablespaces \ --set-gtid-purged=OFF \ --databases "$database" | gzip > "$output" } upload_backup() { local source="$1" local destination="$2" local app_uid app_gid app_uid="$(env_value WWWUSER)" app_gid="$(env_value WWWGROUP)" app_uid="${app_uid:-1000}" app_gid="${app_gid:-1000}" chown "${app_uid}:${app_gid}" "$RUN_DIR" "$source" chmod 0750 "$RUN_DIR" chmod 0640 "$source" prod exec -T --user "${app_uid}:${app_gid}" app \ php artisan backup:upload "/var/www/html/storage/app/private/database-backups/${RUN_DIR##*/}/${source##*/}" "$destination" } main() { command -v docker >/dev/null 2>&1 || fail 'Не найден Docker' command -v gzip >/dev/null 2>&1 || fail 'Не найден gzip' command -v flock >/dev/null 2>&1 || fail 'Не найден flock' docker compose version >/dev/null 2>&1 || fail 'Не найден Docker Compose Plugin' [[ -f "$ENV_FILE" ]] || fail "Не найден ${ENV_FILE}" [[ -f "$PROD_COMPOSE" ]] || fail "Не найден ${PROD_COMPOSE}" mkdir -p "$BACKUP_WORK_DIR" exec 9>"$LOCK_FILE" if ! flock -n 9; then log 'Предыдущий запуск ещё выполняется, новый запуск пропущен' return 0 fi local timestamp object_prefix web_database local acore_host acore_port acore_username acore_password acore_databases local database filename local -a databases=() files=() timestamp="$(date -u '+%Y-%m-%dT%H-%M-%SZ')" object_prefix="$(env_value DATABASE_BACKUP_PREFIX)" object_prefix="${object_prefix:-backups/databases}" object_prefix="${object_prefix%/}/$(date -u '+%Y/%m/%d')/${timestamp}" web_database="$(env_value DB_DATABASE)" acore_host="$(env_value AZEROTHCORE_AUTH_DB_HOST)" acore_port="$(env_value AZEROTHCORE_AUTH_DB_PORT)" acore_username="$(env_value AZEROTHCORE_AUTH_DB_USERNAME)" acore_password="$(env_value AZEROTHCORE_AUTH_DB_PASSWORD)" acore_databases="$(env_value DATABASE_BACKUP_ACORE_DATABASES)" acore_databases="${acore_databases:-acore_auth acore_characters acore_world acore_playerbots store}" [[ -n "$web_database" ]] || fail 'DB_DATABASE не задан' [[ -n "$acore_host" && -n "$acore_port" && -n "$acore_username" ]] || fail 'Не настроено подключение к AzerothCore' validate_database_name "$web_database" RUN_DIR="${BACKUP_WORK_DIR}/${timestamp}" mkdir -m 0700 "$RUN_DIR" trap cleanup EXIT filename="${web_database}.sql.gz" dump_web_database "$web_database" "${RUN_DIR}/${filename}" databases+=("$web_database") files+=("$filename") for database in $acore_databases; do validate_database_name "$database" filename="${database}.sql.gz" dump_remote_database "$database" "${RUN_DIR}/${filename}" "$acore_host" "$acore_port" "$acore_username" "$acore_password" databases+=("$database") files+=("$filename") done log "Все дампы созданы, загружаю набор ${timestamp} в S3" for filename in "${files[@]}"; do [[ -s "${RUN_DIR}/${filename}" ]] || fail "Пустой файл дампа: ${filename}" upload_backup "${RUN_DIR}/${filename}" "${object_prefix}/${filename}" done log "Бэкап завершён: ${#databases[@]} баз, s3://${object_prefix}/" } main "$@"