Files
alt-mysql/scripts/da-integration/alt_mysql_restore_payload.sh
T
2026-07-04 15:48:19 +02:00

294 lines
9.6 KiB
Bash

#!/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 "$@"