313 lines
8.4 KiB
Bash
313 lines
8.4 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"
|
|
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 "$@"
|