feat: import native backup/<dbname>.conf+.sql pairs directly into alt-mariadb

This commit is contained in:
Marek Miklewicz
2026-07-05 16:53:31 +02:00
parent d5040037d6
commit 6b7ec39025
2 changed files with 320 additions and 1 deletions
@@ -127,10 +127,188 @@ parse_native_db_conf() {
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/<db>.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() is added in Task 3.
main "$@"
fi