#!/bin/bash set -Eeuo pipefail ERROR_MSG="" RESTORE_OK=0 DA_USER="" DB_NAME="" JOB_ID="" LOG_FILE="" RESTORE_MODE="" SOURCE_DB_NAME="" SOURCE_FILE="" RESTORE_ROLLBACK_FILE="" RESTORE_ROLLBACK_CREATED=0 JOB_FILE="${1:-}" if [ -z "$JOB_FILE" ] || [ ! -f "$JOB_FILE" ]; then echo "mysql: missing job file" >&2 exit 1 fi PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)" CONFIG_FILE="${PLUGIN_DIR}/plugin-settings.conf" LOG_DIR="${PLUGIN_DIR}/data/logs" COMMON_FILE="${PLUGIN_DIR}/scripts/setup/mysql_common.sh" mkdir -p "$LOG_DIR" urlencode() { local s="$1" local out="" local i c for ((i=0; i<${#s}; i++)); do c="${s:i:1}" case "$c" in [a-zA-Z0-9.~_-]) out+="$c" ;; ' ') out+='%20' ;; *) printf -v c '%%%02X' "'$c"; out+="$c" ;; esac done printf '%s' "$out" } notify_user() { local code="$1" if [ -z "${DA_USER:-}" ]; then return 0 fi local subject message if [ "$code" -eq 0 ] && [ "$RESTORE_OK" -eq 1 ]; then subject="Przywracanie bazy MySQL zakończone pomyślnie" message="Przywracanie bazy ${DB_NAME} z pliku ${SOURCE_FILE:-nieznany} zakończone." else subject="Błąd przywracania bazy MySQL" if [ -n "$ERROR_MSG" ]; then message="Przywracanie bazy ${DB_NAME} nie powiodło się: ${ERROR_MSG}" else message="Przywracanie bazy ${DB_NAME} nie powiodło się. Sprawdź log zadania." fi fi local task_queue="/usr/local/directadmin/data/task.queue" local users="select1%3D$(urlencode "$DA_USER")" local line="action=notify&value=users&subject=$(urlencode "$subject")&message=$(urlencode "$message")&users=${users}" printf "%s\n" "$line" >> "$task_queue" } fail() { local msg="$1" ERROR_MSG="$msg" if [ -n "${LOG_FILE:-}" ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: ${msg}" >> "$LOG_FILE" fi } log_unexpected_error() { local line_no="$1" local command="$2" local code="$3" if [ -z "${LOG_FILE:-}" ]; then return 0 fi if [ -n "${ERROR_MSG:-}" ]; then return 0 fi ERROR_MSG="Nieoczekiwany błąd wykonywania przywracania (kod ${code}, linia ${line_no})." echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: ${ERROR_MSG} CMD: ${command}" >> "$LOG_FILE" } load_plugin_settings() { if [ ! -f "$CONFIG_FILE" ]; then fail "Brak pliku konfiguracyjnego pluginu: $CONFIG_FILE" exit 1 fi # shellcheck disable=SC1090 source "$CONFIG_FILE" } sanitize_mysql_dump() { awk ' function lower(s) { return tolower(s) } { line = $0 l = lower(line) if (l ~ /^[[:space:]]*create[[:space:]]+database[[:space:]]+/) { next } if (l ~ /^[[:space:]]*drop[[:space:]]+database[[:space:]]+/) { next } if (l ~ /^[[:space:]]*use[[:space:]]+[`"]/ ) { next } if (l ~ /^[[:space:]]*use[[:space:]]+[a-z0-9_]+[[:space:]]*;/) { next } print line } ' } preclean_database() { local db_name="$1" local current_charset="" local current_collation="" current_charset="$(mysql_exec mysql -Nse "SELECT DEFAULT_CHARACTER_SET_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '${db_name}'" 2>>"$LOG_FILE" || true)" current_collation="$(mysql_exec mysql -Nse "SELECT DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '${db_name}'" 2>>"$LOG_FILE" || true)" [ -n "$current_charset" ] || current_charset="utf8mb4" [ -n "$current_collation" ] || current_collation="utf8mb4_unicode_ci" echo "[$(date '+%Y-%m-%d %H:%M:%S')] Recreating database ${db_name} before restore" >> "$LOG_FILE" mysql_exec mysql -e "DROP DATABASE IF EXISTS $(mysql_quote_identifier "$db_name");" >> "$LOG_FILE" 2>&1 mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$db_name") CHARACTER SET ${current_charset} COLLATE ${current_collation};" >> "$LOG_FILE" 2>&1 } cleanup_restore_rollback() { if [ -n "${RESTORE_ROLLBACK_FILE:-}" ] && [ -f "$RESTORE_ROLLBACK_FILE" ]; then rm -f "$RESTORE_ROLLBACK_FILE" fi } backup_alt_database_for_rollback() { local db_name="$1" RESTORE_ROLLBACK_FILE="$(mktemp)" RESTORE_ROLLBACK_CREATED=0 if [ -z "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '$(mysql_escape_literal "$db_name")' LIMIT 1" 2>>"$LOG_FILE" || true)" ]; then rm -f "$RESTORE_ROLLBACK_FILE" RESTORE_ROLLBACK_FILE="" return 0 fi echo "[$(date '+%Y-%m-%d %H:%M:%S')] Creating rollback dump for ${db_name}" >> "$LOG_FILE" if mysqldump_exec \ --single-transaction \ --quick \ --skip-lock-tables \ --routines \ --triggers \ --events \ --default-character-set=utf8mb4 \ "$db_name" > "$RESTORE_ROLLBACK_FILE" 2>>"$LOG_FILE"; then RESTORE_ROLLBACK_CREATED=1 return 0 fi rm -f "$RESTORE_ROLLBACK_FILE" RESTORE_ROLLBACK_FILE="" return 1 } restore_alt_database_from_rollback() { local db_name="$1" if [ "$RESTORE_ROLLBACK_CREATED" -ne 1 ] || [ -z "${RESTORE_ROLLBACK_FILE:-}" ] || [ ! -r "$RESTORE_ROLLBACK_FILE" ]; then return 0 fi echo "[$(date '+%Y-%m-%d %H:%M:%S')] Restoring rollback dump for ${db_name}" >> "$LOG_FILE" mysql_exec mysql -e "DROP DATABASE IF EXISTS $(mysql_quote_identifier "$db_name");" >> "$LOG_FILE" 2>&1 || true mysql_exec mysql -e "CREATE DATABASE $(mysql_quote_identifier "$db_name") CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" >> "$LOG_FILE" 2>&1 || true mysql_exec "$db_name" < "$RESTORE_ROLLBACK_FILE" >> "$LOG_FILE" 2>&1 || { echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: rollback restore failed for ${db_name}" >> "$LOG_FILE" return 1 } } quote_sh_value() { local value="$1" value="${value//\'/\'\\\'\'}" printf "'%s'" "$value" } set_job_var() { local key="$1" local value="$2" local tmp local owner_group="" if [ -f "$JOB_FILE" ]; then owner_group="$(stat -c '%u:%g' "$JOB_FILE" 2>/dev/null || stat -f '%u:%g' "$JOB_FILE" 2>/dev/null || true)" fi tmp="$(mktemp)" grep -v "^${key}=" "$JOB_FILE" > "$tmp" 2>/dev/null || true printf "%s=%s\n" "$key" "$(quote_sh_value "$value")" >> "$tmp" mv "$tmp" "$JOB_FILE" chmod 600 "$JOB_FILE" 2>/dev/null || true if [ "$(id -u)" -eq 0 ] && [ -n "$owner_group" ]; then chown "$owner_group" "$JOB_FILE" 2>/dev/null || true fi } on_exit() { local code="$1" if [ "$code" -ne 0 ] && [ -z "${ERROR_MSG:-}" ] && [ -n "${LOG_FILE:-}" ]; then ERROR_MSG="Zadanie przywracania zakończyło się błędem (kod ${code}) bez zarejestrowanego szczegółu." echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: ${ERROR_MSG}" >> "$LOG_FILE" fi notify_user "$code" } trap 'log_unexpected_error "$LINENO" "$BASH_COMMAND" "$?"' ERR trap cleanup_restore_rollback INT TERM trap 'code=$?; cleanup_restore_rollback; on_exit "$code"' EXIT [ -f "$COMMON_FILE" ] || { echo "Brak helpera MySQL: $COMMON_FILE" >&2 exit 1 } # shellcheck disable=SC1090 source "$COMMON_FILE" # shellcheck disable=SC1090 source "$JOB_FILE" JOB_ID="${JOB_ID:-$(basename "$JOB_FILE" .env)}" DA_USER="${DA_USER:-}" DB_NAME="${DB_NAME:-}" RESTORE_MODE="${RESTORE_MODE:-overwrite}" SOURCE_DB_NAME="${SOURCE_DB_NAME:-}" SOURCE_FILE="${SOURCE_FILE:-}" LOG_FILE="${LOG_DIR}/${JOB_ID}.log" echo "[$(date '+%Y-%m-%d %H:%M:%S')] Start restore job ${JOB_ID}" > "$LOG_FILE" if [ -z "$DA_USER" ] || [ -z "$DB_NAME" ] || [ -z "$SOURCE_FILE" ]; then fail "Brak DA_USER, DB_NAME lub SOURCE_FILE w pliku zadania." exit 1 fi if [[ "$DB_NAME" != "${DA_USER}_"* ]]; then fail "Baza ${DB_NAME} nie należy do użytkownika ${DA_USER}." exit 1 fi if [ ! -r "$SOURCE_FILE" ]; then fail "Nie można odczytać pliku źródłowego: ${SOURCE_FILE}" exit 1 fi load_plugin_settings mysql_load_alt_credentials MYSQL_BIN="$(mysql_alt_binary mysql)" || { fail "Brak binarium mysql." exit 1 } if [ -z "$(mysql_exec mysql -Nse "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '${DB_NAME}'" 2>>"$LOG_FILE" || true)" ]; then fail "Docelowa baza danych nie istnieje: ${DB_NAME}" exit 1 fi if ! backup_alt_database_for_rollback "$DB_NAME"; then fail "Nie udało się utworzyć kopii rollback przed przywracaniem bazy ${DB_NAME}." exit 1 fi if ! preclean_database "$DB_NAME"; then restore_alt_database_from_rollback "$DB_NAME" || true fail "Nie udało się przygotować bazy ${DB_NAME} do importu; przywrócono kopię rollback, jeśli była dostępna." exit 1 fi echo "[$(date '+%Y-%m-%d %H:%M:%S')] Restore source file: ${SOURCE_FILE}" >> "$LOG_FILE" if [ -n "$SOURCE_DB_NAME" ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] Restore source DB label: ${SOURCE_DB_NAME}" >> "$LOG_FILE" fi echo "[$(date '+%Y-%m-%d %H:%M:%S')] Restore target DB: ${DB_NAME}" >> "$LOG_FILE" set +e if [[ "$SOURCE_FILE" == *.gz ]]; then gunzip -c "$SOURCE_FILE" | sanitize_mysql_dump | mysql_exec "$DB_NAME" >> "$LOG_FILE" 2>&1 RC=${PIPESTATUS[2]} else sanitize_mysql_dump < "$SOURCE_FILE" | mysql_exec "$DB_NAME" >> "$LOG_FILE" 2>&1 RC=${PIPESTATUS[1]} fi set -e if [ "$RC" -ne 0 ]; then restore_alt_database_from_rollback "$DB_NAME" || true fail "Import pliku ${SOURCE_FILE} do bazy ${DB_NAME} zakończył się błędem." exit 1 fi set_job_var "END_TS" "$(date +%s)" echo "[$(date '+%Y-%m-%d %H:%M:%S')] Restore completed successfully." >> "$LOG_FILE" RESTORE_OK=1 exit 0