#!/bin/bash set -euo pipefail MYSQL_PLUGIN_CREDENTIALS_FILE="${MYSQL_PLUGIN_CREDENTIALS_FILE:-/usr/local/directadmin/conf/alt-mysql.conf}" MYSQL_PLUGIN_METADATA_FILE="${MYSQL_PLUGIN_METADATA_FILE:-/usr/local/directadmin/conf/alt-mariadb.env}" MYSQL_PLUGIN_ALT_MY_CNF="${MYSQL_PLUGIN_ALT_MY_CNF:-/usr/local/directadmin/conf/alt-my.cnf}" MYSQL_PLUGIN_ALT_INSTANCE_CNF="${MYSQL_PLUGIN_ALT_INSTANCE_CNF:-/etc/alt-mariadb.cnf}" trim_value() { local value="${1:-}" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" printf '%s' "$value" } strip_inline_comment() { local value value="$(trim_value "${1:-}")" value="${value%%#*}" printf '%s' "$(trim_value "$value")" } unquote_value() { local value value="$(trim_value "${1:-}")" if [ "${#value}" -ge 2 ] && [ "${value:0:1}" = "'" ] && [ "${value: -1}" = "'" ]; then value="${value:1:${#value}-2}" elif [ "${#value}" -ge 2 ] && [ "${value:0:1}" = '"' ] && [ "${value: -1}" = '"' ]; then value="${value:1:${#value}-2}" fi printf '%s' "$value" } mysql_require_root() { if [ "${EUID:-$(id -u)}" -ne 0 ]; then echo "Ten skrypt musi być uruchomiony jako root." >&2 exit 1 fi } mysql_detect_binary() { local bin_name="$1" local candidate="" local alt_name="" if [ -n "${MYSQL_PLUGIN_CLIENT_BIN_DIR:-}" ]; then for alt_name in "$bin_name" "$(mysql_binary_alias "$bin_name")"; do [ -n "$alt_name" ] || continue candidate="${MYSQL_PLUGIN_CLIENT_BIN_DIR%/}/${alt_name}" if [ -x "$candidate" ]; then printf '%s\n' "$candidate" return 0 fi done fi for alt_name in "$bin_name" "$(mysql_binary_alias "$bin_name")"; do [ -n "$alt_name" ] || continue candidate="$(command -v "$alt_name" 2>/dev/null || true)" if [ -n "$candidate" ]; then printf '%s\n' "$candidate" return 0 fi done for alt_name in "$bin_name" "$(mysql_binary_alias "$bin_name")"; do [ -n "$alt_name" ] || continue while IFS= read -r candidate; do if [ -x "$candidate" ]; then printf '%s\n' "$candidate" return 0 fi done < <(find /usr/local -maxdepth 3 -type f -path '/usr/local/mariadb-*/bin/'"$alt_name" 2>/dev/null | sort -V -r) for candidate in /usr/bin/"$alt_name" /usr/local/bin/"$alt_name"; do if [ -x "$candidate" ]; then printf '%s\n' "$candidate" return 0 fi done done return 1 } mysql_binary_alias() { case "$1" in mysql) printf 'mariadb\n' ;; mysqldump) printf 'mariadb-dump\n' ;; mysqladmin) printf 'mariadb-admin\n' ;; mysql_upgrade) printf 'mariadb-upgrade\n' ;; *) printf '\n' ;; esac } mysql_load_plugin_settings_file() { local file="${1:-}" local raw_line key value [ -n "$file" ] || return 0 [ -r "$file" ] || return 0 while IFS= read -r raw_line || [ -n "$raw_line" ]; do raw_line="$(trim_value "$raw_line")" [ -n "$raw_line" ] || continue case "$raw_line" in \#*) continue ;; esac [[ "$raw_line" == *=* ]] || continue key="$(trim_value "${raw_line%%=*}")" value="$(unquote_value "$(strip_inline_comment "${raw_line#*=}")")" case "$key" in MYSQL_PLUGIN_CREDENTIALS_FILE) MYSQL_PLUGIN_CREDENTIALS_FILE="$value" ;; MYSQL_PLUGIN_METADATA_FILE) MYSQL_PLUGIN_METADATA_FILE="$value" ;; MYSQL_PLUGIN_ALT_MY_CNF) MYSQL_PLUGIN_ALT_MY_CNF="$value" ;; MYSQL_PLUGIN_ALT_MARIADB_CNF) MYSQL_PLUGIN_ALT_INSTANCE_CNF="$value" ;; MYSQL_PLUGIN_CLIENT_BIN_DIR) MYSQL_PLUGIN_CLIENT_BIN_DIR="$value" ;; esac done < "$file" } mysql_read_key_value() { local file="$1" local key="$2" [ -r "$file" ] || return 1 awk -F= -v key="$key" ' function trim(s) { sub(/^[[:space:]]+/, "", s) sub(/[[:space:]]+$/, "", s) return s } $0 ~ /^[[:space:]]*#/ || $0 !~ /=/ { next } { k = trim($1) if (k != key) { next } sub(/^[^=]*=/, "") v = trim($0) if (v ~ /^'\''.*'\''$/ || v ~ /^".*"$/) { v = substr(v, 2, length(v) - 2) } print v exit } ' "$file" } mysql_read_cnf_value() { local file="$1" local section="$2" local key="$3" [ -r "$file" ] || return 1 awk -F= -v section="$section" -v key="$key" ' function trim(s) { sub(/^[[:space:]]+/, "", s) sub(/[[:space:]]+$/, "", s) return s } /^[[:space:]]*#/ || /^[[:space:]]*;/ { next } /^\[[^]]+\]/ { current=$0 gsub(/^\[[[:space:]]*/, "", current) gsub(/[[:space:]]*\]$/, "", current) current=trim(current) next } current == section && $0 ~ /=/ { k=trim($1) if (k != key) { next } sub(/^[^=]*=/, "") v=trim($0) if (v ~ /^'\''.*'\''$/ || v ~ /^".*"$/) { v=substr(v, 2, length(v)-2) } print v exit } ' "$file" } mysql_load_alt_runtime() { MYSQL_ALT_METADATA_FILE="${MYSQL_ALT_METADATA_FILE:-$MYSQL_PLUGIN_METADATA_FILE}" MYSQL_ALT_MYSQL_CONF="${MYSQL_ALT_MYSQL_CONF:-$MYSQL_PLUGIN_CREDENTIALS_FILE}" MYSQL_ALT_MY_CNF="${MYSQL_ALT_MY_CNF:-$MYSQL_PLUGIN_ALT_MY_CNF}" MYSQL_ALT_INSTANCE_CNF="${MYSQL_ALT_INSTANCE_CNF:-$MYSQL_PLUGIN_ALT_INSTANCE_CNF}" MYSQL_ALT_SERVICE_NAME="${MYSQL_ALT_SERVICE_NAME:-alt-mariadb}" MYSQL_ALT_BASEDIR="${MYSQL_ALT_BASEDIR:-}" MYSQL_ALT_DATADIR="${MYSQL_ALT_DATADIR:-}" MYSQL_ALT_PORT="${MYSQL_ALT_PORT:-}" MYSQL_ALT_SOCKET="${MYSQL_ALT_SOCKET:-}" if [ -r "$MYSQL_ALT_METADATA_FILE" ]; then MYSQL_ALT_SERVICE_NAME="$(mysql_read_key_value "$MYSQL_ALT_METADATA_FILE" SERVICE_NAME || printf '%s' "$MYSQL_ALT_SERVICE_NAME")" MYSQL_ALT_BASEDIR="$(mysql_read_key_value "$MYSQL_ALT_METADATA_FILE" MARIADB_DIR || printf '%s' "$MYSQL_ALT_BASEDIR")" MYSQL_ALT_DATADIR="$(mysql_read_key_value "$MYSQL_ALT_METADATA_FILE" DATADIR || printf '%s' "$MYSQL_ALT_DATADIR")" MYSQL_ALT_PORT="$(mysql_read_key_value "$MYSQL_ALT_METADATA_FILE" PORT || printf '%s' "$MYSQL_ALT_PORT")" MYSQL_ALT_SOCKET="$(mysql_read_key_value "$MYSQL_ALT_METADATA_FILE" SOCKET || printf '%s' "$MYSQL_ALT_SOCKET")" MYSQL_ALT_MYSQL_CONF="$(mysql_read_key_value "$MYSQL_ALT_METADATA_FILE" ALT_MYSQL_CONF || printf '%s' "$MYSQL_ALT_MYSQL_CONF")" MYSQL_ALT_MY_CNF="$(mysql_read_key_value "$MYSQL_ALT_METADATA_FILE" ALT_MY_CNF || printf '%s' "$MYSQL_ALT_MY_CNF")" MYSQL_ALT_INSTANCE_CNF="$(mysql_read_key_value "$MYSQL_ALT_METADATA_FILE" INSTANCE_CNF || printf '%s' "$MYSQL_ALT_INSTANCE_CNF")" fi if [ -r "$MYSQL_ALT_MYSQL_CONF" ]; then [ -n "$MYSQL_ALT_PORT" ] || MYSQL_ALT_PORT="$(mysql_read_key_value "$MYSQL_ALT_MYSQL_CONF" port || true)" [ -n "$MYSQL_ALT_SOCKET" ] || MYSQL_ALT_SOCKET="$(mysql_read_key_value "$MYSQL_ALT_MYSQL_CONF" socket || true)" fi if [ -r "$MYSQL_ALT_INSTANCE_CNF" ]; then [ -n "$MYSQL_ALT_BASEDIR" ] || MYSQL_ALT_BASEDIR="$(mysql_read_cnf_value "$MYSQL_ALT_INSTANCE_CNF" mariadb basedir || true)" [ -n "$MYSQL_ALT_DATADIR" ] || MYSQL_ALT_DATADIR="$(mysql_read_cnf_value "$MYSQL_ALT_INSTANCE_CNF" mariadb datadir || true)" [ -n "$MYSQL_ALT_PORT" ] || MYSQL_ALT_PORT="$(mysql_read_cnf_value "$MYSQL_ALT_INSTANCE_CNF" mariadb port || true)" [ -n "$MYSQL_ALT_SOCKET" ] || MYSQL_ALT_SOCKET="$(mysql_read_cnf_value "$MYSQL_ALT_INSTANCE_CNF" mariadb socket || true)" fi if [ -n "$MYSQL_ALT_BASEDIR" ] && [ -d "$MYSQL_ALT_BASEDIR/bin" ]; then MYSQL_PLUGIN_CLIENT_BIN_DIR="${MYSQL_PLUGIN_CLIENT_BIN_DIR:-$MYSQL_ALT_BASEDIR/bin}" fi [ -n "$MYSQL_ALT_PORT" ] || MYSQL_ALT_PORT="3306" [ -n "$MYSQL_ALT_MYSQL_CONF" ] || MYSQL_ALT_MYSQL_CONF="/usr/local/directadmin/conf/alt-mysql.conf" [ -n "$MYSQL_ALT_MY_CNF" ] || MYSQL_ALT_MY_CNF="/usr/local/directadmin/conf/alt-my.cnf" [ -n "$MYSQL_ALT_INSTANCE_CNF" ] || MYSQL_ALT_INSTANCE_CNF="/etc/alt-mariadb.cnf" [ -n "$MYSQL_ALT_SERVICE_NAME" ] || MYSQL_ALT_SERVICE_NAME="alt-mariadb" } mysql_alt_service_name() { mysql_load_alt_runtime printf '%s\n' "$MYSQL_ALT_SERVICE_NAME" } mysql_alt_basedir() { mysql_load_alt_runtime printf '%s\n' "$MYSQL_ALT_BASEDIR" } mysql_alt_datadir() { mysql_load_alt_runtime printf '%s\n' "$MYSQL_ALT_DATADIR" } mysql_alt_port() { mysql_load_alt_runtime printf '%s\n' "$MYSQL_ALT_PORT" } mysql_alt_socket() { mysql_load_alt_runtime printf '%s\n' "$MYSQL_ALT_SOCKET" } mysql_alt_client_defaults() { mysql_load_alt_runtime printf '%s\n' "$MYSQL_ALT_MY_CNF" } mysql_alt_binary() { local bin_name="$1" mysql_load_alt_runtime mysql_detect_binary "$bin_name" } mysql_native_binary() { local bin_name="$1" local alt_name candidate for alt_name in "$bin_name" "$(mysql_binary_alias "$bin_name")"; do [ -n "$alt_name" ] || continue for candidate in /usr/bin/"$alt_name" /usr/local/bin/"$alt_name"; do if [ -x "$candidate" ]; then printf '%s\n' "$candidate" return 0 fi done candidate="$(command -v "$alt_name" 2>/dev/null || true)" if [ -n "$candidate" ]; then printf '%s\n' "$candidate" return 0 fi done return 1 } mysql_load_credentials() { local file="${1:-$MYSQL_PLUGIN_CREDENTIALS_FILE}" [ -r "$file" ] || { echo "Brak pliku poświadczeń MySQL: $file" >&2 exit 1 } MYSQL_HOST="" MYSQL_PORT="" MYSQL_USER="" MYSQL_PASSWORD="" MYSQL_DATABASE="" MYSQL_SOCKET="" while IFS= read -r raw_line || [ -n "$raw_line" ]; do raw_line="$(trim_value "$raw_line")" [ -n "$raw_line" ] || continue case "$raw_line" in \#*) continue ;; esac if [[ "$raw_line" == *=* ]]; then local key="${raw_line%%=*}" local value="${raw_line#*=}" key="$(printf '%s' "$key" | tr '[:upper:]' '[:lower:]')" key="$(trim_value "$key")" value="$(unquote_value "$(strip_inline_comment "$value")")" case "$key" in host|mysql_host) MYSQL_HOST="$value" ;; port|mysql_port) MYSQL_PORT="$value" ;; user|username|mysql_user) MYSQL_USER="$value" ;; password|passwd|pass|mysql_password) MYSQL_PASSWORD="$value" ;; database|dbname|mysql_database) MYSQL_DATABASE="$value" ;; socket|mysql_socket) MYSQL_SOCKET="$value" ;; esac continue fi local host_part port_part db_part user_part pass_part rest host_part="${raw_line%%:*}" rest="${raw_line#*:}" port_part="${rest%%:*}" rest="${rest#*:}" db_part="${rest%%:*}" rest="${rest#*:}" user_part="${rest%%:*}" pass_part="${rest#*:}" MYSQL_HOST="$host_part" MYSQL_PORT="$port_part" MYSQL_DATABASE="$db_part" MYSQL_USER="$user_part" MYSQL_PASSWORD="$pass_part" break done < "$file" MYSQL_HOST="$(trim_value "${MYSQL_HOST:-}")" MYSQL_PORT="$(trim_value "${MYSQL_PORT:-}")" MYSQL_USER="$(trim_value "${MYSQL_USER:-}")" MYSQL_PASSWORD="${MYSQL_PASSWORD:-}" MYSQL_DATABASE="$(trim_value "${MYSQL_DATABASE:-}")" MYSQL_SOCKET="$(trim_value "${MYSQL_SOCKET:-}")" [ -n "$MYSQL_HOST" ] || MYSQL_HOST="localhost" [ -n "$MYSQL_PORT" ] || MYSQL_PORT="3306" [ -n "$MYSQL_USER" ] || MYSQL_USER="root" [ -n "$MYSQL_DATABASE" ] || MYSQL_DATABASE="mysql" } mysql_load_alt_credentials() { mysql_load_alt_runtime mysql_load_credentials "$MYSQL_ALT_MYSQL_CONF" } mysql_base_args() { local args=("--user=$MYSQL_USER") if [ "$MYSQL_HOST" = "localhost" ]; then if [ -n "$MYSQL_SOCKET" ]; then args+=("--socket=$MYSQL_SOCKET") else args+=("--host=localhost") fi else args+=("--protocol=TCP" "--host=$MYSQL_HOST" "--port=$MYSQL_PORT") fi printf '%s\n' "${args[@]}" } mysql_exec() { local database="$1" shift local -a args=() while IFS= read -r arg; do args+=("$arg") done < <(mysql_base_args) MYSQL_PWD="$MYSQL_PASSWORD" "$(mysql_detect_binary mysql)" "${args[@]}" --database="$database" "$@" } mysql_exec_sql() { local database="$1" local sql="$2" mysql_exec "$database" -e "$sql" } mysqldump_exec() { local -a args=() while IFS= read -r arg; do args+=("$arg") done < <(mysql_base_args) MYSQL_PWD="$MYSQL_PASSWORD" "$(mysql_detect_binary mysqldump)" "${args[@]}" "$@" } mysql_alt_version() { mysql_load_alt_credentials mysql_exec mysql -Nse "SELECT VERSION()" 2>/dev/null || true } mysql_ensure_metadata_schema() { mysql_exec mysql -e " CREATE TABLE IF NOT EXISTS da_plugin_access_hosts ( da_user VARCHAR(255) NOT NULL, role_name VARCHAR(255) NOT NULL, host_pattern VARCHAR(255) NOT NULL, note VARCHAR(180) NOT NULL DEFAULT '', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (role_name, host_pattern), KEY access_hosts_da_user_idx (da_user) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS da_plugin_grant_profiles ( db_name VARCHAR(255) NOT NULL, role_name VARCHAR(255) NOT NULL, privileges TEXT NOT NULL, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (db_name, role_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS da_plugin_database_owners ( db_name VARCHAR(255) NOT NULL, da_user VARCHAR(255) NOT NULL, role_name VARCHAR(255) NOT NULL, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (db_name), KEY database_owners_da_user_idx (da_user), KEY database_owners_role_idx (role_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS da_plugin_sso_accounts ( role_name VARCHAR(255) NOT NULL, host_pattern VARCHAR(255) NOT NULL, expires_at INT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (role_name, host_pattern), KEY sso_accounts_expires_idx (expires_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS da_plugin_system_databases ( service_name VARCHAR(64) NOT NULL, db_name VARCHAR(255) NOT NULL, db_user VARCHAR(255) NOT NULL, endpoint_mode VARCHAR(32) NOT NULL DEFAULT 'tcp', config_path VARCHAR(512) NOT NULL DEFAULT '', enabled TINYINT(1) NOT NULL DEFAULT 1, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (service_name), KEY system_databases_db_idx (db_name), KEY system_databases_user_idx (db_user) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; " } mysql_service_name() { local service for service in mysqld mariadb mysql; do if systemctl list-unit-files "${service}.service" >/dev/null 2>&1; then printf '%s\n' "$service" return 0 fi done printf '%s\n' "mysqld" } mysql_safe_identifier() { local value="${1:-}" if [[ ! "$value" =~ ^[A-Za-z0-9_]+$ ]]; then echo "Nieprawidłowy identyfikator MySQL: $value" >&2 exit 1 fi printf '%s' "$value" } mysql_quote_identifier() { local value value="$(mysql_safe_identifier "$1")" printf '`%s`' "$value" } mysql_escape_literal() { printf "%s" "${1:-}" | sed "s/'/''/g" } mysql_non_system_databases() { mysql_exec mysql -Nse "SHOW DATABASES" | grep -Ev '^(information_schema|performance_schema|mysql|sys)$' || true } mysql_restore_stream() { local database="$1" local file="$2" if [[ "$file" == *.gz ]]; then gunzip -c "$file" | mysql_exec "$database" return ${PIPESTATUS[1]} fi mysql_exec "$database" < "$file" } RESTORE_ROLLBACK_FILE="" RESTORE_ROLLBACK_CREATED=0 cleanup_restore_rollback() { if [ -n "${RESTORE_ROLLBACK_FILE:-}" ] && [ -f "$RESTORE_ROLLBACK_FILE" ]; then rm -f "$RESTORE_ROLLBACK_FILE" fi RESTORE_ROLLBACK_FILE="" RESTORE_ROLLBACK_CREATED=0 } backup_alt_database_for_rollback() { local db="$1" cleanup_restore_rollback RESTORE_ROLLBACK_FILE="$(mktemp)" if [ -z "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$db")' LIMIT 1" 2>>"${LOG_FILE:-/dev/null}" || true)" ]; then cleanup_restore_rollback return 0 fi log "creating rollback dump: $db" if mysqldump_exec \ --single-transaction \ --quick \ --skip-lock-tables \ --routines \ --triggers \ --events \ --default-character-set=utf8mb4 \ "$db" > "$RESTORE_ROLLBACK_FILE" 2>>"${LOG_FILE:-/dev/null}"; then RESTORE_ROLLBACK_CREATED=1 return 0 fi cleanup_restore_rollback return 1 } restore_alt_database_from_rollback() { local db="$1" if [ "$RESTORE_ROLLBACK_CREATED" -ne 1 ] || [ -z "${RESTORE_ROLLBACK_FILE:-}" ] || [ ! -r "$RESTORE_ROLLBACK_FILE" ]; then return 0 fi log "restoring rollback dump: $db" mysql_exec mysql -e "DROP DATABASE IF EXISTS $(mysql_quote_identifier "$db");" >> "${LOG_FILE:-/dev/null}" 2>&1 || true mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$db") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" >> "${LOG_FILE:-/dev/null}" 2>&1 || true mysql_exec "$db" < "$RESTORE_ROLLBACK_FILE" >> "${LOG_FILE:-/dev/null}" 2>&1 || { log "ERROR: rollback restore failed for $db" return 1 } }