#!/bin/bash set -Eeuo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" COMMON_FILE="$PLUGIN_DIR/scripts/setup/mysql_common.sh" SETTINGS_FILE="${DA_ALT_MYSQL_SETTINGS_FILE:-$PLUGIN_DIR/plugin-settings.conf}" LOG_FILE="${DA_ALT_MYSQL_RESTORE_LOG:-$PLUGIN_DIR/data/logs/da-restore.log}" mkdir -p "$(dirname "$LOG_FILE")" log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE" } die() { log "ERROR: $*" printf 'alt-mysql native backup restore: %s\n' "$*" >&2 exit 1 } url_decode() { local encoded="${1//+/ }" printf '%b' "${encoded//%/\\x}" } native_conf_field() { local qs="$1" field="$2" pair local -a pairs IFS='&' read -ra pairs <<< "$qs" for pair in "${pairs[@]}"; do if [ "${pair%%=*}" = "$field" ]; then printf '%s' "${pair#*=}" return 0 fi done return 0 } NATIVE_PRIV_FLAG_MAP=( "select_priv:SELECT" "insert_priv:INSERT" "update_priv:UPDATE" "delete_priv:DELETE" "create_priv:CREATE" "drop_priv:DROP" "index_priv:INDEX" "alter_priv:ALTER" "create_tmp_table_priv:CREATE TEMPORARY TABLES" "lock_tables_priv:LOCK TABLES" "create_view_priv:CREATE VIEW" "show_view_priv:SHOW VIEW" "create_routine_priv:CREATE ROUTINE" "alter_routine_priv:ALTER ROUTINE" "execute_priv:EXECUTE" "event_priv:EVENT" "trigger_priv:TRIGGER" "references_priv:REFERENCES" ) native_conf_privilege_list() { local qs="$1" local entry flag name value local -a privs=() for entry in "${NATIVE_PRIV_FLAG_MAP[@]}"; do flag="${entry%%:*}" name="${entry#*:}" value="$(native_conf_field "$qs" "$flag")" [ "$value" = "Y" ] && privs+=("$name") done if [ "${#privs[@]}" -eq 0 ]; then printf 'ALL' else local IFS=, printf '%s' "${privs[*]}" fi } parse_native_db_conf() { local conf_file="$1" local line key value NATIVE_DB_CHARSET="" NATIVE_DB_COLLATION="" NATIVE_DB_USER_NAMES=() NATIVE_DB_USER_HOST=() NATIVE_DB_USER_HASH=() NATIVE_DB_USER_PLUGIN=() NATIVE_DB_USER_PRIVS=() [ -r "$conf_file" ] || { log "cannot read conf file: $conf_file"; return 1; } while IFS= read -r line || [ -n "$line" ]; do [ -n "$line" ] || continue [[ "$line" == *=* ]] || continue key="${line%%=*}" value="${line#*=}" case "$key" in db_collation) NATIVE_DB_CHARSET="$(native_conf_field "$value" DEFAULT_CHARACTER_SET_NAME)" NATIVE_DB_COLLATION="$(native_conf_field "$value" DEFAULT_COLLATION_NAME)" ;; accesshosts) ;; *) NATIVE_DB_USER_NAMES+=("$key") NATIVE_DB_USER_HOST+=("$(native_conf_field "$value" accesshosts)") NATIVE_DB_USER_HASH+=("$(url_decode "$(native_conf_field "$value" passwd)")") NATIVE_DB_USER_PLUGIN+=("$(native_conf_field "$value" plugin)") NATIVE_DB_USER_PRIVS+=("$(native_conf_privilege_list "$value")") ;; esac done < "$conf_file" if [ -z "$NATIVE_DB_CHARSET" ]; then log "missing db_collation charset in $conf_file" NATIVE_DB_CHARSET="" NATIVE_DB_COLLATION="" NATIVE_DB_USER_NAMES=() NATIVE_DB_USER_HOST=() NATIVE_DB_USER_HASH=() NATIVE_DB_USER_PLUGIN=() NATIVE_DB_USER_PRIVS=() return 1 fi if [ "${#NATIVE_DB_USER_NAMES[@]}" -eq 0 ]; then log "no MySQL users found in $conf_file" NATIVE_DB_CHARSET="" NATIVE_DB_COLLATION="" NATIVE_DB_USER_NAMES=() NATIVE_DB_USER_HOST=() NATIVE_DB_USER_HASH=() NATIVE_DB_USER_PLUGIN=() NATIVE_DB_USER_PRIVS=() return 1 fi return 0 } mysql_import_sql_file() { local database="$1" file="$2" if [[ "$file" == *.gz ]]; then gunzip -c "$file" | mysql_exec "$database" return "${PIPESTATUS[1]}" fi mysql_exec "$database" < "$file" } verify_database_exists() { local db="$1" [ -n "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$db")' LIMIT 1")" ] \ || die "database was not restored: $db" } rebuild_metadata_for_native_import() { local db="$1" da_user="$2" local i owner="" role host for i in "${!NATIVE_DB_USER_NAMES[@]}"; do role="${NATIVE_DB_USER_NAMES[$i]}" host="${NATIVE_DB_USER_HOST[$i]:-localhost}" [ -n "$owner" ] || owner="$role" mysql_exec mysql -e " INSERT INTO da_plugin_grant_profiles (db_name, role_name, privileges) VALUES ('$(mysql_escape_literal "$db")', '$(mysql_escape_literal "$role")', '$(mysql_escape_literal "${NATIVE_DB_USER_PRIVS[$i]}")') ON DUPLICATE KEY UPDATE privileges=VALUES(privileges), updated_at=CURRENT_TIMESTAMP; " >> "$LOG_FILE" 2>&1 || true mysql_exec mysql -e " INSERT INTO da_plugin_access_hosts (da_user, role_name, host_pattern, note) VALUES ('$(mysql_escape_literal "$da_user")', '$(mysql_escape_literal "$role")', '$(mysql_escape_literal "$host")', 'restored from native backup/.conf') ON DUPLICATE KEY UPDATE da_user=VALUES(da_user), note=VALUES(note), updated_at=CURRENT_TIMESTAMP; " >> "$LOG_FILE" 2>&1 || true done if [ -n "$owner" ]; then mysql_exec mysql -e " INSERT INTO da_plugin_database_owners (db_name, da_user, role_name) VALUES ('$(mysql_escape_literal "$db")', '$(mysql_escape_literal "$da_user")', '$(mysql_escape_literal "$owner")') ON DUPLICATE KEY UPDATE da_user=VALUES(da_user), role_name=VALUES(role_name), updated_at=CURRENT_TIMESTAMP; " >> "$LOG_FILE" 2>&1 || true fi } import_native_database_from_conf() { local db="$1" conf_file="$2" sql_file="$3" da_user="$4" overwrite="$5" parse_native_db_conf "$conf_file" || { log "SKIP $db: conf did not parse"; return 1; } local collation="${NATIVE_DB_COLLATION:-}" if [ -z "$collation" ]; then log "SKIP $db: no collation parsed" return 1 fi local i for i in "${!NATIVE_DB_USER_PLUGIN[@]}"; do if [ "${NATIVE_DB_USER_PLUGIN[$i]}" != "mysql_native_password" ]; then log "SKIP $db: unsupported auth plugin '${NATIVE_DB_USER_PLUGIN[$i]}' for user ${NATIVE_DB_USER_NAMES[$i]}" return 1 fi done if [ "$overwrite" = "deny" ] && [ -n "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$db")' LIMIT 1")" ]; then log "SKIP $db: already exists and overwrite is denied" return 1 fi backup_alt_database_for_rollback "$db" || { log "SKIP $db: could not create rollback dump"; return 1; } if ! mysql_exec mysql -e "DROP DATABASE IF EXISTS $(mysql_quote_identifier "$db");" >> "$LOG_FILE" 2>&1; then restore_alt_database_from_rollback "$db" || true log "SKIP $db: could not drop database before import" return 1 fi if ! mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$db") CHARACTER SET $(mysql_safe_identifier "$NATIVE_DB_CHARSET") COLLATE $(mysql_safe_identifier "$collation");" >> "$LOG_FILE" 2>&1; then restore_alt_database_from_rollback "$db" || true log "SKIP $db: could not create database before import" return 1 fi local user host hash for i in "${!NATIVE_DB_USER_NAMES[@]}"; do user="${NATIVE_DB_USER_NAMES[$i]}" host="${NATIVE_DB_USER_HOST[$i]:-localhost}" hash="${NATIVE_DB_USER_HASH[$i]}" if ! { printf "DROP USER IF EXISTS '%s'@'%s';\n" "$(mysql_escape_literal "$user")" "$(mysql_escape_literal "$host")" printf "CREATE USER '%s'@'%s' IDENTIFIED WITH mysql_native_password AS '%s';\n" \ "$(mysql_escape_literal "$user")" "$(mysql_escape_literal "$host")" "$(mysql_escape_literal "$hash")" printf "GRANT %s ON %s.* TO '%s'@'%s';\n" \ "${NATIVE_DB_USER_PRIVS[$i]}" "$(mysql_quote_identifier "$db")" "$(mysql_escape_literal "$user")" "$(mysql_escape_literal "$host")" } | mysql_exec mysql >> "$LOG_FILE" 2>&1; then restore_alt_database_from_rollback "$db" || true log "SKIP $db: could not recreate user ${user}@${host}" return 1 fi done if ! mysql_import_sql_file "$db" "$sql_file" >> "$LOG_FILE" 2>&1; then restore_alt_database_from_rollback "$db" || true log "SKIP $db: could not import dump" return 1 fi cleanup_restore_rollback verify_database_exists "$db" rebuild_metadata_for_native_import "$db" "$da_user" mysql_exec mysql -e "FLUSH PRIVILEGES;" >> "$LOG_FILE" 2>&1 || true return 0 } find_native_pairs_in_dir() { local root="$1" prefix="$2" local conf base sql while IFS= read -r conf; do [ -n "$conf" ] || continue base="$(basename "$conf" .conf)" sql="$(dirname "$conf")/${base}.sql" [ -r "$sql" ] || continue printf '%s\t%s\t%s\n' "$base" "$conf" "$sql" done < <(find "$root" -maxdepth 4 -type f -name "${prefix}_*.conf" 2>/dev/null | sort) } main() { local da_user="" root="" overwrite="allow" while [ "$#" -gt 0 ]; do case "$1" in --user) da_user="${2:-}"; shift 2 ;; --root) root="${2:-}"; shift 2 ;; --overwrite) overwrite="${2:-}"; shift 2 ;; *) die "unknown argument: $1" ;; esac done [ -n "$da_user" ] || die "--user is required" [ -n "$root" ] || die "--root is required" [[ "$da_user" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]] || die "unsafe DirectAdmin username: $da_user" [ -d "$root" ] || die "backup root directory does not exist: $root" mysql_load_alt_credentials mysql_ensure_metadata_schema local -a handled=() local base conf sql while IFS=$'\t' read -r base conf sql; do [ -n "$base" ] || continue handled+=("$base") if [[ "$base" != "${da_user}_"* ]]; then printf 'SKIPPED %s wrong-user-prefix\n' "$base" continue fi if import_native_database_from_conf "$base" "$conf" "$sql" "$da_user" "$overwrite"; then printf 'IMPORTED %s\n' "$base" else printf 'SKIPPED %s import-failed\n' "$base" fi done < <(find_native_pairs_in_dir "$root" "$da_user") # Any *.sql belonging to this user's prefix with no matching .conf was not # covered by the loop above (find_native_pairs_in_dir only yields pairs # that have both files) - report those explicitly so the caller knows to # fall back to native-staging-migrate for them too. local sql_base while IFS= read -r sql; do [ -n "$sql" ] || continue sql_base="$(basename "$sql" .sql)" [[ "$sql_base" == "${da_user}_"* ]] || continue local already=0 h for h in "${handled[@]}"; do [ "$h" = "$sql_base" ] && { already=1; break; } done [ "$already" -eq 1 ] || printf 'SKIPPED %s no-matching-conf\n' "$sql_base" done < <(find "$root" -maxdepth 4 -type f -name "${da_user}_*.sql" 2>/dev/null | sort) } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then [ -r "$COMMON_FILE" ] || die "missing helper: $COMMON_FILE" # shellcheck source=../setup/mysql_common.sh source "$COMMON_FILE" mysql_load_plugin_settings_file "$SETTINGS_FILE" main "$@" fi