From b6016bafd8b21c12ca87090926bebe048799e7b5 Mon Sep 17 00:00:00 2001 From: Marek Miklewicz Date: Sun, 5 Jul 2026 15:32:43 +0200 Subject: [PATCH] feat: parse DirectAdmin native backup/.conf format --- .../alt_mysql_native_backup_restore.sh | 122 ++++++++++++++++++ ..._mysql_native_backup_restore_parse_test.sh | 79 ++++++++++++ 2 files changed, 201 insertions(+) create mode 100755 scripts/da-integration/alt_mysql_native_backup_restore.sh create mode 100644 tests/alt_mysql_native_backup_restore_parse_test.sh diff --git a/scripts/da-integration/alt_mysql_native_backup_restore.sh b/scripts/da-integration/alt_mysql_native_backup_restore.sh new file mode 100755 index 0000000..d131962 --- /dev/null +++ b/scripts/da-integration/alt_mysql_native_backup_restore.sh @@ -0,0 +1,122 @@ +#!/bin/bash +set -Eeuo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$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" + return 1 + fi + if [ "${#NATIVE_DB_USER_NAMES[@]}" -eq 0 ]; then + log "no MySQL users found in $conf_file" + return 1 + fi + + return 0 +} + +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() is added in Task 3. +fi diff --git a/tests/alt_mysql_native_backup_restore_parse_test.sh b/tests/alt_mysql_native_backup_restore_parse_test.sh new file mode 100644 index 0000000..090e786 --- /dev/null +++ b/tests/alt_mysql_native_backup_restore_parse_test.sh @@ -0,0 +1,79 @@ +#!/bin/bash +set -euo pipefail + +PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)" +SCRIPT="$PLUGIN_DIR/scripts/da-integration/alt_mysql_native_backup_restore.sh" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +# Source only the parsing functions, without triggering main() (the script +# guards its own main() call behind a BASH_SOURCE check, see Task 3 Step 1). +# shellcheck source=/dev/null +source "$SCRIPT" + +# --- Real example content, copied verbatim from example_backup/psy/backup/psy_it301.conf --- +CONF_FILE="$TMP_DIR/psy_it301.conf" +cat > "$CONF_FILE" <<'EOF' +accesshosts=0=localhost +db_collation=DEFAULT_CHARACTER_SET_NAME=utf8mb3&DEFAULT_COLLATION_NAME=utf8mb3_general_ci&SCHEMA_NAME=psy_it301 +psy_it301=accesshosts=localhost&alter_priv=Y&alter_routine_priv=Y&create_priv=Y&create_routine_priv=Y&create_tmp_table_priv=Y&create_view_priv=Y&delete_priv=Y&drop_priv=Y&event_priv=Y&execute_priv=Y&index_priv=Y&insert_priv=Y&lock_tables_priv=Y&passwd=%2A8E2A6497279CF1B7F115E213B9904BD32703F4EF&plugin=mysql_native_password&references_priv=Y&select_priv=Y&show_view_priv=Y&trigger_priv=Y&update_priv=Y +EOF + +# url_decode +[ "$(url_decode '%2A8E2A6497279CF1B7F115E213B9904BD32703F4EF')" = "*8E2A6497279CF1B7F115E213B9904BD32703F4EF" ] \ + || fail "url_decode did not decode %2A correctly" + +# native_conf_field +QS="accesshosts=localhost&select_priv=Y&passwd=%2Aabc&plugin=mysql_native_password" +[ "$(native_conf_field "$QS" passwd)" = "%2Aabc" ] || fail "native_conf_field did not extract passwd" +[ "$(native_conf_field "$QS" plugin)" = "mysql_native_password" ] || fail "native_conf_field did not extract plugin" +[ "$(native_conf_field "$QS" missing_field)" = "" ] || fail "native_conf_field should return empty for a missing field" + +# native_conf_privilege_list +PRIVS="$(native_conf_privilege_list "$QS")" +[ "$PRIVS" = "SELECT" ] || fail "expected only SELECT for a single select_priv=Y field, got: $PRIVS" + +NO_PRIVS="$(native_conf_privilege_list "accesshosts=localhost&plugin=mysql_native_password")" +[ "$NO_PRIVS" = "ALL" ] || fail "expected ALL when no *_priv=Y flags are present, got: $NO_PRIVS" + +# parse_native_db_conf against the real example file +parse_native_db_conf "$CONF_FILE" || fail "parse_native_db_conf should succeed on a valid conf file" + +[ "$NATIVE_DB_CHARSET" = "utf8mb3" ] || fail "expected charset utf8mb3, got: $NATIVE_DB_CHARSET" +[ "$NATIVE_DB_COLLATION" = "utf8mb3_general_ci" ] || fail "expected collation utf8mb3_general_ci, got: $NATIVE_DB_COLLATION" +[ "${#NATIVE_DB_USER_NAMES[@]}" -eq 1 ] || fail "expected exactly 1 user, got: ${#NATIVE_DB_USER_NAMES[@]}" +[ "${NATIVE_DB_USER_NAMES[0]}" = "psy_it301" ] || fail "expected username psy_it301, got: ${NATIVE_DB_USER_NAMES[0]}" +[ "${NATIVE_DB_USER_HOST[0]}" = "localhost" ] || fail "expected host localhost, got: ${NATIVE_DB_USER_HOST[0]}" +[ "${NATIVE_DB_USER_HASH[0]}" = "*8E2A6497279CF1B7F115E213B9904BD32703F4EF" ] || fail "password hash not decoded correctly, got: ${NATIVE_DB_USER_HASH[0]}" +[ "${NATIVE_DB_USER_PLUGIN[0]}" = "mysql_native_password" ] || fail "expected mysql_native_password plugin, got: ${NATIVE_DB_USER_PLUGIN[0]}" +echo "${NATIVE_DB_USER_PRIVS[0]}" | grep -q "SELECT" || fail "expected SELECT in privilege list, got: ${NATIVE_DB_USER_PRIVS[0]}" + +# --- A conf file with two users on the same database --- +MULTI_CONF="$TMP_DIR/multi.conf" +cat > "$MULTI_CONF" <<'EOF' +accesshosts=0=localhost +db_collation=DEFAULT_CHARACTER_SET_NAME=utf8mb4&DEFAULT_COLLATION_NAME=utf8mb4_general_ci&SCHEMA_NAME=demo_multi +demo_owner=accesshosts=localhost&select_priv=Y&insert_priv=Y&passwd=%2Aaaa&plugin=mysql_native_password +demo_reader=accesshosts=localhost&select_priv=Y&passwd=%2Abbb&plugin=mysql_native_password +EOF + +parse_native_db_conf "$MULTI_CONF" || fail "parse_native_db_conf should succeed on a multi-user conf file" +[ "${#NATIVE_DB_USER_NAMES[@]}" -eq 2 ] || fail "expected 2 users in a multi-user conf, got: ${#NATIVE_DB_USER_NAMES[@]}" + +# --- A malformed conf file (no db_collation) must fail closed, not guess --- +BAD_CONF="$TMP_DIR/bad.conf" +cat > "$BAD_CONF" <<'EOF' +accesshosts=0=localhost +demo_user=select_priv=Y&passwd=%2Aaaa&plugin=mysql_native_password +EOF + +if parse_native_db_conf "$BAD_CONF" 2>/dev/null; then + fail "parse_native_db_conf must fail when db_collation is missing, not guess a default" +fi + +echo "alt_mysql_native_backup_restore_parse_test: OK"