feat: add bds core, locale updates and safety fixes

This commit is contained in:
2026-05-31 14:01:34 +03:00
parent d1686355e8
commit c769140913
34 changed files with 1535 additions and 12 deletions

33
lib/detect.sh Normal file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
detect_system() {
if [[ -f /etc/os-release ]]; then
# shellcheck disable=SC1091
. /etc/os-release
OS_ID="${ID:-unknown}"
OS_VERSION="${VERSION_ID:-unknown}"
OS_NAME="${PRETTY_NAME:-unknown}"
else
fail "$(t detect_os_failed)"
fi
ARCHITECTURE="$(uname -m)"
if command -v apt-get >/dev/null 2>&1; then
PKG_MANAGER="apt"
elif command -v dnf >/dev/null 2>&1; then
PKG_MANAGER="dnf"
elif command -v yum >/dev/null 2>&1; then
PKG_MANAGER="yum"
elif command -v pacman >/dev/null 2>&1; then
PKG_MANAGER="pacman"
else
fail "$(t no_package_manager)"
fi
if command -v systemctl >/dev/null 2>&1; then
INIT_SYSTEM="systemd"
else
INIT_SYSTEM="none"
fi
}

65
lib/download.sh Normal file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env bash
http_get() {
local url="$1"
curl -fsSL -H "User-Agent: AstraDeploy/${DEPLOY_VERSION}" "$url"
}
get_papermc_download_url() {
local project="$1"
local version="$2"
if [[ "$version" == "latest" ]]; then
version="$(http_get "https://fill.papermc.io/v3/projects/${project}" | jq -r '.versions | if type=="array" then .[-1] else (to_entries | last | .value[0]) end')"
fi
local builds_json
builds_json="$(http_get "https://fill.papermc.io/v3/projects/${project}/versions/${version}/builds")"
local url
url="$(printf '%s\n' "$builds_json" | jq -r '.builds | map(select(.channel=="STABLE")) | .[0].downloads."server:default".url // empty')"
if [[ -z "$url" || "$url" == "null" ]]; then
fail "$(t no_stable_build "$project" "$version")"
fi
echo "$url"
}
get_purpur_download_url() {
local version="$1"
local build="${2:-latest}"
if [[ "$version" == "latest" ]]; then
version="$(http_get "https://api.purpurmc.org/v2/purpur/" | jq -r '.versions | .[-1]')"
fi
if [[ "$build" == "latest" ]]; then
echo "https://api.purpurmc.org/v2/purpur/${version}/latest/download"
else
echo "https://api.purpurmc.org/v2/purpur/${version}/${build}/download"
fi
}
get_github_release_asset_url() {
local repo="$1"
local pattern="$2"
http_get "https://api.github.com/repos/${repo}/releases/latest" \
| jq -r --arg pattern "$pattern" '.assets[] | select(.name | test($pattern)) | .browser_download_url' | head -n 1
}
download_as_user() {
local url="$1"
local target="$2"
emit_step "download_start" "$url"
run_cmd sudo -u "$SERVER_USER" bash -lc "curl -fL \"$url\" -o \"${target}\""
emit_step "download_completed" "$target"
}
download_as_user_atomic() {
local url="$1"
local target="$2"
local tmp="${target}.tmp.${RANDOM}.$(date +%s)"
download_as_user "$url" "$tmp"
run_cmd mv "$tmp" "$target"
emit_step "download_swap" "$target"
}

30
lib/firewall.sh Normal file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
open_firewall_port() {
local port="$1"
local proto="$2"
if [[ "$FIREWALL" != "yes" ]]; then
info "$(t firewall_skip)"
return 0
fi
emit_step "firewall_start" "$port/$proto"
if command -v ufw >/dev/null 2>&1; then
run_cmd ufw allow "$port/$proto" || true
run_cmd ufw --force enable || true
emit_step "firewall_done" "ufw"
return 0
fi
if command -v firewall-cmd >/dev/null 2>&1; then
run_cmd firewall-cmd --permanent --add-port="$port/$proto"
run_cmd firewall-cmd --reload
emit_step "firewall_done" "firewalld"
return 0
fi
warn "$(t firewall_manual_open "$port" "$proto")"
emit_step "firewall_manual" "$port/$proto"
}

50
lib/i18n.sh Normal file
View File

@@ -0,0 +1,50 @@
#!/usr/bin/env bash
LOCALE_DIRECTORY="${SCRIPT_DIR}/locales"
load_locale() {
case "${LOCALE,,}" in
ru*|ru_*)
LOCALE="ru"
;;
en*|en_*)
LOCALE="en"
;;
*)
if [[ "${LANGUAGE:-${LANG:-}}" == ru* ]]; then
LOCALE="ru"
else
LOCALE="en"
fi
;;
esac
case "$LOCALE" in
ru)
LOCALE_FILE="$LOCALE_DIRECTORY/ru.sh"
;;
*)
LOCALE_FILE="$LOCALE_DIRECTORY/en.sh"
;;
esac
if [[ ! -f "$LOCALE_FILE" ]]; then
LOCALE="en"
LOCALE_FILE="$LOCALE_DIRECTORY/en.sh"
fi
# shellcheck source=/dev/null
. "$LOCALE_FILE"
}
t() {
local key="$1"
shift || true
local fn="locale_msg_${key}"
if declare -F "$fn" >/dev/null 2>&1; then
"$fn" "$@"
else
echo "$key"
fi
}

77
lib/java.sh Normal file
View File

@@ -0,0 +1,77 @@
#!/usr/bin/env bash
is_java_core() {
local core="$1"
case "$core" in
paper|purpur|velocity|powernukkitx) return 0 ;;
*) return 1 ;;
esac
}
install_java_21() {
if command -v java >/dev/null 2>&1; then
local version
version="$(java -version 2>&1 | sed -n 's/.*version "\(.*\)".*/\1/p' | head -n1 | sed 's/"//g' | sed 's/^\([0-9][0-9]*\).*/\1/')"
if [[ "$version" =~ ^[0-9]+$ ]] && (( version >= 21 )); then
info "$(t java_already_available "$version")"
return 0
fi
fi
info "$(t installing_java_21)"
case "$PKG_MANAGER" in
apt)
emit_step "java_install" "apt"
run_cmd apt-get install -y openjdk-21-jre-headless
;;
dnf|yum)
emit_step "java_install" "$PKG_MANAGER"
run_cmd "$PKG_MANAGER" install -y java-21-openjdk-headless
;;
pacman)
emit_step "java_install" "pacman"
run_cmd pacman -Sy --noconfirm jre-openjdk-headless
;;
esac
}
create_java_start_script() {
local dir="$1"
local jar_file="$2"
local ram="$3"
local extra_opts="${4:-}"
local script="$dir/start.sh"
emit_step "render_start_script" "$jar_file"
run_cmd bash -c "cat > '$script' <<EOF
#!/usr/bin/env bash
set -e
cd \"\$(dirname \"\$0\")\"
exec java -Xms${ram} -Xmx${ram} ${extra_opts} -jar ${jar_file} nogui
EOF"
run_cmd chmod +x "$script"
emit_step "chmod_start_script" "$script"
}
create_powernukkit_start_script() {
local dir="$1"
local ram="$2"
local script="$dir/start.sh"
emit_step "render_start_script" "powernukkitx.jar"
run_cmd bash -c "cat > '$script' <<EOF
#!/usr/bin/env bash
set -e
cd \"\$(dirname \"\$0\")\"
exec java -Xms${ram} -Xmx${ram} \\
-Dfile.encoding=UTF-8 \\
-Djansi.passthrough=true \\
-Dterminal.ansi=true \\
--add-opens java.base/java.lang=ALL-UNNAMED \\
--add-opens java.base/java.io=ALL-UNNAMED \\
--add-opens java.base/java.net=ALL-UNNAMED \\
-jar powernukkitx.jar
EOF"
run_cmd chmod +x "$script"
emit_step "chmod_start_script" "$script"
}

38
lib/logger.sh Normal file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
log() {
local level="$1"
shift
printf '[%s] [%s] %s\n' "$(date +'%Y-%m-%d %H:%M:%S')" "$level" "$*"
}
info() {
log "INFO" "$@"
}
warn() {
log "WARN" "$@"
}
error() {
log "ERROR" "$@"
}
debug() {
if [[ "${DEBUG:-no}" == "yes" ]]; then
log "DEBUG" "$@"
fi
}
fail() {
error "$@"
exit 1
}
run_cmd() {
if [[ "${DRY_RUN:-no}" == "yes" ]]; then
info "[dry-run] $*"
return 0
fi
"$@"
}

34
lib/packages.sh Normal file
View File

@@ -0,0 +1,34 @@
#!/usr/bin/env bash
install_base_packages() {
emit_step "packages_start" "$(t installing_base_packages)"
local packages=("curl" "wget" "ca-certificates" "jq" "tar" "unzip" "tmux" "screen" "lsof")
local pkg_string
pkg_string="$(printf '%s ' "${packages[@]}")"
if [[ "$DRY_RUN" == "yes" ]]; then
emit_step "packages_dry_run" "$pkg_string"
info "$(t dry_run_install "$pkg_string")"
return 0
fi
case "$PKG_MANAGER" in
apt)
run_cmd apt-get update
emit_step "packages_index" "apt"
run_cmd DEBIAN_FRONTEND=noninteractive apt-get install -y "${packages[@]}"
;;
dnf|yum)
emit_step "packages_index" "$PKG_MANAGER"
run_cmd "$PKG_MANAGER" install -y "${packages[@]}"
;;
pacman)
emit_step "packages_index" "pacman"
run_cmd pacman -Sy --noconfirm "${packages[@]}"
;;
*)
fail "$(t package_manager_unsupported "$PKG_MANAGER")"
;;
esac
}

72
lib/report.sh Normal file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env bash
json_escape() {
local raw="${1//\\/\\\\}"
raw="${raw//\"/\\\"}"
raw="${raw//$'\n'/\\n}"
echo "$raw"
}
send_report() {
local status="$1"
local message="${2:-}"
local step="${3:-}"
local step_index="${4:-}"
local step_total="${5:-}"
local step_percent="null"
local step_index_json="null"
local step_total_json="null"
local step_percent_json="null"
if [[ "$step_index" =~ ^[0-9]+$ ]]; then
step_index_json="$step_index"
fi
if [[ "$step_total" =~ ^[0-9]+$ && "$step_total" -gt 0 ]]; then
step_total_json="$step_total"
fi
if [[ "$step_index_json" != "null" && "$step_total_json" != "null" ]]; then
if (( step_index > step_total )); then
step_percent_json=100
else
step_percent=$(( step_index * 100 / step_total ))
step_percent_json="$step_percent"
fi
fi
if [[ -z "${REPORT_URL:-}" || -z "${REPORT_TOKEN:-}" ]]; then
return 0
fi
local payload
payload="$(printf '{ "status":"%s", "core":"%s", "dir":"%s", "port":%s, "protocol":"%s", "service":"%s", "message":"%s", "step":"%s", "step_index":%s, "step_total":%s, "step_percent":%s }' \
"$(json_escape "$status")" \
"$(json_escape "${CORE}")" \
"$(json_escape "${SERVER_DIR}")" \
"${SERVER_PORT}" \
"$(json_escape "${CORE_PROTOCOL}")" \
"$(json_escape "${SERVICE_MODE_ACTIVE}")" \
"$(json_escape "$message")" \
"$(json_escape "$step")" \
"$step_index_json" \
"$step_total_json" \
"$step_percent_json")"
local attempt
for attempt in 1 2; do
if [[ "${DRY_RUN}" == "yes" ]]; then
info "$(t report_dry_run "$payload")"
return 0
fi
if curl -fsSL -X POST "$REPORT_URL" \
-H "Authorization: Bearer ${REPORT_TOKEN}" \
-H "Content-Type: application/json" \
-d "$payload" >/dev/null 2>&1; then
return 0
fi
sleep 1
done
warn "$(t report_failed)"
}

30
lib/rollback.sh Normal file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
ROLLBACK_IN_PROGRESS="no"
rollback_on_error() {
local exit_code="${1:-1}"
local command_failed="${2:-unknown}"
if [[ "$ROLLBACK_IN_PROGRESS" == "yes" ]]; then
return
fi
ROLLBACK_IN_PROGRESS="yes"
local fail_message
fail_message="$(t deployment_failed "$exit_code" "$command_failed")"
error "$fail_message"
send_report "failed" "$fail_message"
warn "$(t rollback_starts "$SERVER_DIR")"
if [[ "${SERVICE_CREATED:-no}" == "yes" ]]; then
remove_service_if_exists
fi
if [[ -n "${LOG_FILE:-}" ]]; then
info "$(t see_log "$LOG_FILE")"
fi
exit "$exit_code"
}

132
lib/services.sh Normal file
View File

@@ -0,0 +1,132 @@
#!/usr/bin/env bash
SERVICE_NAME=""
SERVICE_FILE=""
SERVICE_CREATED="no"
SERVICE_MODE_ACTIVE="none"
stop_service_if_running() {
local service_name="${1:-${SERVICE_NAME:-${CORE}_minecraft}}"
if [[ "$INIT_SYSTEM" != "systemd" ]]; then
return 0
fi
if [[ "${DRY_RUN:-no}" == "yes" ]]; then
info "[dry-run] systemctl stop ${service_name}"
return 0
fi
emit_step "service_stop" "$service_name"
systemctl stop "$service_name" >/dev/null 2>&1 || true
}
create_service() {
local core="$1"
local dir="$2"
local mode="$3"
local start_script="$4"
local start_script_escaped="$4"
start_script_escaped="${start_script_escaped//\\/\\\\}"
start_script_escaped="${start_script_escaped//\"/\\\"}"
start_script_escaped="${start_script_escaped//%/%%}"
SERVICE_NAME="${core}_minecraft"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
if [[ "$mode" == "none" || "$INIT_SYSTEM" != "systemd" ]]; then
warn "$(t service_create_skipped "$mode" "$INIT_SYSTEM")"
SERVICE_MODE_ACTIVE="none"
return 0
fi
emit_step "service_create" "$core:$mode"
if [[ "$mode" == "tmux-systemd" ]]; then
run_cmd bash -c "cat > '$SERVICE_FILE' <<EOF
[Unit]
Description=Minecraft ${core} server (tmux)
After=network.target
[Service]
Type=forking
User=${SERVER_USER}
Group=${SERVER_USER}
WorkingDirectory=${dir}
ExecStart=/usr/bin/tmux new-session -d -s ${SERVICE_NAME} \"${start_script_escaped}\"
ExecStop=/usr/bin/tmux send-keys -t ${SERVICE_NAME} 'stop' C-m
ExecStopPost=/bin/sleep 8
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF"
elif [[ "$mode" == "screen" ]]; then
run_cmd bash -c "cat > '$SERVICE_FILE' <<EOF
[Unit]
Description=Minecraft ${core} server (screen)
After=network.target
[Service]
Type=forking
User=${SERVER_USER}
Group=${SERVER_USER}
WorkingDirectory=${dir}
ExecStart=/usr/bin/screen -dmS ${SERVICE_NAME} \"${start_script_escaped}\"
ExecStop=/usr/bin/screen -S ${SERVICE_NAME} -X quit
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF"
else
run_cmd bash -c "cat > '$SERVICE_FILE' <<EOF
[Unit]
Description=Minecraft ${core} server
After=network.target
[Service]
Type=simple
User=${SERVER_USER}
Group=${SERVER_USER}
WorkingDirectory=${dir}
ExecStart="${start_script_escaped}"
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF"
fi
SERVICE_CREATED="yes"
SERVICE_MODE_ACTIVE="$mode"
emit_step "service_reload_enable" "$SERVICE_NAME"
run_cmd systemctl daemon-reload
run_cmd systemctl enable "$SERVICE_NAME"
}
start_service() {
if [[ "$SERVICE_MODE_ACTIVE" == "none" ]]; then
info "$(t service_start_skipped "$SERVICE_MODE_ACTIVE")"
return 0
fi
emit_step "service_start" "$SERVICE_NAME"
run_cmd systemctl restart "$SERVICE_NAME"
}
remove_service_if_exists() {
if [[ "$SERVICE_CREATED" != "yes" ]]; then
return 0
fi
if [[ -n "$SERVICE_NAME" ]]; then
run_cmd systemctl stop "$SERVICE_NAME" || true
run_cmd systemctl disable "$SERVICE_NAME" || true
fi
if [[ -n "${SERVICE_FILE}" && -f "$SERVICE_FILE" ]]; then
run_cmd rm -f "$SERVICE_FILE"
fi
run_cmd systemctl daemon-reload || true
}

31
lib/users.sh Normal file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
create_minecraft_user() {
local user="$1"
if [[ "$user" == "root" ]]; then
fail "$(t refuse_root_user)"
fi
if ! id "$user" >/dev/null 2>&1; then
emit_step "user_create" "$user"
info "$(t creating_user "$user")"
if [[ "$DRY_RUN" == "no" ]]; then
useradd -r -m -s /bin/bash "$user"
else
info "$(t dry_run_useradd "$user")"
fi
else
emit_step "user_exists" "$user"
fi
}
prepare_server_dir() {
local dir="$1"
local user="$2"
emit_step "prepare_dir" "$dir"
run_cmd mkdir -p "$dir"
run_cmd chown -R "${user}:${user}" "$dir"
emit_step "permission_fix" "$dir"
}

View File

@@ -7,12 +7,12 @@ validate_root_usage() {
}
validate_core_support() {
local supported_cores="pmmp,paper,purpur,velocity,powernukkitx,nethergamesmc"
local supported_cores="pmmp,paper,purpur,velocity,powernukkitx,nethergamesmc,bds"
local supported_os="ubuntu,debian"
local planned_os="arch,fedora,rocky,almalinux"
case "$CORE" in
pmmp|paper|purpur|velocity|powernukkitx|nethergamesmc)
pmmp|paper|purpur|velocity|powernukkitx|nethergamesmc|bds)
;;
*)
fail "$(t unsupported_core "$CORE" "$supported_cores" "${SCRIPT_DIR}/manifests/cores.json")"