#!/usr/bin/env bash set -euo pipefail WORK_DIR="" while [[ $# -gt 0 ]]; do case "$1" in --work-dir) WORK_DIR="${2:-}" shift 2 ;; *) echo "[ERROR] unsupported argument: $1" >&2 exit 1 ;; esac done if [[ -z "${WORK_DIR}" ]]; then echo "[ERROR] --work-dir is required" >&2 exit 1 fi PAYLOAD_DIR="${WORK_DIR}/payload" STATUS_FILE="${WORK_DIR}/status.json" LOG_FILE="${WORK_DIR}/restore.log" MANIFEST="${PAYLOAD_DIR}/payload-manifest.json" FILES_PLAN="${WORK_DIR}/restore-files.tsv" DATABASES_PLAN="${WORK_DIR}/restore-databases.tsv" MQ_PLAN="${WORK_DIR}/restore-mq.tsv" ENV_FILE="${WORK_DIR}/restore-env.sh" json_escape() { local value="$1" value="${value//\\/\\\\}" value="${value//\"/\\\"}" value="${value//$'\n'/ }" printf '%s' "${value}" } write_status() { local status="$1" local phase="$2" local message="$3" printf '{"status":"%s","phase":"%s","message":"%s","updatedAt":"%s"}\n' \ "$(json_escape "${status}")" \ "$(json_escape "${phase}")" \ "$(json_escape "${message}")" \ "$(date '+%Y-%m-%dT%H:%M:%S%z')" > "${STATUS_FILE}" } log() { printf '[%s] %s\n' "$(date '+%F %T')" "$1" >> "${LOG_FILE}" } fail() { local message="$1" local phase="${2:-FAILED}" log "[ERROR] ${message}" write_status "FAILED" "${phase}" "${message}" exit 1 } require_env() { local name="$1" local value="${!name:-}" if [[ -z "${value}" ]]; then fail "required environment variable ${name} is empty" "${2:-PRECHECK}" fi } restore_files() { [[ -f "${FILES_PLAN}" ]] || return 0 while IFS=$'\t' read -r entry_path restore_path || [[ -n "${entry_path:-}" ]]; do [[ -n "${entry_path:-}" ]] || continue [[ -n "${restore_path:-}" ]] || fail "restore file target is empty for ${entry_path}" "FILE" local source_file="${PAYLOAD_DIR}/${entry_path}" [[ -f "${source_file}" ]] || fail "restore source file not found: ${entry_path}" "FILE" mkdir -p "$(dirname "${restore_path}")" cp -f "${source_file}" "${restore_path}" log "restored file: ${restore_path}" done < "${FILES_PLAN}" } restore_databases() { [[ -f "${DATABASES_PLAN}" ]] || return 0 [[ -s "${DATABASES_PLAN}" ]] || return 0 local script="${DB_RESTORE_SCRIPT_PATH:-}" [[ -n "${script}" && -x "${script}" ]] || fail "DB_RESTORE_SCRIPT_PATH is not executable" "DATABASE" require_env "DB_HOST" "DATABASE" require_env "DB_PORT" "DATABASE" require_env "DB_USER" "DATABASE" local mysql_path="${MYSQL_PATH:-mysql}" local db_password="${DB_PASSWORD:-}" while IFS=$'\t' read -r entry_path db_name || [[ -n "${entry_path:-}" ]]; do [[ -n "${entry_path:-}" ]] || continue [[ -n "${db_name:-}" ]] || fail "database name is empty for ${entry_path}" "DATABASE" local sql_file="${PAYLOAD_DIR}/${entry_path}" [[ -f "${sql_file}" ]] || fail "sql file not found: ${entry_path}" "DATABASE" "${script}" \ --mysql-path "${mysql_path}" \ --host "${DB_HOST}" \ --port "${DB_PORT}" \ --username "${DB_USER}" \ --password "${db_password}" \ --db-name "${db_name}" \ --sql-file "${sql_file}" >> "${LOG_FILE}" 2>&1 log "restored database: ${db_name}" done < "${DATABASES_PLAN}" } replay_mq() { [[ -f "${MQ_PLAN}" ]] || return 0 [[ -s "${MQ_PLAN}" ]] || return 0 local script="${MQ_REPLAY_SCRIPT_PATH:-}" [[ -n "${script}" && -x "${script}" ]] || fail "MQ_REPLAY_SCRIPT_PATH is not executable" "MQ_REPLAY" while IFS= read -r entry_path || [[ -n "${entry_path:-}" ]]; do [[ -n "${entry_path:-}" ]] || continue local context_file="${PAYLOAD_DIR}/${entry_path}" [[ -f "${context_file}" ]] || fail "mq replay context not found: ${entry_path}" "MQ_REPLAY" "${script}" --context-file "${context_file}" --work-dir "${WORK_DIR}" >> "${LOG_FILE}" 2>&1 log "mq replay applied" done < "${MQ_PLAN}" } health_check() { local url="${HEALTH_CHECK_URL:-}" [[ -n "${url}" ]] || { log "health check skipped: HEALTH_CHECK_URL is empty" return 0 } command -v curl >/dev/null 2>&1 || fail "curl command not found for health check" "HEALTH_CHECK" local timeout="${HEALTH_CHECK_TIMEOUT_SECONDS:-5}" local response response="$(curl -fsS --max-time "${timeout}" "${url}")" || fail "health check request failed" "HEALTH_CHECK" if [[ "${response}" != *"UP"* ]]; then fail "health check did not return UP" "HEALTH_CHECK" fi log "health check passed" } mkdir -p "${WORK_DIR}" touch "${LOG_FILE}" chmod 700 "${WORK_DIR}" || true if [[ -f "${ENV_FILE}" ]]; then # shellcheck disable=SC1090 source "${ENV_FILE}" fi [[ -f "${MANIFEST}" ]] || fail "payload manifest not found" "PRECHECK" write_status "RUNNING" "SERVICE_STOP" "stopping services" log "SERVICE_STOP" if [[ -n "${TMS_SCRIPT_PATH:-}" && -x "${TMS_SCRIPT_PATH}" ]]; then "${TMS_SCRIPT_PATH}" stop >> "${LOG_FILE}" 2>&1 || true fi write_status "RUNNING" "FILE" "restoring files" log "FILE" restore_files write_status "RUNNING" "DATABASE" "restoring databases" log "DATABASE" restore_databases write_status "RUNNING" "MQ_REPLAY" "replaying mq configuration" log "MQ_REPLAY" replay_mq write_status "RUNNING" "SERVICE_START" "starting services" log "SERVICE_START" if [[ -n "${TMS_SCRIPT_PATH:-}" && -x "${TMS_SCRIPT_PATH}" ]]; then "${TMS_SCRIPT_PATH}" start >> "${LOG_FILE}" 2>&1 || true fi write_status "RUNNING" "HEALTH_CHECK" "checking health" log "HEALTH_CHECK" health_check write_status "SUCCESS" "DONE" "restore applied" log "DONE"