#!/usr/bin/env bash set -Eeuo pipefail DEPLOY_VERSION="0.1.0" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LIB_DIR="$SCRIPT_DIR/lib" CORE_DIR="$SCRIPT_DIR/cores" CORE="" SERVER_DIR="" SERVER_PORT="" MC_VERSION="latest" BUILD="latest" RAM="2G" SERVICE_MODE="tmux-systemd" FIREWALL="yes" SERVER_USER="minecraft" ACCEPT_EULA="no" ACCEPT_LICENSE="no" LOCALE="${LANGUAGE:-${LANG:-en}}" ASSUME_YES="no" BEGINNER_MODE="no" DRY_RUN="no" DEBUG="no" REPORT_URL="" REPORT_TOKEN="" VERSION_REQUEST="no" SELF_UPDATE="no" ALLOW_EXISTING="no" BACKUP_EXISTING="no" FORCE_CLEAN="no" UPDATE_MODE="no" STEP_INDEX=0 STEP_TOTAL=30 CORE_DEFAULT_PORT="" CORE_PROTOCOL="" CORE_RUNTIME="" CORE_NAME="" CORE_SERVICE_NAME="" OS_ID="" OS_VERSION="" ARCHITECTURE="" PKG_MANAGER="" INIT_SYSTEM="" LOG_DIR="/var/log/astra-deploy" LOG_FILE="" if shopt -s nullglob 2>/dev/null; then for lib in "$LIB_DIR"/*.sh; do # shellcheck source=/dev/null . "$lib" done shopt -u nullglob else for lib in "$LIB_DIR"/*.sh; do . "$lib" done fi usage() { cat < --core-version (legacy alias for mc-version) --dir --port --mc-version --version (legacy alias for mc-version) --build --ram <2G> --service --firewall --user --accept-eula --eula (legacy alias for accept-eula) --accept-license --license (legacy alias for accept-license) --locale --lang --easy, -E --dry-run --yes --non-interactive --allow-existing --backup-existing --force-clean --report-url --report-token --debug --version (legacy mc-version) without value to print tool version --self-update --script-version -V --update --help Examples: # Быстрый запуск для нубов (без вопросов): sudo bash install.sh --easy --core paper # Полный non-interactive запуск для автоматизации (бота): sudo bash install.sh --yes --core paper --dir /opt/minecraft/paper --service tmux-systemd --locale ru EOF } emit_step() { local step_id="$1" local details="${2:-}" STEP_INDEX=$((STEP_INDEX + 1)) local key="step_${step_id}" local message message="$(t "$key")" if [[ "$message" == "$key" ]]; then message="${step_id}" fi if [[ -n "$details" ]]; then message="${message}: ${details}" fi info "$(t progress_step "$STEP_INDEX" "$message")" send_report "step" "$message" "$step_id" "$STEP_INDEX" "$STEP_TOTAL" } init_step_counter() { if is_java_core "$CORE"; then STEP_TOTAL=80 else STEP_TOTAL=60 fi if [[ "$UPDATE_MODE" == "yes" ]]; then STEP_TOTAL=$((STEP_TOTAL - 6)) if (( STEP_TOTAL < 10 )); then STEP_TOTAL=10 fi fi } show_license() { info "$(t license_title)" info "$(t license_intro)" info "$(t license_item_1)" info "$(t license_item_2)" info "$(t license_item_3)" info "$(t license_item_4)" info "$(t license_item_5)" info "$(t license_item_6)" } parse_bool() { case "${1,,}" in yes|y|1|true|on) echo yes ;; no|n|0|false|off) echo no ;; *) echo "" ;; esac } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --core) CORE="${2:-}"; shift 2 ;; --core=*) CORE="${1#*=}"; shift ;; --core-version) MC_VERSION="${2:-}"; shift 2 ;; --core-version=*) MC_VERSION="${1#*=}"; shift ;; --dir) SERVER_DIR="${2:-}"; shift 2 ;; --dir=*) SERVER_DIR="${1#*=}"; shift ;; --port) SERVER_PORT="${2:-}"; shift 2 ;; --port=*) SERVER_PORT="${1#*=}"; shift ;; --mc-version) MC_VERSION="${2:-}"; shift 2 ;; --mc-version=*) MC_VERSION="${1#*=}"; shift ;; --version-code) MC_VERSION="${2:-}"; shift 2 ;; --version-code=*) MC_VERSION="${1#*=}"; shift ;; --version) if [[ "${2:-}" == "" || "${2:-}" == --* ]]; then VERSION_REQUEST="yes" shift else MC_VERSION="${2:-}" shift 2 fi ;; --build) BUILD="${2:-}"; shift 2 ;; --build=*) BUILD="${1#*=}"; shift ;; --ram) RAM="${2:-}"; shift 2 ;; --ram=*) RAM="${1#*=}"; shift ;; --service) SERVICE_MODE="${2:-}"; shift 2 ;; --service=*) SERVICE_MODE="${1#*=}"; shift ;; --firewall) FIREWALL="$(parse_bool "${2:-}")"; shift 2 ;; --firewall=*) FIREWALL="$(parse_bool "${1#*=}")"; shift ;; --user) SERVER_USER="${2:-}"; shift 2 ;; --user=*) SERVER_USER="${1#*=}"; shift ;; --accept-eula) ACCEPT_EULA="$(parse_bool "${2:-}")"; shift 2 ;; --accept-eula=*) ACCEPT_EULA="$(parse_bool "${1#*=}")"; shift ;; --eula) ACCEPT_EULA="$(parse_bool "${2:-}")"; shift 2 ;; --eula=*) ACCEPT_EULA="$(parse_bool "${1#*=}")"; shift ;; --accept-license) ACCEPT_LICENSE="$(parse_bool "${2:-}")"; shift 2 ;; --accept-license=*) ACCEPT_LICENSE="$(parse_bool "${1#*=}")"; shift ;; --license) ACCEPT_LICENSE="$(parse_bool "${2:-}")"; shift 2 ;; --license=*) ACCEPT_LICENSE="$(parse_bool "${1#*=}")"; shift ;; --locale) LOCALE="${2:-}"; shift 2 ;; --locale=*) LOCALE="${1#*=}"; shift ;; --lang) LOCALE="${2:-}"; shift 2 ;; --lang=*) LOCALE="${1#*=}"; shift ;; --easy|--beginner|-E) BEGINNER_MODE="yes" ASSUME_YES="yes" shift ;; --dry-run) DRY_RUN="yes"; shift ;; --yes|-y) ASSUME_YES="yes"; shift ;; --non-interactive) ASSUME_YES="yes"; shift ;; --allow-existing) ALLOW_EXISTING="yes"; shift ;; --backup-existing) BACKUP_EXISTING="yes"; shift ;; --force-clean) FORCE_CLEAN="yes"; shift ;; --report-url) REPORT_URL="${2:-}"; shift 2 ;; --report-url=*) REPORT_URL="${1#*=}"; shift ;; --report-token) REPORT_TOKEN="${2:-}"; shift 2 ;; --report-token=*) REPORT_TOKEN="${1#*=}"; shift ;; --debug) DEBUG="yes"; shift ;; --script-version|-V) VERSION_REQUEST="yes"; shift ;; --self-update) SELF_UPDATE="yes"; shift ;; --update) UPDATE_MODE="yes"; shift ;; --help|-h) usage exit 0 ;; *) error "$(t unknown_argument "$1")" usage exit 1 ;; esac done if [[ "$FIREWALL" == "" ]]; then FIREWALL="yes" fi } init_logger() { local timestamp timestamp="$(date +%F-%H-%M-%S)" mkdir -p "$LOG_DIR" LOG_FILE="$LOG_DIR/install-${timestamp}.log" exec > >(tee -a "$LOG_FILE") 2>&1 info "$(t log_title "$DEPLOY_VERSION")" info "$(t log_file "$LOG_FILE")" } resolve_core_defaults() { if [[ -z "$CORE" ]]; then return 0 fi local check_fn="core_default_port_${CORE}" if [[ "$(type -t "$check_fn" 2>/dev/null)" != "function" ]]; then fail "$(t unsupported_core "$CORE" "pmmp,paper,purpur,velocity,powernukkitx,nethergamesmc" "${SCRIPT_DIR}/manifests/cores.json")" fi CORE_NAME="$(core_name_${CORE})" CORE_DEFAULT_PORT="$(core_default_port_${CORE})" CORE_PROTOCOL="$(core_protocol_${CORE})" CORE_RUNTIME="$(core_runtime_${CORE})" CORE_SERVICE_NAME="${CORE}_minecraft" if [[ -z "$SERVER_PORT" ]]; then SERVER_PORT="$CORE_DEFAULT_PORT" fi } ask_value() { local prompt_text="$1" local default_value="$2" local var_name="$3" local value="" if [[ -t 0 ]]; then read -r -p "$prompt_text [$default_value]: " value < /dev/tty || true else read -r -p "$prompt_text [$default_value]: " value || true fi if [[ -z "$value" ]]; then value="$default_value" fi printf -v "$var_name" "%s" "$value" } ask_yes_no() { local prompt_text="$1" local default_value="$2" local var_name="$3" local value="" while true; do if [[ -t 0 ]]; then read -r -p "$prompt_text (yes/no) [$default_value]: " value < /dev/tty || true else read -r -p "$prompt_text (yes/no) [$default_value]: " value || true fi if [[ -z "$value" ]]; then value="$default_value" fi value="$(parse_bool "$value" || true)" if [[ "$value" == "yes" || "$value" == "no" ]]; then printf -v "$var_name" "%s" "$value" return fi warn "$(t prompt_invalid_yes_no)" done } apply_noob_defaults() { if [[ -z "$CORE" ]]; then CORE="paper" fi if [[ -z "$RAM" ]]; then RAM="2G" fi if [[ -z "$SERVICE_MODE" ]]; then SERVICE_MODE="tmux-systemd" fi if [[ -z "$FIREWALL" ]]; then FIREWALL="yes" fi if [[ -z "$SERVER_USER" ]]; then SERVER_USER="minecraft" fi if [[ -z "$MC_VERSION" ]]; then MC_VERSION="latest" fi if [[ -z "$BUILD" ]]; then BUILD="latest" fi resolve_core_defaults if [[ -z "$SERVER_DIR" ]]; then SERVER_DIR="/opt/minecraft/$CORE" fi if [[ -z "$SERVER_PORT" ]]; then SERVER_PORT="$CORE_DEFAULT_PORT" fi if [[ "$BEGINNER_MODE" == "yes" ]]; then FIREWALL="$(parse_bool "$FIREWALL" || true)" [[ "$FIREWALL" == "" ]] && FIREWALL="yes" ACCEPT_LICENSE="yes" if is_java_core "$CORE"; then ACCEPT_EULA="yes" fi fi } show_noob_plan() { info "$(t easy_plan_title)" info "$(t easy_plan_core "$CORE")" info "$(t easy_plan_dir "$SERVER_DIR")" info "$(t easy_plan_port "$SERVER_PORT")" info "$(t easy_plan_ram "$RAM")" info "$(t easy_plan_service "$SERVICE_MODE")" info "$(t easy_plan_user "$SERVER_USER")" info "$(t easy_plan_firewall "$FIREWALL")" if [[ "$CORE" == "nethergamesmc" ]]; then info "$(t easy_plan_build "$BUILD")" info "$(t easy_plan_mc_version "$MC_VERSION")" fi if is_java_core "$CORE"; then info "$(t easy_plan_eula "$ACCEPT_EULA")" fi } interact() { if [[ "$ASSUME_YES" == "yes" ]]; then return 0 fi if [[ -z "$CORE" ]]; then ask_value "$(t prompt_core)" "paper" CORE fi resolve_core_defaults if [[ -z "$SERVER_DIR" ]]; then ask_value "$(t prompt_server_directory)" "/opt/minecraft/$CORE" SERVER_DIR fi if [[ -z "$SERVER_PORT" ]]; then ask_value "$(t prompt_server_port)" "$CORE_DEFAULT_PORT" SERVER_PORT fi ask_value "$(t prompt_ram)" "$RAM" RAM if [[ "$CORE" == "nethergamesmc" ]]; then ask_value "$(t prompt_php_build)" "${BUILD:-latest}" BUILD ask_value "$(t prompt_pocketmine_version)" "${MC_VERSION:-latest}" MC_VERSION fi ask_value "$(t prompt_service_mode)" "$SERVICE_MODE" SERVICE_MODE ask_value "$(t prompt_open_firewall)" "$FIREWALL" FIREWALL FIREWALL="$(parse_bool "$FIREWALL" || true)" [[ "$FIREWALL" == "" ]] && FIREWALL="yes" ask_value "$(t prompt_install_user)" "$SERVER_USER" SERVER_USER if is_java_core "$CORE"; then ask_yes_no "$(t eula_prompt)" "${ACCEPT_EULA}" ACCEPT_EULA fi } configure_plan() { if [[ -z "$CORE" ]]; then fail "$(t core_required)" fi resolve_core_defaults local validate_fn="core_validate_${CORE}" if [[ "$(type -t "$validate_fn" 2>/dev/null)" != "function" ]]; then fail "$(t core_validator_missing "$CORE")" fi "$validate_fn" if [[ -z "$SERVER_DIR" ]]; then fail "$(t server_dir_required)" fi if [[ -z "$SERVER_PORT" ]]; then fail "$(t server_port_required)" fi if [[ ! "$SERVER_PORT" =~ ^[0-9]+$ ]]; then fail "$(t port_must_be_numeric)" fi if (( SERVER_PORT < 1 || SERVER_PORT > 65535 )); then fail "$(t port_out_of_range "$SERVER_PORT")" fi if [[ "$SERVICE_MODE" != "systemd" && "$SERVICE_MODE" != "tmux-systemd" && "$SERVICE_MODE" != "screen" && "$SERVICE_MODE" != "none" ]]; then fail "$(t invalid_service_mode "$SERVICE_MODE")" fi if [[ "$FIREWALL" != "yes" && "$FIREWALL" != "no" ]]; then fail "$(t invalid_firewall_value "$FIREWALL")" fi } validate_license() { if [[ "${ACCEPT_LICENSE,,}" == "yes" ]]; then return 0 fi if [[ "$ASSUME_YES" == "yes" ]]; then fail "$(t license_denied)" fi show_license ask_yes_no "$(t license_prompt)" "no" ACCEPT_LICENSE if [[ "${ACCEPT_LICENSE,,}" != "yes" ]]; then fail "$(t license_denied)" fi } check_existing_directory() { if [[ ! -d "$SERVER_DIR" ]]; then run_cmd mkdir -p "$SERVER_DIR" return fi if [[ -z "$(ls -A "$SERVER_DIR" 2>/dev/null)" ]]; then return fi if [[ "$FORCE_CLEAN" == "yes" ]]; then run_cmd find "$SERVER_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + return fi if [[ "$BACKUP_EXISTING" == "yes" ]]; then local backup_dir="${SERVER_DIR}.backup-$(date +%F-%H-%M-%S)" run_cmd cp -a "$SERVER_DIR" "$backup_dir" info "$(t dir_backed_up "$backup_dir")" return fi if [[ "$ALLOW_EXISTING" == "yes" ]]; then return fi if [[ "$ASSUME_YES" == "yes" ]]; then fail "$(t dir_not_empty_auto "$SERVER_DIR")" fi warn "$(t dir_not_empty "$SERVER_DIR")" while true; do local action if [[ -t 0 ]]; then read -r -p "$(t dir_not_empty_actions)" action < /dev/tty || true else read -r -p "$(t dir_not_empty_actions)" action || true fi case "$action" in 1) fail "$(t action_cancelled_by_user)";; 2) return;; 3) local backup_dir="${SERVER_DIR}.backup-$(date +%F-%H-%M-%S)" run_cmd cp -a "$SERVER_DIR" "$backup_dir" info "$(t dir_backed_up "$backup_dir")" return ;; 4) run_cmd find "$SERVER_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + return ;; *) warn "$(t dir_invalid_option)" ;; esac done } backup_file() { local path="$1" if [[ ! -e "$path" ]]; then return 0 fi local backup_path="${path}.backup-$(date +%F-%H-%M-%S)" run_cmd cp -a "$path" "$backup_path" info "$(t file_backed_up "$path" "$backup_path")" } preflight() { emit_step "preflight_begin" emit_step "preflight_validate_root" validate_root_usage emit_step "preflight_detect" emit_step "preflight_detect_os" detect_system emit_step "preflight_validate_support" validate_core_support if [[ "$UPDATE_MODE" == "yes" ]]; then emit_step "preflight_update_mode_check" if [[ ! -d "$SERVER_DIR" ]]; then fail "$(t update_requires_existing_dir)" fi if [[ -z "$(ls -A "$SERVER_DIR" 2>/dev/null)" ]]; then fail "$(t update_requires_non_empty_dir)" fi fi emit_step "preflight_validate_plan" configure_plan emit_step "preflight_validate_licenses" validate_license validate_eula emit_step "preflight_validate_port" if is_java_core "$CORE"; then validate_port "$SERVER_PORT" "tcp" else validate_port "$SERVER_PORT" "udp" fi emit_step "preflight_directory" if [[ "$UPDATE_MODE" == "no" ]]; then check_existing_directory fi emit_step "preflight_packages" install_base_packages if is_java_core "$CORE"; then emit_step "preflight_java" install_java_21 fi emit_step "preflight_user" create_minecraft_user "$SERVER_USER" prepare_server_dir "$SERVER_DIR" "$SERVER_USER" } install_core() { emit_step "install_core" "$CORE" local install_fn="core_install_${CORE}" local post_fn="core_post_install_${CORE}" local update_fn="core_update_${CORE}" if [[ "$UPDATE_MODE" == "yes" && "$(type -t "$update_fn" 2>/dev/null)" == "function" ]]; then stop_service_if_running "${CORE_SERVICE_NAME}" emit_step "update_core" "$CORE" "$update_fn" else emit_step "deploy_core" "$CORE" "$install_fn" fi if [[ ! "$(type -t "$post_fn" 2>/dev/null)" == "function" ]]; then fail "$(t post_install_callback_missing "$CORE")" fi "$post_fn" } finalize() { info "$(t install_completed "$CORE_NAME")" info "$(t result_server_dir "$SERVER_DIR")" info "$(t result_service_mode "${SERVICE_MODE_ACTIVE:-none}")" info "$(t result_service_name "${SERVICE_NAME:-${CORE}_minecraft}")" info "$(t result_log_file "$LOG_FILE")" send_report "success" "$(t install_finished)" } main() { for core_file in "$CORE_DIR"/*.sh; do . "$core_file" done load_locale parse_args "$@" if [[ "$VERSION_REQUEST" == "yes" ]]; then echo "AstraDeploy v$DEPLOY_VERSION" exit 0 fi load_locale validate_root_usage init_logger trap 'rollback_on_error "$?" "$BASH_COMMAND"' ERR send_report "started" "Deployment started" if [[ "$SELF_UPDATE" == "yes" ]]; then warn "$(t self_update_not_implemented)" fi emit_step "parse_complete" interact emit_step "prepare_defaults" apply_noob_defaults if [[ "$BEGINNER_MODE" == "yes" ]]; then show_noob_plan fi init_step_counter emit_step "preflight_start" preflight emit_step "install_start" install_core emit_step "finalize" finalize } main "$@"