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,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 "$@"