Import alt-mysql plugin

This commit is contained in:
Marek Miklewicz
2026-07-04 15:48:19 +02:00
commit d71fcf9ace
101 changed files with 18635 additions and 0 deletions
@@ -0,0 +1,293 @@
#!/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="$PLUGIN_DIR/plugin-settings.conf"
LOG_FILE="${DA_ALT_MYSQL_RESTORE_LOG:-$PLUGIN_DIR/data/logs/da-restore.log}"
DA_USER=""
PAYLOAD_DIR=""
OVERWRITE_POLICY=""
RESTORE_ROLLBACK_FILE=""
RESTORE_ROLLBACK_CREATED=0
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 restore payload: %s\n' "$*" >&2
exit 1
}
usage() {
cat >&2 <<'EOF'
Usage: alt_mysql_restore_payload.sh --user DA_USER --payload /path/to/backup/alt_mysql [--overwrite allow|deny]
EOF
exit 2
}
while [ "$#" -gt 0 ]; do
case "$1" in
--user) DA_USER="${2:-}"; shift 2 ;;
--payload) PAYLOAD_DIR="${2:-}"; shift 2 ;;
--overwrite) OVERWRITE_POLICY="${2:-}"; shift 2 ;;
*) usage ;;
esac
done
[ -n "$DA_USER" ] || usage
[ -n "$PAYLOAD_DIR" ] || usage
[[ "$DA_USER" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]] || die "unsafe DirectAdmin username: $DA_USER"
[ -d "$PAYLOAD_DIR" ] || die "payload directory does not exist: $PAYLOAD_DIR"
[ -r "$COMMON_FILE" ] || die "missing helper: $COMMON_FILE"
# shellcheck source=../setup/mysql_common.sh
source "$COMMON_FILE"
mysql_load_plugin_settings_file "$SETTINGS_FILE"
read_plugin_setting() {
local key="$1"
local default="$2"
local line value
[ -r "$SETTINGS_FILE" ] || {
printf '%s\n' "$default"
return 0
}
line="$(grep -E "^${key}=" "$SETTINGS_FILE" | tail -n 1 || true)"
[ -n "$line" ] || {
printf '%s\n' "$default"
return 0
}
value="${line#*=}"
value="${value%%#*}"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
value="$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
printf '%s\n' "${value:-$default}"
}
sha256_file() {
local file="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$file" | awk '{print $1}'
return 0
fi
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$file" | awk '{print $1}'
return 0
fi
die "missing sha256sum/shasum"
}
require_php_cli() {
command -v php >/dev/null 2>&1 || die "php CLI is required to parse manifest.json"
}
manifest_database_rows() {
local manifest="$1"
php -r '
$manifest = $argv[1];
$data = json_decode(file_get_contents($manifest), true);
if (!is_array($data) || ($data["format"] ?? "") !== "da-alt-mysql-v1") {
fwrite(STDERR, "invalid manifest\n");
exit(3);
}
foreach (($data["databases"] ?? []) as $db) {
$name = (string)($db["name"] ?? "");
$file = (string)($db["file"] ?? "");
$sha = (string)($db["sha256"] ?? "");
if ($name === "" || $file === "" || $sha === "") {
fwrite(STDERR, "invalid database entry\n");
exit(4);
}
echo $name, "\t", $file, "\t", $sha, "\n";
}
' "$manifest"
}
safe_database_name() {
local db="$1"
[[ "$db" =~ ^[A-Za-z0-9_]+$ ]] || die "unsafe database name in payload: $db"
printf '%s\n' "$db"
}
database_allowed_for_user() {
local db="$1"
[[ "$db" == "${DA_USER}_"* ]]
}
import_gzip_sql() {
local database="$1"
local file="$2"
if [[ "$file" == *.gz ]]; then
gunzip -c "$file" | mysql_exec "$database"
return "${PIPESTATUS[1]}"
fi
mysql_exec "$database" < "$file"
}
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" || 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"; 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" 2>&1 || true
mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$db") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" >> "$LOG_FILE" 2>&1 || true
mysql_exec "$db" < "$RESTORE_ROLLBACK_FILE" >> "$LOG_FILE" 2>&1 || {
log "ERROR: rollback restore failed for $db"
return 1
}
}
trap cleanup_restore_rollback INT TERM EXIT
metadata_cleanup_for_user() {
local escaped_user
escaped_user="$(mysql_escape_literal "$DA_USER")"
mysql_exec mysql -e "DELETE FROM da_plugin_access_hosts WHERE da_user='${escaped_user}' OR role_name LIKE '${escaped_user}\\_%';"
mysql_exec mysql -e "DELETE FROM da_plugin_grant_profiles WHERE db_name LIKE '${escaped_user}\\_%' OR role_name LIKE '${escaped_user}\\_%';"
mysql_exec mysql -e "DELETE FROM da_plugin_database_owners WHERE db_name LIKE '${escaped_user}\\_%' OR role_name LIKE '${escaped_user}\\_%';"
}
verify_database_exists() {
local db="$1"
local escaped_db
escaped_db="$(mysql_escape_literal "$db")"
[ -n "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='${escaped_db}' LIMIT 1")" ] \
|| die "database was not restored: $db"
}
sync_remote_hosts_if_enabled() {
local allow sync_script
allow="$(read_plugin_setting allow_remote_hosts 0)"
[ "$allow" = "1" ] || return 0
sync_script="$(read_plugin_setting MYSQL_PLUGIN_SUDO_SYNC_HOSTS /usr/local/directadmin/plugins/alt-mysql/scripts/setup/mysql_host_sync.sh)"
if [ -x "$sync_script" ]; then
"$sync_script" >> "$LOG_FILE" 2>&1 || log "WARNING: host sync failed after restore"
fi
}
main() {
local manifest users_file grants_file metadata_file db file rel_file expected_sha actual_sha overwrite
manifest="$PAYLOAD_DIR/manifest.json"
users_file="$PAYLOAD_DIR/grants/users.sql.gz"
grants_file="$PAYLOAD_DIR/grants/grants.sql.gz"
[ -r "$manifest" ] || die "missing manifest: $manifest"
[ -r "$users_file" ] || die "missing users payload: $users_file"
[ -r "$grants_file" ] || die "missing grants payload: $grants_file"
require_php_cli
mysql_load_alt_credentials
mysql_ensure_metadata_schema
overwrite="${OVERWRITE_POLICY:-$(read_plugin_setting DA_ALT_MYSQL_RESTORE_OVERWRITE allow)}"
case "$overwrite" in
allow|deny) ;;
prompt) overwrite="deny" ;;
*) overwrite="deny" ;;
esac
log "restore payload start: user=$DA_USER payload=$PAYLOAD_DIR overwrite=$overwrite"
manifest_database_rows "$manifest" >/dev/null
while IFS=$'\t' read -r db rel_file expected_sha; do
db="$(safe_database_name "$db")"
database_allowed_for_user "$db" || die "database $db does not match target user prefix $DA_USER"
file="$PAYLOAD_DIR/$rel_file"
[ -r "$file" ] || die "missing database dump: $file"
actual_sha="$(sha256_file "$file")"
[ "$actual_sha" = "$expected_sha" ] || die "checksum mismatch for $db"
if [ "$overwrite" = "deny" ] && [ -n "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$db")' LIMIT 1")" ]; then
die "target database exists and overwrite is denied: $db"
fi
done < <(manifest_database_rows "$manifest")
log "restoring user definitions"
import_gzip_sql mysql "$users_file" >> "$LOG_FILE" 2>&1 || die "cannot restore MySQL users from payload"
metadata_cleanup_for_user
while IFS=$'\t' read -r db rel_file expected_sha; do
db="$(safe_database_name "$db")"
file="$PAYLOAD_DIR/$rel_file"
log "restoring database: $db"
backup_alt_database_for_rollback "$db" || die "cannot create rollback dump before restoring database: $db"
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
die "cannot drop database before restore: $db"
fi
if ! mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$db") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" >> "$LOG_FILE" 2>&1; then
restore_alt_database_from_rollback "$db" || true
die "cannot create database before restore: $db"
fi
if ! import_gzip_sql "$db" "$file" >> "$LOG_FILE" 2>&1; then
restore_alt_database_from_rollback "$db" || true
die "cannot import database dump: $db"
fi
cleanup_restore_rollback
verify_database_exists "$db"
done < <(manifest_database_rows "$manifest")
for metadata_file in \
"$PAYLOAD_DIR/metadata/da_plugin_database_owners.sql.gz" \
"$PAYLOAD_DIR/metadata/da_plugin_grant_profiles.sql.gz" \
"$PAYLOAD_DIR/metadata/da_plugin_access_hosts.sql.gz"; do
if [ -r "$metadata_file" ]; then
log "restoring metadata: ${metadata_file##*/}"
import_gzip_sql mysql "$metadata_file" >> "$LOG_FILE" 2>&1 || die "cannot restore metadata: $metadata_file"
fi
done
log "restoring grants"
import_gzip_sql mysql "$grants_file" >> "$LOG_FILE" 2>&1 || die "cannot restore grants from payload"
mysql_exec mysql -e "FLUSH PRIVILEGES;" >> "$LOG_FILE" 2>&1 || true
sync_remote_hosts_if_enabled
log "restore payload completed: user=$DA_USER"
}
main "$@"
@@ -0,0 +1,312 @@
#!/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"
LOG_FILE="${DA_ALT_MYSQL_BACKUP_LOG:-$PLUGIN_DIR/data/logs/da-backup.log}"
DRY_RUN=0
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
esac
done
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 backup hook: %s\n' "$*" >&2
exit 1
}
[ -r "$COMMON_FILE" ] || die "missing helper: $COMMON_FILE"
# shellcheck source=../setup/mysql_common.sh
source "$COMMON_FILE"
mysql_load_plugin_settings_file "$PLUGIN_DIR/plugin-settings.conf"
json_escape() {
printf '%s' "${1:-}" | sed 's/\\/\\\\/g; s/"/\\"/g'
}
json_string() {
printf '"%s"' "$(json_escape "${1:-}")"
}
sha256_file() {
local file="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$file" | awk '{print $1}'
return 0
fi
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$file" | awk '{print $1}'
return 0
fi
die "missing sha256sum/shasum"
}
detect_da_user() {
local value="${DA_USER:-${username:-${user:-${USERNAME:-}}}}"
if [ -z "$value" ] && [ "${1:-}" != "--dry-run" ]; then
value="${1:-}"
fi
if [[ ! "$value" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]]; then
die "cannot detect safe DirectAdmin username"
fi
printf '%s\n' "$value"
}
candidate_dir_if_valid() {
local candidate="${1:-}"
[ -n "$candidate" ] || return 1
[ -d "$candidate" ] || return 1
printf '%s\n' "$(cd "$candidate" && pwd -P)"
}
candidate_backup_root_if_valid() {
local candidate="${1:-}"
local dir=""
if ! dir="$(candidate_dir_if_valid "$candidate")"; then
return 1
fi
if [ "$DRY_RUN" -eq 1 ] || [ -d "$dir/backup" ]; then
printf '%s\n' "$dir"
return 0
fi
return 1
}
detect_backup_root() {
local current candidate dir file_candidate
current="$(pwd -P)"
for candidate in \
"${DA_ALT_MYSQL_BACKUP_ROOT:-}" \
"${backup_path:-}" \
"${backupdir:-}" \
"${backup_dir:-}" \
"${tmpdir:-}" \
"$current"; do
if dir="$(candidate_backup_root_if_valid "$candidate")"; then
printf '%s\n' "$dir"
return 0
fi
done
for file_candidate in "${file:-}" "${filename:-}" "${backup_file:-}"; do
[ -n "$file_candidate" ] || continue
candidate="$(dirname "$file_candidate")"
if dir="$(candidate_backup_root_if_valid "$candidate")"; then
printf '%s\n' "$dir"
return 0
fi
done
die "cannot detect DirectAdmin backup assembly directory"
}
list_user_databases() {
local da_user="$1"
local escaped_user
escaped_user="$(mysql_escape_literal "$da_user")"
mysql_exec mysql -Nse "
SELECT SCHEMA_NAME
FROM information_schema.SCHEMATA
WHERE SCHEMA_NAME LIKE '${escaped_user}\\_%' ESCAPE '\\'
ORDER BY SCHEMA_NAME
" | grep -Ev '^(information_schema|performance_schema|mysql|sys)$' || true
}
list_user_roles() {
local da_user="$1"
local escaped_user
escaped_user="$(mysql_escape_literal "$da_user")"
mysql_exec mysql -Nse "
SELECT DISTINCT User
FROM mysql.user
WHERE User LIKE '${escaped_user}\\_%' ESCAPE '\\'
AND User <> ''
ORDER BY User
" || true
}
dump_users_file() {
local da_user="$1"
local output_file="$2"
local tmp_file user host q_user q_host create_sql
tmp_file="$(mktemp)"
{
printf '%s\n' "-- alt-mysql user definitions for $da_user"
printf '%s\n' "SET sql_log_bin=0;"
} > "$tmp_file"
while IFS=$'\t' read -r user host; do
[ -n "${user:-}" ] || continue
[ -n "${host:-}" ] || continue
q_user="$(mysql_escape_literal "$user")"
q_host="$(mysql_escape_literal "$host")"
create_sql="$(mysql_exec mysql -Nse "SHOW CREATE USER '${q_user}'@'${q_host}'" | awk -F'\t' '{print $NF}' || true)"
[ -n "$create_sql" ] || continue
printf "DROP USER IF EXISTS '%s'@'%s';\n" "$q_user" "$q_host" >> "$tmp_file"
printf '%s;\n' "$create_sql" >> "$tmp_file"
done < <(
mysql_exec mysql -Nse "
SELECT User, Host
FROM mysql.user
WHERE User LIKE '$(mysql_escape_literal "$da_user")\\_%' ESCAPE '\\'
AND User <> ''
ORDER BY User, Host
" || true
)
printf '%s\n' "SET sql_log_bin=1;" >> "$tmp_file"
gzip -9 < "$tmp_file" > "$output_file"
rm -f "$tmp_file"
}
dump_grants_file() {
local da_user="$1"
local output_file="$2"
local tmp_file user host q_user q_host grant_line
tmp_file="$(mktemp)"
printf '%s\n' "-- alt-mysql grants for $da_user" > "$tmp_file"
while IFS=$'\t' read -r user host; do
[ -n "${user:-}" ] || continue
[ -n "${host:-}" ] || continue
q_user="$(mysql_escape_literal "$user")"
q_host="$(mysql_escape_literal "$host")"
while IFS= read -r grant_line; do
[ -n "$grant_line" ] || continue
printf '%s;\n' "$grant_line" >> "$tmp_file"
done < <(mysql_exec mysql -Nse "SHOW GRANTS FOR '${q_user}'@'${q_host}'" || true)
printf '\n' >> "$tmp_file"
done < <(
mysql_exec mysql -Nse "
SELECT User, Host
FROM mysql.user
WHERE User LIKE '$(mysql_escape_literal "$da_user")\\_%' ESCAPE '\\'
AND User <> ''
ORDER BY User, Host
" || true
)
gzip -9 < "$tmp_file" > "$output_file"
rm -f "$tmp_file"
}
dump_metadata_table() {
local table="$1"
local where="$2"
local output_file="$3"
mysqldump_exec \
--single-transaction \
--skip-lock-tables \
--no-create-info \
--compact \
--where="$where" \
mysql "$table" | gzip -9 > "$output_file"
}
write_manifest() {
local da_user="$1"
local payload_dir="$2"
shift 2
local db_json="$1"
local manifest="$payload_dir/manifest.json"
local version
version="$(mysql_alt_version)"
mysql_load_alt_runtime
cat > "$manifest" <<JSON
{
"format": "da-alt-mysql-v1",
"created_at": "$(date -u '+%Y-%m-%dT%H:%M:%SZ')",
"da_user": $(json_string "$da_user"),
"mariadb_version": $(json_string "$version"),
"source": "alt-mariadb",
"source_instance": {
"service": $(json_string "$MYSQL_ALT_SERVICE_NAME"),
"basedir": $(json_string "$MYSQL_ALT_BASEDIR"),
"datadir": $(json_string "$MYSQL_ALT_DATADIR"),
"port": $(json_string "$MYSQL_ALT_PORT"),
"socket": $(json_string "$MYSQL_ALT_SOCKET")
},
"databases": [
$db_json
]
}
JSON
}
main() {
local da_user backup_root payload_dir db_dir grants_dir metadata_dir
local db db_file rel_file sha db_entries entry prefix where
da_user="$(detect_da_user "${1:-}")"
backup_root="$(detect_backup_root)"
payload_dir="$backup_root/backup/alt_mysql"
db_dir="$payload_dir/databases"
grants_dir="$payload_dir/grants"
metadata_dir="$payload_dir/metadata"
log "backup hook env: user=$da_user pwd=$(pwd -P) file=${file:-} filename=${filename:-} root=$backup_root"
mysql_load_alt_credentials
mysql_ensure_metadata_schema
if [ "$DRY_RUN" -eq 1 ]; then
printf 'Would write alt-mysql payload for %s into %s\n' "$da_user" "$payload_dir"
list_user_databases "$da_user"
return 0
fi
rm -rf "$payload_dir"
mkdir -p "$db_dir" "$grants_dir" "$metadata_dir"
db_entries=""
while IFS= read -r db; do
[ -n "$db" ] || continue
db_file="$db_dir/${db}.sql.gz"
mysqldump_exec \
--single-transaction \
--quick \
--skip-lock-tables \
--routines \
--triggers \
--events \
--default-character-set=utf8mb4 \
"$db" | gzip -9 > "$db_file"
rel_file="databases/${db}.sql.gz"
sha="$(sha256_file "$db_file")"
entry=" { \"name\": $(json_string "$db"), \"file\": $(json_string "$rel_file"), \"sha256\": $(json_string "$sha") }"
if [ -n "$db_entries" ]; then
db_entries+=$',\n'
fi
db_entries+="$entry"
log "added database dump: $db"
done < <(list_user_databases "$da_user")
dump_users_file "$da_user" "$grants_dir/users.sql.gz"
dump_grants_file "$da_user" "$grants_dir/grants.sql.gz"
prefix="$(mysql_escape_literal "$da_user")"
where="db_name LIKE '${prefix}\\_%'"
dump_metadata_table da_plugin_database_owners "$where" "$metadata_dir/da_plugin_database_owners.sql.gz"
dump_metadata_table da_plugin_grant_profiles "$where" "$metadata_dir/da_plugin_grant_profiles.sql.gz"
where="da_user = '${prefix}' OR role_name LIKE '${prefix}\\_%'"
dump_metadata_table da_plugin_access_hosts "$where" "$metadata_dir/da_plugin_access_hosts.sql.gz"
write_manifest "$da_user" "$payload_dir" "$db_entries"
chmod -R go-rwx "$payload_dir" 2>/dev/null || true
log "backup payload completed: $payload_dir"
}
main "$@"
@@ -0,0 +1,165 @@
#!/bin/bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
SETTINGS_FILE="$PLUGIN_DIR/plugin-settings.conf"
LOG_FILE="${DA_ALT_MYSQL_RESTORE_LOG:-$PLUGIN_DIR/data/logs/da-restore.log}"
RESTORE_PAYLOAD_SCRIPT="$SCRIPT_DIR/alt_mysql_restore_payload.sh"
NATIVE_MIGRATE_SCRIPT="$SCRIPT_DIR/da_restore_native_staging_to_alt_mysql.sh"
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 restore hook: %s\n' "$*" >&2
exit 1
}
read_plugin_setting() {
local key="$1"
local default="$2"
local line value
[ -r "$SETTINGS_FILE" ] || {
printf '%s\n' "$default"
return 0
}
line="$(grep -E "^${key}=" "$SETTINGS_FILE" | tail -n 1 || true)"
[ -n "$line" ] || {
printf '%s\n' "$default"
return 0
}
value="${line#*=}"
value="${value%%#*}"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
value="$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
printf '%s\n' "${value:-$default}"
}
detect_da_user() {
local value="${DA_USER:-${username:-${user:-${USERNAME:-}}}}"
if [ -z "$value" ]; then
value="${1:-}"
fi
if [[ ! "$value" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]]; then
die "cannot detect safe DirectAdmin username"
fi
printf '%s\n' "$value"
}
candidate_dir_if_valid() {
local candidate="${1:-}"
[ -n "$candidate" ] || return 1
[ -d "$candidate" ] || return 1
printf '%s\n' "$(cd "$candidate" && pwd -P)"
}
find_payload_in_dir() {
local root="$1"
local payload=""
[ -d "$root" ] || return 1
for payload in \
"$root/backup/alt_mysql" \
"$root/alt_mysql" \
"$root/mysql/backup/alt_mysql"; do
if [ -r "$payload/manifest.json" ]; then
printf '%s\n' "$payload"
return 0
fi
done
payload="$(find "$root" -maxdepth 5 -type f -path '*/backup/alt_mysql/manifest.json' -print -quit 2>/dev/null || true)"
if [ -n "$payload" ]; then
dirname "$payload"
return 0
fi
return 1
}
extract_payload_from_archive() {
local archive="$1"
local tmp_dir payload_member
[ -r "$archive" ] || return 1
command -v tar >/dev/null 2>&1 || return 1
payload_member="$(tar -tf "$archive" 2>/dev/null | grep -E '(^|/)backup/alt_mysql/manifest\.json$' | head -n 1 || true)"
[ -n "$payload_member" ] || return 1
tmp_dir="$(mktemp -d)"
tar -xf "$archive" -C "$tmp_dir" --wildcards '*/backup/alt_mysql/*' 'backup/alt_mysql/*' 2>/dev/null \
|| tar -xf "$archive" -C "$tmp_dir" 2>/dev/null
find_payload_in_dir "$tmp_dir"
}
detect_payload_dir() {
local current candidate dir archive payload
current="$(pwd -P)"
for candidate in \
"${DA_ALT_MYSQL_RESTORE_ROOT:-}" \
"${restore_path:-}" \
"${restore_dir:-}" \
"${tmpdir:-}" \
"${backup_path:-}" \
"$current"; do
if dir="$(candidate_dir_if_valid "$candidate")"; then
if payload="$(find_payload_in_dir "$dir")"; then
printf '%s\n' "$payload"
return 0
fi
fi
done
for archive in "${filename:-}" "${file:-}" "${backup_file:-}"; do
[ -n "$archive" ] || continue
if payload="$(extract_payload_from_archive "$archive")"; then
printf '%s\n' "$payload"
return 0
fi
done
return 1
}
main() {
local da_user enabled overwrite payload native_mode
da_user="$(detect_da_user "${1:-}")"
enabled="$(read_plugin_setting DA_ALT_MYSQL_RESTORE_ENABLED true)"
native_mode="$(read_plugin_setting DA_ALT_MYSQL_NATIVE_STAGING_MIGRATE true)"
log "restore hook env: user=$da_user pwd=$(pwd -P) file=${file:-} filename=${filename:-}"
case "$enabled" in
true|yes|on|1) ;;
*) log "restore hook disabled by DA_ALT_MYSQL_RESTORE_ENABLED=$enabled"; return 0 ;;
esac
overwrite="$(read_plugin_setting DA_ALT_MYSQL_RESTORE_OVERWRITE allow)"
if payload="$(detect_payload_dir)"; then
[ -x "$RESTORE_PAYLOAD_SCRIPT" ] || die "restore payload script is not executable: $RESTORE_PAYLOAD_SCRIPT"
log "plugin payload detected: $payload"
"$RESTORE_PAYLOAD_SCRIPT" --user "$da_user" --payload "$payload" --overwrite "$overwrite"
return 0
fi
log "plugin payload not found; considering native staging migration"
case "$native_mode" in
true|yes|on|1)
[ -x "$NATIVE_MIGRATE_SCRIPT" ] || die "native staging migration script is not executable: $NATIVE_MIGRATE_SCRIPT"
"$NATIVE_MIGRATE_SCRIPT" --user "$da_user"
;;
*)
log "native staging migration disabled; account restore continues without alt-mysql DB migration"
;;
esac
}
main "$@"
@@ -0,0 +1,68 @@
#!/bin/bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
SETTINGS_FILE="$PLUGIN_DIR/plugin-settings.conf"
LOG_FILE="${DA_ALT_MYSQL_DB_HOOK_LOG:-$PLUGIN_DIR/data/logs/da-db-hooks.log}"
HOOK_NAME="${DA_HOOK_NAME:-$(basename "$0")}"
mkdir -p "$(dirname "$LOG_FILE")"
read_plugin_setting() {
local key="$1"
local default="$2"
local line value
[ -r "$SETTINGS_FILE" ] || {
printf '%s\n' "$default"
return 0
}
line="$(grep -E "^${key}=" "$SETTINGS_FILE" | tail -n 1 || true)"
[ -n "$line" ] || {
printf '%s\n' "$default"
return 0
}
value="${line#*=}"
value="${value%%#*}"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
value="$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
printf '%s\n' "${value:-$default}"
}
log() {
printf '[%s] hook=%s username=%s database=%s user=%s %s\n' \
"$(date '+%Y-%m-%d %H:%M:%S')" \
"$HOOK_NAME" \
"${username:-}" \
"${database:-${name:-}}" \
"${user:-}" \
"$*" >> "$LOG_FILE"
}
enabled="$(read_plugin_setting DA_ALT_MYSQL_BLOCK_NATIVE_DB_HOOKS true)"
case "$enabled" in
true|yes|on|1) ;;
*) log "native DB hook blocker disabled"; exit 0 ;;
esac
if [ "${DA_ALT_MYSQL_ALLOW_NATIVE_DB_ACTION:-0}" = "1" ]; then
log "native DB action explicitly allowed by environment"
exit 0
fi
case "$HOOK_NAME" in
*_post.sh|*_post)
log "native DirectAdmin database action observed after completion"
exit 0
;;
esac
log "blocked native DirectAdmin database action"
cat >&2 <<'EOF'
Databases on this server are managed by the MySQL plugin and the alt-mariadb instance.
Use the MySQL plugin instead of DirectAdmin native database actions.
EOF
exit 1
@@ -0,0 +1,448 @@
#!/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="$PLUGIN_DIR/plugin-settings.conf"
LOG_FILE="${DA_ALT_MYSQL_RESTORE_LOG:-$PLUGIN_DIR/data/logs/da-restore.log}"
DA_USER=""
NATIVE_MY_CNF="${NATIVE_MY_CNF:-/usr/local/directadmin/conf/my.cnf}"
OVERWRITE_POLICY=""
CLEANUP_POLICY=""
RESTORE_ROLLBACK_FILE=""
RESTORE_ROLLBACK_CREATED=0
mkdir -p "$(dirname "$LOG_FILE")"
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE"
}
die() {
log "ERROR: $*"
printf 'native staging to alt-mysql: %s\n' "$*" >&2
exit 1
}
usage() {
cat >&2 <<'EOF'
Usage: da_restore_native_staging_to_alt_mysql.sh --user DA_USER [--native-my-cnf /path] [--overwrite allow|deny] [--cleanup after_success|keep]
EOF
exit 2
}
while [ "$#" -gt 0 ]; do
case "$1" in
--user) DA_USER="${2:-}"; shift 2 ;;
--native-my-cnf) NATIVE_MY_CNF="${2:-}"; shift 2 ;;
--overwrite) OVERWRITE_POLICY="${2:-}"; shift 2 ;;
--cleanup) CLEANUP_POLICY="${2:-}"; shift 2 ;;
*) usage ;;
esac
done
[ -n "$DA_USER" ] || usage
[[ "$DA_USER" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]*$ ]] || die "unsafe DirectAdmin username: $DA_USER"
[ -r "$COMMON_FILE" ] || die "missing helper: $COMMON_FILE"
# shellcheck source=../setup/mysql_common.sh
source "$COMMON_FILE"
mysql_load_plugin_settings_file "$SETTINGS_FILE"
read_plugin_setting() {
local key="$1"
local default="$2"
local line value
[ -r "$SETTINGS_FILE" ] || {
printf '%s\n' "$default"
return 0
}
line="$(grep -E "^${key}=" "$SETTINGS_FILE" | tail -n 1 || true)"
[ -n "$line" ] || {
printf '%s\n' "$default"
return 0
}
value="${line#*=}"
value="${value%%#*}"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
value="$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
printf '%s\n' "${value:-$default}"
}
native_mysql() {
"$NATIVE_MYSQL_BIN" --defaults-extra-file="$NATIVE_MY_CNF" "$@"
}
native_query() {
local database="$1"
local sql="$2"
native_mysql --batch --skip-column-names "$database" -e "$sql"
}
safe_identifier() {
local value="$1"
[[ "$value" =~ ^[A-Za-z0-9_]+$ ]] || die "unsafe MySQL identifier: $value"
printf '%s\n' "$value"
}
list_native_databases() {
native_query information_schema "
SELECT SCHEMA_NAME
FROM SCHEMATA
WHERE SCHEMA_NAME LIKE '$(mysql_escape_literal "$DA_USER")\\_%' ESCAPE '\\'
ORDER BY SCHEMA_NAME
" | grep -Ev '^(information_schema|performance_schema|mysql|sys)$' || true
}
list_native_user_hosts() {
native_query mysql "
SELECT User, Host
FROM user
WHERE User LIKE '$(mysql_escape_literal "$DA_USER")\\_%' ESCAPE '\\'
AND User <> ''
ORDER BY User, Host
" || true
}
list_native_roles_for_db() {
local db="$1"
native_query mysql "
SELECT DISTINCT User
FROM db
WHERE Db = '$(mysql_escape_literal "$db")'
AND User LIKE '$(mysql_escape_literal "$DA_USER")\\_%' ESCAPE '\\'
AND User <> ''
ORDER BY User
" || true
}
native_show_create_user() {
local user="$1"
local host="$2"
native_query mysql "SHOW CREATE USER '$(mysql_escape_literal "$user")'@'$(mysql_escape_literal "$host")'" | awk -F'\t' '{print $NF}'
}
native_show_grants() {
local user="$1"
local host="$2"
native_query mysql "SHOW GRANTS FOR '$(mysql_escape_literal "$user")'@'$(mysql_escape_literal "$host")'" || true
}
apply_user_definition_to_alt() {
local user="$1"
local host="$2"
local create_sql
create_sql="$(native_show_create_user "$user" "$host" || true)"
[ -n "$create_sql" ] || die "cannot recover password/authentication definition for ${user}@${host}"
{
printf "DROP USER IF EXISTS '%s'@'%s';\n" "$(mysql_escape_literal "$user")" "$(mysql_escape_literal "$host")"
printf '%s;\n' "$create_sql"
} | mysql_exec mysql >> "$LOG_FILE" 2>&1 || die "cannot recreate user with preserved password: ${user}@${host}"
}
apply_grants_to_alt() {
local user="$1"
local host="$2"
local grant_line
while IFS= read -r grant_line; do
[ -n "$grant_line" ] || continue
printf '%s;\n' "$grant_line" | mysql_exec mysql >> "$LOG_FILE" 2>&1 \
|| die "cannot apply grant for ${user}@${host}"
done < <(native_show_grants "$user" "$host")
}
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" || 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"; 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" 2>&1 || true
mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$db") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" >> "$LOG_FILE" 2>&1 || true
mysql_exec "$db" < "$RESTORE_ROLLBACK_FILE" >> "$LOG_FILE" 2>&1 || {
log "ERROR: rollback restore failed for $db"
return 1
}
}
trap cleanup_restore_rollback INT TERM EXIT
db_privilege_profile() {
local db="$1"
local role="$2"
local row privs=()
row="$(native_query mysql "
SELECT Select_priv, Insert_priv, Update_priv, Delete_priv, Create_priv, Drop_priv, Index_priv, Alter_priv,
Create_tmp_table_priv, Lock_tables_priv, Create_view_priv, Show_view_priv, Create_routine_priv,
Alter_routine_priv, Execute_priv, Event_priv, Trigger_priv
FROM db
WHERE Db='$(mysql_escape_literal "$db")'
AND User='$(mysql_escape_literal "$role")'
LIMIT 1
" | head -n 1 || true)"
if [ -z "$row" ]; then
printf 'ALL\n'
return 0
fi
IFS=$'\t' read -r select_p insert_p update_p delete_p create_p drop_p index_p alter_p tmp_p lock_p create_view_p show_view_p create_routine_p alter_routine_p execute_p event_p trigger_p <<< "$row"
[ "${select_p:-N}" = "Y" ] && privs+=("SELECT")
[ "${insert_p:-N}" = "Y" ] && privs+=("INSERT")
[ "${update_p:-N}" = "Y" ] && privs+=("UPDATE")
[ "${delete_p:-N}" = "Y" ] && privs+=("DELETE")
[ "${create_p:-N}" = "Y" ] && privs+=("CREATE")
[ "${drop_p:-N}" = "Y" ] && privs+=("DROP")
[ "${index_p:-N}" = "Y" ] && privs+=("INDEX")
[ "${alter_p:-N}" = "Y" ] && privs+=("ALTER")
[ "${tmp_p:-N}" = "Y" ] && privs+=("CREATE TEMPORARY TABLES")
[ "${lock_p:-N}" = "Y" ] && privs+=("LOCK TABLES")
[ "${create_view_p:-N}" = "Y" ] && privs+=("CREATE VIEW")
[ "${show_view_p:-N}" = "Y" ] && privs+=("SHOW VIEW")
[ "${create_routine_p:-N}" = "Y" ] && privs+=("CREATE ROUTINE")
[ "${alter_routine_p:-N}" = "Y" ] && privs+=("ALTER ROUTINE")
[ "${execute_p:-N}" = "Y" ] && privs+=("EXECUTE")
[ "${event_p:-N}" = "Y" ] && privs+=("EVENT")
[ "${trigger_p:-N}" = "Y" ] && privs+=("TRIGGER")
if [ "${#privs[@]}" -eq 0 ]; then
printf 'ALL\n'
else
local IFS=,
printf '%s\n' "${privs[*]}"
fi
}
metadata_cleanup_for_user() {
local escaped_user
escaped_user="$(mysql_escape_literal "$DA_USER")"
mysql_exec mysql -e "DELETE FROM da_plugin_access_hosts WHERE da_user='${escaped_user}' OR role_name LIKE '${escaped_user}\\_%';"
mysql_exec mysql -e "DELETE FROM da_plugin_grant_profiles WHERE db_name LIKE '${escaped_user}\\_%' OR role_name LIKE '${escaped_user}\\_%';"
mysql_exec mysql -e "DELETE FROM da_plugin_database_owners WHERE db_name LIKE '${escaped_user}\\_%' OR role_name LIKE '${escaped_user}\\_%';"
}
rebuild_metadata_for_db() {
local db="$1"
local owner="" role profile host
while IFS= read -r role; do
[ -n "$role" ] || continue
if [ -z "$owner" ]; then
owner="$role"
fi
profile="$(db_privilege_profile "$db" "$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 "$profile")')
ON DUPLICATE KEY UPDATE privileges=VALUES(privileges), updated_at=CURRENT_TIMESTAMP;
"
while IFS=$'\t' read -r _user host; do
[ -n "${host:-}" ] || continue
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")', 'migrated from native DirectAdmin restore')
ON DUPLICATE KEY UPDATE da_user=VALUES(da_user), note=VALUES(note), updated_at=CURRENT_TIMESTAMP;
"
done < <(list_native_user_hosts | awk -F'\t' -v role="$role" '$1 == role { print $0 }')
done < <(list_native_roles_for_db "$db")
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;
"
fi
}
verify_alt_database() {
local db="$1"
[ -n "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$db")' LIMIT 1")" ] \
|| die "alt database missing after import: $db"
}
drop_native_staging() {
local db user host remaining
log "cleanup native staging for $DA_USER"
while IFS= read -r db; do
[ -n "$db" ] || continue
native_query mysql "DROP DATABASE IF EXISTS $(mysql_quote_identifier "$db")" >> "$LOG_FILE" 2>&1 || die "cannot drop native staged database: $db"
done < <(list_native_databases)
while IFS=$'\t' read -r user host; do
[ -n "${user:-}" ] || continue
[ -n "${host:-}" ] || continue
remaining="$(native_query mysql "
SELECT COUNT(*)
FROM db
WHERE User='$(mysql_escape_literal "$user")'
AND Db NOT LIKE '$(mysql_escape_literal "$DA_USER")\\_%' ESCAPE '\\'
" | tail -n 1)"
if [ "${remaining:-0}" = "0" ]; then
native_query mysql "DROP USER IF EXISTS '$(mysql_escape_literal "$user")'@'$(mysql_escape_literal "$host")'" >> "$LOG_FILE" 2>&1 || true
else
log "keeping native user ${user}@${host}; it still has non-staging grants"
fi
done < <(list_native_user_hosts)
}
sync_remote_hosts_if_enabled() {
local allow sync_script
allow="$(read_plugin_setting allow_remote_hosts 0)"
[ "$allow" = "1" ] || return 0
sync_script="$(read_plugin_setting MYSQL_PLUGIN_SUDO_SYNC_HOSTS /usr/local/directadmin/plugins/alt-mysql/scripts/setup/mysql_host_sync.sh)"
if [ -x "$sync_script" ]; then
"$sync_script" >> "$LOG_FILE" 2>&1 || log "WARNING: host sync failed after native staging migration"
fi
}
main() {
local db tmp_dump user host overwrite cleanup native_count
[ -r "$NATIVE_MY_CNF" ] || die "native DirectAdmin MySQL client config is not readable: $NATIVE_MY_CNF"
NATIVE_MYSQL_BIN="$(mysql_native_binary mysql)" || die "native mysql/mariadb client not found"
NATIVE_MYSQLDUMP_BIN="$(mysql_native_binary mysqldump)" || die "native mysqldump/mariadb-dump client not found"
mysql_load_alt_credentials
mysql_ensure_metadata_schema
native_mysql mysql -e "SELECT 1" >/dev/null 2>&1 || die "native DirectAdmin MySQL staging is not reachable"
mysql_exec mysql -e "SELECT 1" >/dev/null 2>&1 || die "alt-mariadb is not reachable"
overwrite="${OVERWRITE_POLICY:-$(read_plugin_setting DA_ALT_MYSQL_RESTORE_OVERWRITE allow)}"
cleanup="${CLEANUP_POLICY:-$(read_plugin_setting DA_ALT_MYSQL_NATIVE_CLEANUP after_success)}"
case "$overwrite" in allow|deny) ;; prompt) overwrite="deny" ;; *) overwrite="deny" ;; esac
case "$cleanup" in after_success|keep) ;; *) cleanup="keep" ;; esac
native_count="$(list_native_databases | wc -l | tr -d '[:space:]')"
if [ "${native_count:-0}" = "0" ]; then
log "no native staged databases for $DA_USER"
return 0
fi
log "native staging migration start: user=$DA_USER db_count=$native_count overwrite=$overwrite cleanup=$cleanup"
if [ "$overwrite" = "deny" ]; then
while IFS= read -r db; do
[ -n "$db" ] || continue
if [ -n "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$db")' LIMIT 1")" ]; then
die "target database exists and overwrite is denied: $db"
fi
done < <(list_native_databases)
fi
while IFS=$'\t' read -r user host; do
[ -n "${user:-}" ] || continue
[ -n "${host:-}" ] || continue
log "preserving native user definition: ${user}@${host}"
apply_user_definition_to_alt "$user" "$host"
done < <(list_native_user_hosts)
metadata_cleanup_for_user
while IFS= read -r db; do
[ -n "$db" ] || continue
db="$(safe_identifier "$db")"
if [ "$overwrite" = "deny" ] && [ -n "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(mysql_escape_literal "$db")' LIMIT 1")" ]; then
die "target database exists and overwrite is denied: $db"
fi
tmp_dump="$(mktemp)"
log "dumping native staged database: $db"
"$NATIVE_MYSQLDUMP_BIN" --defaults-extra-file="$NATIVE_MY_CNF" \
--single-transaction \
--quick \
--skip-lock-tables \
--routines \
--triggers \
--events \
--default-character-set=utf8mb4 \
"$db" > "$tmp_dump" 2>>"$LOG_FILE" || {
rm -f "$tmp_dump"
die "cannot dump native staged database: $db"
}
log "importing database into alt-mariadb: $db"
backup_alt_database_for_rollback "$db" || {
rm -f "$tmp_dump"
die "cannot create rollback dump before importing native staged database: $db"
}
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
rm -f "$tmp_dump"
die "cannot drop target database before importing native staged database: $db"
fi
if ! mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$db") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" >> "$LOG_FILE" 2>&1; then
restore_alt_database_from_rollback "$db" || true
rm -f "$tmp_dump"
die "cannot create target database before importing native staged database: $db"
fi
mysql_exec "$db" < "$tmp_dump" >> "$LOG_FILE" 2>&1 || {
restore_alt_database_from_rollback "$db" || true
rm -f "$tmp_dump"
die "cannot import native staged database into alt-mariadb: $db"
}
rm -f "$tmp_dump"
cleanup_restore_rollback
verify_alt_database "$db"
rebuild_metadata_for_db "$db"
done < <(list_native_databases)
while IFS=$'\t' read -r user host; do
[ -n "${user:-}" ] || continue
[ -n "${host:-}" ] || continue
log "applying preserved grants: ${user}@${host}"
apply_grants_to_alt "$user" "$host"
done < <(list_native_user_hosts)
mysql_exec mysql -e "FLUSH PRIVILEGES;" >> "$LOG_FILE" 2>&1 || true
sync_remote_hosts_if_enabled
if [ "$cleanup" = "after_success" ]; then
drop_native_staging
else
log "native staging cleanup disabled by policy"
fi
log "native staging migration completed: user=$DA_USER"
}
main "$@"