#!/bin/bash # ============================================================================== # HELPER FUNCTIONS FOR PACKAGE MANAGEMENT # ============================================================================== # ------------------------------------------------------------------------------ # Cache installed version to avoid repeated checks # ------------------------------------------------------------------------------ cache_installed_version() { local app="$1" local version="$2" mkdir -p /var/cache/app-versions echo "$version" >"/var/cache/app-versions/${app}_version.txt" } get_cached_version() { local app="$1" mkdir -p /var/cache/app-versions if [[ -f "/var/cache/app-versions/${app}_version.txt" ]]; then cat "/var/cache/app-versions/${app}_version.txt" return 0 fi return 0 } # ------------------------------------------------------------------------------ # Unified package upgrade function (with apt update caching) # ------------------------------------------------------------------------------ upgrade_package() { local package="$1" msg_info "Upgrading $package" # Use same caching logic as ensure_dependencies local apt_cache_file="/var/cache/apt-update-timestamp" local current_time=$(date +%s) local last_update=0 if [[ -f "$apt_cache_file" ]]; then last_update=$(cat "$apt_cache_file" 2>/dev/null || echo 0) fi if ((current_time - last_update > 300)); then $STD apt update echo "$current_time" >"$apt_cache_file" fi $STD apt install --only-upgrade -y "$package" msg_ok "Upgraded $package" } # ------------------------------------------------------------------------------ # Repository availability check # ------------------------------------------------------------------------------ verify_repo_available() { local repo_url="$1" local suite="$2" if curl -fsSL --max-time 10 "${repo_url}/dists/${suite}/Release" &>/dev/null; then return 0 fi return 1 } # ------------------------------------------------------------------------------ # Ensure dependencies are installed (with apt update caching) # ------------------------------------------------------------------------------ ensure_dependencies() { local deps=("$@") local missing=() for dep in "${deps[@]}"; do if ! command -v "$dep" &>/dev/null && ! is_package_installed "$dep"; then missing+=("$dep") fi done if [[ ${#missing[@]} -gt 0 ]]; then msg_info "Installing dependencies: ${missing[*]}" # Only run apt update if not done recently (within last 5 minutes) local apt_cache_file="/var/cache/apt-update-timestamp" local current_time=$(date +%s) local last_update=0 if [[ -f "$apt_cache_file" ]]; then last_update=$(cat "$apt_cache_file" 2>/dev/null || echo 0) fi if ((current_time - last_update > 300)); then $STD apt update echo "$current_time" >"$apt_cache_file" fi $STD apt install -y "${missing[@]}" msg_ok "Installed dependencies" fi } # ------------------------------------------------------------------------------ # Smart version comparison # ------------------------------------------------------------------------------ version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1" } # ------------------------------------------------------------------------------ # Get system architecture (normalized) # ------------------------------------------------------------------------------ get_system_arch() { local arch_type="${1:-dpkg}" # dpkg, uname, or both local arch case "$arch_type" in dpkg) arch=$(dpkg --print-architecture 2>/dev/null) ;; uname) arch=$(uname -m) [[ "$arch" == "x86_64" ]] && arch="amd64" [[ "$arch" == "aarch64" ]] && arch="arm64" ;; both | *) arch=$(dpkg --print-architecture 2>/dev/null || uname -m) [[ "$arch" == "x86_64" ]] && arch="amd64" [[ "$arch" == "aarch64" ]] && arch="arm64" ;; esac echo "$arch" } # ------------------------------------------------------------------------------ # Create temporary directory with automatic cleanup # ------------------------------------------------------------------------------ create_temp_dir() { local tmp_dir=$(mktemp -d) # Set trap to cleanup on EXIT, ERR, INT, TERM trap "rm -rf '$tmp_dir'" EXIT ERR INT TERM echo "$tmp_dir" } # ------------------------------------------------------------------------------ # Check if package is installed (faster than dpkg -l | grep) # ------------------------------------------------------------------------------ is_package_installed() { local package="$1" dpkg-query -W -f='${Status}' "$package" 2>/dev/null | grep -q "^install ok installed$" } # ------------------------------------------------------------------------------ # GitHub API call with authentication and rate limit handling # ------------------------------------------------------------------------------ github_api_call() { local url="$1" local output_file="${2:-/dev/stdout}" local max_retries=3 local retry_delay=2 local header_args=() [[ -n "${GITHUB_TOKEN:-}" ]] && header_args=(-H "Authorization: Bearer $GITHUB_TOKEN") for attempt in $(seq 1 $max_retries); do local http_code http_code=$(curl -fsSL -w "%{http_code}" -o "$output_file" \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ "${header_args[@]}" \ "$url" 2>/dev/null || echo "000") case "$http_code" in 200) return 0 ;; 403) # Rate limit - check if we can retry if [[ $attempt -lt $max_retries ]]; then msg_warn "GitHub API rate limit, waiting ${retry_delay}s... (attempt $attempt/$max_retries)" sleep "$retry_delay" retry_delay=$((retry_delay * 2)) continue fi msg_error "GitHub API rate limit exceeded. Set GITHUB_TOKEN to increase limits." return 1 ;; 404) msg_error "GitHub API endpoint not found: $url" return 1 ;; *) if [[ $attempt -lt $max_retries ]]; then sleep "$retry_delay" continue fi msg_error "GitHub API call failed with HTTP $http_code" return 1 ;; esac done return 1 } should_upgrade() { local current="$1" local target="$2" [[ -z "$current" ]] && return 0 version_gt "$target" "$current" && return 0 return 1 } # ------------------------------------------------------------------------------ # Get OS information (cached for performance) # ------------------------------------------------------------------------------ get_os_info() { local field="${1:-all}" # id, codename, version, version_id, all # Cache OS info to avoid repeated file reads if [[ -z "${_OS_ID:-}" ]]; then export _OS_ID=$(awk -F= '/^ID=/{gsub(/"/,"",$2); print $2}' /etc/os-release) export _OS_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{gsub(/"/,"",$2); print $2}' /etc/os-release) export _OS_VERSION=$(awk -F= '/^VERSION_ID=/{gsub(/"/,"",$2); print $2}' /etc/os-release) export _OS_VERSION_FULL=$(awk -F= '/^VERSION=/{gsub(/"/,"",$2); print $2}' /etc/os-release) fi case "$field" in id) echo "$_OS_ID" ;; codename) echo "$_OS_CODENAME" ;; version) echo "$_OS_VERSION" ;; version_id) echo "$_OS_VERSION" ;; version_full) echo "$_OS_VERSION_FULL" ;; all) echo "ID=$_OS_ID CODENAME=$_OS_CODENAME VERSION=$_OS_VERSION" ;; *) echo "$_OS_ID" ;; esac } # ------------------------------------------------------------------------------ # Check if running on specific OS # ------------------------------------------------------------------------------ is_debian() { [[ "$(get_os_info id)" == "debian" ]] } is_ubuntu() { [[ "$(get_os_info id)" == "ubuntu" ]] } is_alpine() { [[ "$(get_os_info id)" == "alpine" ]] } # ------------------------------------------------------------------------------ # Get Debian/Ubuntu major version # ------------------------------------------------------------------------------ get_os_version_major() { local version=$(get_os_info version) echo "${version%%.*}" } # ------------------------------------------------------------------------------ # Download file with retry logic and progress # ------------------------------------------------------------------------------ download_file() { local url="$1" local output="$2" local max_retries="${3:-3}" local show_progress="${4:-false}" local curl_opts=(-fsSL) [[ "$show_progress" == "true" ]] && curl_opts=(-fL#) for attempt in $(seq 1 $max_retries); do if curl "${curl_opts[@]}" -o "$output" "$url"; then return 0 fi if [[ $attempt -lt $max_retries ]]; then msg_warn "Download failed, retrying... (attempt $attempt/$max_retries)" sleep 2 fi done msg_error "Failed to download: $url" return 1 } # ------------------------------------------------------------------------------ # Get fallback suite for repository (comprehensive mapping) # ------------------------------------------------------------------------------ get_fallback_suite() { local distro_id="$1" local distro_codename="$2" local repo_base_url="$3" # Check if current codename works if verify_repo_available "$repo_base_url" "$distro_codename"; then echo "$distro_codename" return 0 fi # Comprehensive fallback mappings case "$distro_id" in debian) case "$distro_codename" in # Debian 13 (Trixie) → Debian 12 (Bookworm) trixie | forky | sid) echo "bookworm" ;; # Debian 12 (Bookworm) stays bookworm) echo "bookworm" ;; # Debian 11 (Bullseye) stays bullseye) echo "bullseye" ;; # Unknown → latest stable *) echo "bookworm" ;; esac ;; ubuntu) case "$distro_codename" in # Ubuntu 24.10 (Oracular) → 24.04 LTS (Noble) oracular | plucky) echo "noble" ;; # Ubuntu 24.04 LTS (Noble) stays noble) echo "noble" ;; # Ubuntu 23.10 (Mantic) → 22.04 LTS (Jammy) mantic | lunar) echo "jammy" ;; # Ubuntu 22.04 LTS (Jammy) stays jammy) echo "jammy" ;; # Ubuntu 20.04 LTS (Focal) stays focal) echo "focal" ;; # Unknown → latest LTS *) echo "jammy" ;; esac ;; *) echo "$distro_codename" ;; esac } # ------------------------------------------------------------------------------ # Verify package source and version # ------------------------------------------------------------------------------ verify_package_source() { local package="$1" local expected_version="$2" if apt-cache policy "$package" 2>/dev/null | grep -q "$expected_version"; then return 0 fi return 1 } # ------------------------------------------------------------------------------ # Check if running on LTS version # ------------------------------------------------------------------------------ is_lts_version() { local os_id=$(get_os_info id) local codename=$(get_os_info codename) if [[ "$os_id" == "ubuntu" ]]; then case "$codename" in focal | jammy | noble) return 0 ;; # 20.04, 22.04, 24.04 *) return 1 ;; esac elif [[ "$os_id" == "debian" ]]; then # Debian releases are all "stable" case "$codename" in bullseye | bookworm | trixie) return 0 ;; *) return 1 ;; esac fi return 1 } # ------------------------------------------------------------------------------ # Get optimal number of parallel jobs (cached) # ------------------------------------------------------------------------------ get_parallel_jobs() { if [[ -z "${_PARALLEL_JOBS:-}" ]]; then local cpu_count=$(nproc 2>/dev/null || echo 1) local mem_gb=$(free -g | awk '/^Mem:/{print $2}') # Limit by available memory (assume 1GB per job for compilation) local max_by_mem=$((mem_gb > 0 ? mem_gb : 1)) local max_jobs=$((cpu_count < max_by_mem ? cpu_count : max_by_mem)) # At least 1, at most cpu_count export _PARALLEL_JOBS=$((max_jobs > 0 ? max_jobs : 1)) fi echo "$_PARALLEL_JOBS" } # ------------------------------------------------------------------------------ # Get default PHP version for OS # ------------------------------------------------------------------------------ get_default_php_version() { local os_id=$(get_os_info id) local os_version=$(get_os_version_major) case "$os_id" in debian) case "$os_version" in 13) echo "8.3" ;; # Debian 13 (Trixie) 12) echo "8.2" ;; # Debian 12 (Bookworm) 11) echo "7.4" ;; # Debian 11 (Bullseye) *) echo "8.2" ;; esac ;; ubuntu) case "$os_version" in 24) echo "8.3" ;; # Ubuntu 24.04 LTS (Noble) 22) echo "8.1" ;; # Ubuntu 22.04 LTS (Jammy) 20) echo "7.4" ;; # Ubuntu 20.04 LTS (Focal) *) echo "8.1" ;; esac ;; *) echo "8.2" ;; esac } # ------------------------------------------------------------------------------ # Get default Python version for OS # ------------------------------------------------------------------------------ get_default_python_version() { local os_id=$(get_os_info id) local os_version=$(get_os_version_major) case "$os_id" in debian) case "$os_version" in 13) echo "3.12" ;; # Debian 13 (Trixie) 12) echo "3.11" ;; # Debian 12 (Bookworm) 11) echo "3.9" ;; # Debian 11 (Bullseye) *) echo "3.11" ;; esac ;; ubuntu) case "$os_version" in 24) echo "3.12" ;; # Ubuntu 24.04 LTS 22) echo "3.10" ;; # Ubuntu 22.04 LTS 20) echo "3.8" ;; # Ubuntu 20.04 LTS *) echo "3.10" ;; esac ;; *) echo "3.11" ;; esac } # ------------------------------------------------------------------------------ # Get default Node.js LTS version # ------------------------------------------------------------------------------ get_default_nodejs_version() { # Always return current LTS (as of 2025) echo "20" } # ------------------------------------------------------------------------------ # Check if package manager is locked # ------------------------------------------------------------------------------ is_apt_locked() { if fuser /var/lib/dpkg/lock-frontend &>/dev/null || fuser /var/lib/apt/lists/lock &>/dev/null || fuser /var/cache/apt/archives/lock &>/dev/null; then return 0 fi return 1 } # ------------------------------------------------------------------------------ # Wait for apt to be available # ------------------------------------------------------------------------------ wait_for_apt() { local max_wait="${1:-300}" # 5 minutes default local waited=0 while is_apt_locked; do if [[ $waited -ge $max_wait ]]; then msg_error "Timeout waiting for apt to be available" return 1 fi debug_log "Waiting for apt to be available... (${waited}s)" sleep 5 waited=$((waited + 5)) done return 0 } # ------------------------------------------------------------------------------ # Cleanup old repository files (migration helper) # ------------------------------------------------------------------------------ cleanup_old_repo_files() { local app="$1" # Remove old-style .list files (including backups) rm -f /etc/apt/sources.list.d/"${app}"*.list rm -f /etc/apt/sources.list.d/"${app}"*.list.save rm -f /etc/apt/sources.list.d/"${app}"*.list.distUpgrade rm -f /etc/apt/sources.list.d/"${app}"*.list.dpkg-* # Remove old GPG keys from trusted.gpg.d rm -f /etc/apt/trusted.gpg.d/"${app}"*.gpg # Remove duplicate .sources files (keep only the main one) local sources_file="/etc/apt/sources.list.d/${app}.sources" if [[ -f "$sources_file" ]]; then find /etc/apt/sources.list.d/ -name "${app}*.sources" ! -name "${app}.sources" -delete 2>/dev/null || true fi } # ------------------------------------------------------------------------------ # Standardized deb822 repository setup # ------------------------------------------------------------------------------ setup_deb822_repo() { local name="$1" local gpg_url="$2" local repo_url="$3" local suite="$4" local component="${5:-main}" local architectures="${6:-amd64 arm64}" msg_info "Setting up $name repository" # Cleanup old configs cleanup_old_repo_files "$name" # Ensure keyring directory exists mkdir -p /etc/apt/keyrings # Download GPG key (with --yes to avoid interactive prompts) curl -fsSL "$gpg_url" | gpg --dearmor --yes -o "/etc/apt/keyrings/${name}.gpg" # Create deb822 sources file cat </etc/apt/sources.list.d/${name}.sources Types: deb URIs: $repo_url Suites: $suite Components: $component Architectures: $architectures Signed-By: /etc/apt/keyrings/${name}.gpg EOF # Use cached apt update local apt_cache_file="/var/cache/apt-update-timestamp" local current_time=$(date +%s) local last_update=0 if [[ -f "$apt_cache_file" ]]; then last_update=$(cat "$apt_cache_file" 2>/dev/null || echo 0) fi # For repo changes, always update but respect short-term cache (30s) if ((current_time - last_update > 30)); then $STD apt update echo "$current_time" >"$apt_cache_file" fi msg_ok "Set up $name repository" } # ------------------------------------------------------------------------------ # Package version hold/unhold helpers # ------------------------------------------------------------------------------ hold_package_version() { local package="$1" $STD apt-mark hold "$package" } unhold_package_version() { local package="$1" $STD apt-mark unhold "$package" } # ------------------------------------------------------------------------------ # Safe service restart with verification # ------------------------------------------------------------------------------ safe_service_restart() { local service="$1" if systemctl is-active --quiet "$service"; then $STD systemctl restart "$service" else $STD systemctl start "$service" fi if ! systemctl is-active --quiet "$service"; then msg_error "Failed to start $service" systemctl status "$service" --no-pager return 1 fi return 0 } # ------------------------------------------------------------------------------ # Enable and start service (with error handling) # ------------------------------------------------------------------------------ enable_and_start_service() { local service="$1" if ! systemctl enable "$service" &>/dev/null; then msg_warn "Could not enable $service (may not be installed yet)" fi if ! systemctl start "$service" &>/dev/null; then msg_error "Failed to start $service" systemctl status "$service" --no-pager return 1 fi return 0 } # ------------------------------------------------------------------------------ # Check if service is enabled # ------------------------------------------------------------------------------ is_service_enabled() { local service="$1" systemctl is-enabled --quiet "$service" 2>/dev/null } # ------------------------------------------------------------------------------ # Check if service is running # ------------------------------------------------------------------------------ is_service_running() { local service="$1" systemctl is-active --quiet "$service" 2>/dev/null } # ------------------------------------------------------------------------------ # Extract version from JSON (GitHub releases) # ------------------------------------------------------------------------------ extract_version_from_json() { local json="$1" local field="${2:-tag_name}" local strip_v="${3:-true}" ensure_dependencies jq local version version=$(echo "$json" | jq -r ".${field} // empty") if [[ -z "$version" ]]; then return 1 fi if [[ "$strip_v" == "true" ]]; then echo "${version#v}" else echo "$version" fi } # ------------------------------------------------------------------------------ # Get latest GitHub release version # ------------------------------------------------------------------------------ get_latest_github_release() { local repo="$1" local strip_v="${2:-true}" local temp_file=$(mktemp) if ! github_api_call "https://api.github.com/repos/${repo}/releases/latest" "$temp_file"; then rm -f "$temp_file" return 1 fi local version version=$(extract_version_from_json "$(cat "$temp_file")" "tag_name" "$strip_v") rm -f "$temp_file" if [[ -z "$version" ]]; then return 1 fi echo "$version" } # ------------------------------------------------------------------------------ # Debug logging (only if DEBUG=1) # ------------------------------------------------------------------------------ debug_log() { [[ "${DEBUG:-0}" == "1" ]] && echo "[DEBUG] $*" >&2 } # ------------------------------------------------------------------------------ # Performance timing helper # ------------------------------------------------------------------------------ start_timer() { echo $(date +%s) } end_timer() { local start_time="$1" local label="${2:-Operation}" local end_time=$(date +%s) local duration=$((end_time - start_time)) debug_log "$label took ${duration}s" } # ------------------------------------------------------------------------------ # GPG key fingerprint verification # ------------------------------------------------------------------------------ verify_gpg_fingerprint() { local key_file="$1" local expected_fingerprint="$2" local actual_fingerprint actual_fingerprint=$(gpg --show-keys --with-fingerprint --with-colons "$key_file" 2>&1 | grep -m1 '^fpr:' | cut -d: -f10) if [[ "$actual_fingerprint" == "$expected_fingerprint" ]]; then return 0 fi msg_error "GPG fingerprint mismatch! Expected: $expected_fingerprint, Got: $actual_fingerprint" return 1 } # ============================================================================== # EXISTING FUNCTIONS # ============================================================================== # ------------------------------------------------------------------------------ # Checks for new GitHub release (latest tag). # # Description: # - Queries the GitHub API for the latest release tag # - Compares it to a local cached version (~/.) # - If newer, sets global CHECK_UPDATE_RELEASE and returns 0 # # Usage: # if check_for_gh_release "flaresolverr" "FlareSolverr/FlareSolverr" [optional] "v1.1.1"; then # # trigger update... # fi # exit 0 # } (end of update_script not from the function) # # Notes: # - Requires `jq` (auto-installed if missing) # - Does not modify anything, only checks version state # - Does not support pre-releases # ------------------------------------------------------------------------------ check_for_gh_release() { local app="$1" local source="$2" local pinned_version_in="${3:-}" # optional local app_lc="${app,,}" local current_file="$HOME/.${app_lc}" msg_info "Checking for update: ${app}" # DNS check if ! getent hosts api.github.com >/dev/null 2>&1; then msg_error "Network error: cannot resolve api.github.com" return 1 fi ensure_dependencies jq # Fetch releases and exclude drafts/prereleases local releases_json releases_json=$(curl -fsSL --max-time 20 \ -H 'Accept: application/vnd.github+json' \ -H 'X-GitHub-Api-Version: 2022-11-28' \ "https://api.github.com/repos/${source}/releases") || { msg_error "Unable to fetch releases for ${app}" return 1 } mapfile -t raw_tags < <(jq -r '.[] | select(.draft==false and .prerelease==false) | .tag_name' <<<"$releases_json") if ((${#raw_tags[@]} == 0)); then msg_error "No stable releases found for ${app}" return 1 fi local clean_tags=() for t in "${raw_tags[@]}"; do clean_tags+=("${t#v}") done local latest_raw="${raw_tags[0]}" local latest_clean="${clean_tags[0]}" # current installed (stored without v) local current="" if [[ -f "$current_file" ]]; then current="$(<"$current_file")" else # Migration: search for any /opt/*_version.txt local legacy_files mapfile -t legacy_files < <(find /opt -maxdepth 1 -type f -name "*_version.txt" 2>/dev/null) if ((${#legacy_files[@]} == 1)); then current="$(<"${legacy_files[0]}")" echo "${current#v}" >"$current_file" rm -f "${legacy_files[0]}" fi fi current="${current#v}" # Pinned version handling if [[ -n "$pinned_version_in" ]]; then local pin_clean="${pinned_version_in#v}" local match_raw="" for i in "${!clean_tags[@]}"; do if [[ "${clean_tags[$i]}" == "$pin_clean" ]]; then match_raw="${raw_tags[$i]}" break fi done if [[ -z "$match_raw" ]]; then msg_error "Pinned version ${pinned_version_in} not found upstream" return 1 fi if [[ "$current" != "$pin_clean" ]]; then msg_info "${app} pinned to ${pinned_version_in} (installed ${current:-none}) → update required" CHECK_UPDATE_RELEASE="$match_raw" return 0 fi if [[ "$pin_clean" == "$latest_clean" ]]; then msg_ok "${app} pinned to ${pinned_version_in} (up to date)" else msg_ok "${app} pinned to ${pinned_version_in} (already installed, upstream ${latest_raw})" fi return 1 fi # No pinning → use latest if [[ -z "$current" || "$current" != "$latest_clean" ]]; then CHECK_UPDATE_RELEASE="$latest_raw" msg_info "New release available: ${latest_raw} (current: v${current:-none})" return 0 fi msg_ok "${app} is up to date (${latest_raw})" return 1 } # ------------------------------------------------------------------------------ # Creates and installs self-signed certificates. # # Description: # - Create a self-signed certificate with option to override application name # # Variables: # APP - Application name (default: $APPLICATION variable) # ------------------------------------------------------------------------------ create_self_signed_cert() { local APP_NAME="${1:-${APPLICATION}}" local CERT_DIR="/etc/ssl/${APP_NAME}" local CERT_KEY="${CERT_DIR}/${APP_NAME}.key" local CERT_CRT="${CERT_DIR}/${APP_NAME}.crt" if [[ -f "$CERT_CRT" && -f "$CERT_KEY" ]]; then return 0 fi $STD apt update $STD apt install -y openssl mkdir -p "$CERT_DIR" $STD openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 \ -subj "/C=US/ST=State/L=City/O=Organization/CN=${APP_NAME}" \ -keyout "$CERT_KEY" \ -out "$CERT_CRT" chmod 600 "$CERT_KEY" chmod 644 "$CERT_CRT" } # ------------------------------------------------------------------------------ # Downloads file with optional progress indicator using pv. # # Arguments: # $1 - URL # $2 - Destination path # ------------------------------------------------------------------------------ function download_with_progress() { local url="$1" local output="$2" if [ -n "$SPINNER_PID" ] && ps -p "$SPINNER_PID" >/dev/null; then kill "$SPINNER_PID" >/dev/null; fi ensure_dependencies pv set -o pipefail # Content-Length aus HTTP-Header holen local content_length content_length=$(curl -fsSLI "$url" | awk '/Content-Length/ {print $2}' | tr -d '\r' || true) if [[ -z "$content_length" ]]; then if ! curl -fL# -o "$output" "$url"; then msg_error "Download failed" return 1 fi else if ! curl -fsSL "$url" | pv -s "$content_length" >"$output"; then msg_error "Download failed" return 1 fi fi } # ------------------------------------------------------------------------------ # Ensures /usr/local/bin is permanently in system PATH. # # Description: # - Adds to /etc/profile.d if not present # ------------------------------------------------------------------------------ function ensure_usr_local_bin_persist() { local PROFILE_FILE="/etc/profile.d/custom_path.sh" if [[ ! -f "$PROFILE_FILE" ]] && ! command -v pveversion &>/dev/null; then echo 'export PATH="/usr/local/bin:$PATH"' >"$PROFILE_FILE" chmod +x "$PROFILE_FILE" fi } # ------------------------------------------------------------------------------ # Downloads and deploys latest GitHub release (source, binary, tarball, asset). # # Description: # - Fetches latest release metadata from GitHub API # - Supports the following modes: # - tarball: Source code tarball (default if omitted) # - source: Alias for tarball (same behavior) # - binary: .deb package install (arch-dependent) # - prebuild: Prebuilt .tar.gz archive (e.g. Go binaries) # - singlefile: Standalone binary (no archive, direct chmod +x install) # - Handles download, extraction/installation and version tracking in ~/. # # Parameters: # $1 APP - Application name (used for install path and version file) # $2 REPO - GitHub repository in form user/repo # $3 MODE - Release type: # tarball → source tarball (.tar.gz) # binary → .deb file (auto-arch matched) # prebuild → prebuilt archive (e.g. tar.gz) # singlefile→ standalone binary (chmod +x) # $4 VERSION - Optional release tag (default: latest) # $5 TARGET_DIR - Optional install path (default: /opt/) # $6 ASSET_FILENAME - Required for: # - prebuild → archive filename or pattern # - singlefile→ binary filename or pattern # # Optional: # - Set GITHUB_TOKEN env var to increase API rate limit (recommended for CI/CD). # # Examples: # # 1. Minimal: Fetch and deploy source tarball # fetch_and_deploy_gh_release "myapp" "myuser/myapp" # # # 2. Binary install via .deb asset (architecture auto-detected) # fetch_and_deploy_gh_release "myapp" "myuser/myapp" "binary" # # # 3. Prebuilt archive (.tar.gz) with asset filename match # fetch_and_deploy_gh_release "hanko" "teamhanko/hanko" "prebuild" "latest" "/opt/hanko" "hanko_Linux_x86_64.tar.gz" # # # 4. Single binary (chmod +x) like Argus, Promtail etc. # fetch_and_deploy_gh_release "argus" "release-argus/Argus" "singlefile" "0.26.3" "/opt/argus" "Argus-.*linux-amd64" # ------------------------------------------------------------------------------ function fetch_and_deploy_gh_release() { local app="$1" local repo="$2" local mode="${3:-tarball}" # tarball | binary | prebuild | singlefile local version="${4:-latest}" local target="${5:-/opt/$app}" local asset_pattern="${6:-}" local app_lc=$(echo "${app,,}" | tr -d ' ') local version_file="$HOME/.${app_lc}" local api_timeout="--connect-timeout 10 --max-time 60" local download_timeout="--connect-timeout 15 --max-time 900" local current_version="" [[ -f "$version_file" ]] && current_version=$(<"$version_file") ensure_dependencies jq local api_url="https://api.github.com/repos/$repo/releases" [[ "$version" != "latest" ]] && api_url="$api_url/tags/$version" || api_url="$api_url/latest" local header=() [[ -n "${GITHUB_TOKEN:-}" ]] && header=(-H "Authorization: token $GITHUB_TOKEN") # dns pre check local gh_host gh_host=$(awk -F/ '{print $3}' <<<"$api_url") if ! getent hosts "$gh_host" &>/dev/null; then msg_error "DNS resolution failed for $gh_host – check /etc/resolv.conf or networking" return 1 fi local max_retries=3 retry_delay=2 attempt=1 success=false resp http_code while ((attempt <= max_retries)); do resp=$(curl $api_timeout -fsSL -w "%{http_code}" -o /tmp/gh_rel.json "${header[@]}" "$api_url") && success=true && break sleep "$retry_delay" ((attempt++)) done if ! $success; then msg_error "Failed to fetch release metadata from $api_url after $max_retries attempts" return 1 fi http_code="${resp:(-3)}" [[ "$http_code" != "200" ]] && { msg_error "GitHub API returned HTTP $http_code" return 1 } local json tag_name json=$(/dev/null || uname -m) [[ "$arch" == "x86_64" ]] && arch="amd64" [[ "$arch" == "aarch64" ]] && arch="arm64" local assets url_match="" assets=$(echo "$json" | jq -r '.assets[].browser_download_url') # If explicit filename pattern is provided (param $6), match that first if [[ -n "$asset_pattern" ]]; then for u in $assets; do case "${u##*/}" in $asset_pattern) url_match="$u" break ;; esac done fi # If no match via explicit pattern, fall back to architecture heuristic if [[ -z "$url_match" ]]; then for u in $assets; do if [[ "$u" =~ ($arch|amd64|x86_64|aarch64|arm64).*\.deb$ ]]; then url_match="$u" break fi done fi # Fallback: any .deb file if [[ -z "$url_match" ]]; then for u in $assets; do [[ "$u" =~ \.deb$ ]] && url_match="$u" && break done fi if [[ -z "$url_match" ]]; then msg_error "No suitable .deb asset found for $app" rm -rf "$tmpdir" return 1 fi filename="${url_match##*/}" curl $download_timeout -fsSL -o "$tmpdir/$filename" "$url_match" || { msg_error "Download failed: $url_match" rm -rf "$tmpdir" return 1 } chmod 644 "$tmpdir/$filename" $STD apt install -y "$tmpdir/$filename" || { $STD dpkg -i "$tmpdir/$filename" || { msg_error "Both apt and dpkg installation failed" rm -rf "$tmpdir" return 1 } } ### Prebuild Mode ### elif [[ "$mode" == "prebuild" ]]; then local pattern="${6%\"}" pattern="${pattern#\"}" [[ -z "$pattern" ]] && { msg_error "Mode 'prebuild' requires 6th parameter (asset filename pattern)" rm -rf "$tmpdir" return 1 } local asset_url="" for u in $(echo "$json" | jq -r '.assets[].browser_download_url'); do filename_candidate="${u##*/}" case "$filename_candidate" in $pattern) asset_url="$u" break ;; esac done [[ -z "$asset_url" ]] && { msg_error "No asset matching '$pattern' found" rm -rf "$tmpdir" return 1 } filename="${asset_url##*/}" curl $download_timeout -fsSL -o "$tmpdir/$filename" "$asset_url" || { msg_error "Download failed: $asset_url" rm -rf "$tmpdir" return 1 } local unpack_tmp unpack_tmp=$(mktemp -d) mkdir -p "$target" if [[ "${CLEAN_INSTALL:-0}" == "1" ]]; then rm -rf "${target:?}/"* fi if [[ "$filename" == *.zip ]]; then ensure_dependencies unzip unzip -q "$tmpdir/$filename" -d "$unpack_tmp" elif [[ "$filename" == *.tar.* || "$filename" == *.tgz ]]; then tar -xf "$tmpdir/$filename" -C "$unpack_tmp" else msg_error "Unsupported archive format: $filename" rm -rf "$tmpdir" "$unpack_tmp" return 1 fi local top_dirs top_dirs=$(find "$unpack_tmp" -mindepth 1 -maxdepth 1 -type d | wc -l) local top_entries inner_dir top_entries=$(find "$unpack_tmp" -mindepth 1 -maxdepth 1) if [[ "$(echo "$top_entries" | wc -l)" -eq 1 && -d "$top_entries" ]]; then # Strip leading folder inner_dir="$top_entries" shopt -s dotglob nullglob if compgen -G "$inner_dir/*" >/dev/null; then cp -r "$inner_dir"/* "$target/" || { msg_error "Failed to copy contents from $inner_dir to $target" rm -rf "$tmpdir" "$unpack_tmp" return 1 } else msg_error "Inner directory is empty: $inner_dir" rm -rf "$tmpdir" "$unpack_tmp" return 1 fi shopt -u dotglob nullglob else # Copy all contents shopt -s dotglob nullglob if compgen -G "$unpack_tmp/*" >/dev/null; then cp -r "$unpack_tmp"/* "$target/" || { msg_error "Failed to copy contents to $target" rm -rf "$tmpdir" "$unpack_tmp" return 1 } else msg_error "Unpacked archive is empty" rm -rf "$tmpdir" "$unpack_tmp" return 1 fi shopt -u dotglob nullglob fi ### Singlefile Mode ### elif [[ "$mode" == "singlefile" ]]; then local pattern="${6%\"}" pattern="${pattern#\"}" [[ -z "$pattern" ]] && { msg_error "Mode 'singlefile' requires 6th parameter (asset filename pattern)" rm -rf "$tmpdir" return 1 } local asset_url="" for u in $(echo "$json" | jq -r '.assets[].browser_download_url'); do filename_candidate="${u##*/}" case "$filename_candidate" in $pattern) asset_url="$u" break ;; esac done [[ -z "$asset_url" ]] && { msg_error "No asset matching '$pattern' found" rm -rf "$tmpdir" return 1 } filename="${asset_url##*/}" mkdir -p "$target" local use_filename="${USE_ORIGINAL_FILENAME:-false}" local target_file="$app" [[ "$use_filename" == "true" ]] && target_file="$filename" curl $download_timeout -fsSL -o "$target/$target_file" "$asset_url" || { msg_error "Download failed: $asset_url" rm -rf "$tmpdir" return 1 } if [[ "$target_file" != *.jar && -f "$target/$target_file" ]]; then chmod +x "$target/$target_file" fi else msg_error "Unknown mode: $mode" rm -rf "$tmpdir" return 1 fi echo "$version" >"$version_file" msg_ok "Deployed: $app ($version)" rm -rf "$tmpdir" } # ------------------------------------------------------------------------------ # Loads LOCAL_IP from persistent store or detects if missing. # # Description: # - Loads from /run/local-ip.env or performs runtime lookup # ------------------------------------------------------------------------------ function import_local_ip() { local IP_FILE="/run/local-ip.env" if [[ -f "$IP_FILE" ]]; then # shellcheck disable=SC1090 source "$IP_FILE" fi if [[ -z "${LOCAL_IP:-}" ]]; then get_current_ip() { local targets=("8.8.8.8" "1.1.1.1" "192.168.1.1" "10.0.0.1" "172.16.0.1" "default") local ip for target in "${targets[@]}"; do if [[ "$target" == "default" ]]; then ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') else ip=$(ip route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') fi if [[ -n "$ip" ]]; then echo "$ip" return 0 fi done return 1 } LOCAL_IP="$(get_current_ip || true)" if [[ -z "$LOCAL_IP" ]]; then msg_error "Could not determine LOCAL_IP" return 1 fi fi export LOCAL_IP } # ------------------------------------------------------------------------------ # Installs Adminer (Debian/Ubuntu via APT, Alpine via direct download). # # Description: # - Adds Adminer to Apache or web root # - Supports Alpine and Debian-based systems # ------------------------------------------------------------------------------ function setup_adminer() { local CACHED_VERSION CACHED_VERSION=$(get_cached_version "adminer") if grep -qi alpine /etc/os-release; then msg_info "Installing Adminer (Alpine)" mkdir -p /var/www/localhost/htdocs/adminer curl -fsSL https://github.com/vrana/adminer/releases/latest/download/adminer.php \ -o /var/www/localhost/htdocs/adminer/index.php || { msg_error "Failed to download Adminer" return 1 } cache_installed_version "adminer" "latest-alpine" msg_ok "Installed Adminer (Alpine)" else msg_info "Installing Adminer (Debian/Ubuntu)" ensure_dependencies adminer $STD a2enconf adminer $STD systemctl reload apache2 local VERSION VERSION=$(dpkg -s adminer 2>/dev/null | grep '^Version:' | awk '{print $2}') cache_installed_version "adminer" "${VERSION:-unknown}" msg_ok "Installed Adminer (Debian/Ubuntu)" fi } # ------------------------------------------------------------------------------ # Installs or updates Composer globally (robust, idempotent). # # - Installs to /usr/local/bin/composer # - Removes old binaries/symlinks in /usr/bin, /bin, /root/.composer, etc. # - Ensures /usr/local/bin is in PATH (permanent) # ------------------------------------------------------------------------------ function setup_composer() { local COMPOSER_BIN="/usr/local/bin/composer" local CACHED_VERSION CACHED_VERSION=$(get_cached_version "composer") export COMPOSER_ALLOW_SUPERUSER=1 for old in /usr/bin/composer /bin/composer /root/.composer/vendor/bin/composer; do [[ -e "$old" && "$old" != "$COMPOSER_BIN" ]] && rm -f "$old" done ensure_usr_local_bin_persist export PATH="/usr/local/bin:$PATH" if [[ -x "$COMPOSER_BIN" ]]; then local CURRENT_VERSION CURRENT_VERSION=$("$COMPOSER_BIN" --version 2>/dev/null | awk '{print $3}') msg_info "Updating Composer from $CURRENT_VERSION to latest" else msg_info "Installing Composer" fi curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php || { msg_error "Failed to download Composer installer" return 1 } $STD php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer rm -f /tmp/composer-setup.php if [[ ! -x "$COMPOSER_BIN" ]]; then msg_error "Composer installation failed" return 1 fi chmod +x "$COMPOSER_BIN" $STD "$COMPOSER_BIN" self-update --no-interaction || true local FINAL_VERSION FINAL_VERSION=$("$COMPOSER_BIN" --version 2>/dev/null | awk '{print $3}') cache_installed_version "composer" "$FINAL_VERSION" msg_ok "Installed Composer $FINAL_VERSION" } # ------------------------------------------------------------------------------ # Installs FFmpeg from source or prebuilt binary (Debian/Ubuntu only). # # Description: # - Downloads and builds FFmpeg from GitHub (https://github.com/FFmpeg/FFmpeg) # - Supports specific version override via FFMPEG_VERSION (e.g. n7.1.1) # - Supports build profile via FFMPEG_TYPE: # - minimal : x264, vpx, mp3 only # - medium : adds subtitles, fonts, opus, vorbis # - full : adds dav1d, svt-av1, zlib, numa # - binary : downloads static build (johnvansickle.com) # - Defaults to latest stable version and full feature set # # Notes: # - Requires: curl, jq, build-essential, and matching codec libraries # - Result is installed to /usr/local/bin/ffmpeg # ------------------------------------------------------------------------------ function setup_ffmpeg() { local TMP_DIR=$(mktemp -d) local GITHUB_REPO="FFmpeg/FFmpeg" local VERSION="${FFMPEG_VERSION:-latest}" local TYPE="${FFMPEG_TYPE:-full}" local BIN_PATH="/usr/local/bin/ffmpeg" local CACHED_VERSION CACHED_VERSION=$(get_cached_version "ffmpeg") # Binary fallback mode if [[ "$TYPE" == "binary" ]]; then msg_info "Installing FFmpeg (static binary)" curl -fsSL https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz -o "$TMP_DIR/ffmpeg.tar.xz" || { msg_error "Failed to download FFmpeg binary" rm -rf "$TMP_DIR" return 1 } tar -xf "$TMP_DIR/ffmpeg.tar.xz" -C "$TMP_DIR" local EXTRACTED_DIR EXTRACTED_DIR=$(find "$TMP_DIR" -maxdepth 1 -type d -name "ffmpeg-*") cp "$EXTRACTED_DIR/ffmpeg" "$BIN_PATH" cp "$EXTRACTED_DIR/ffprobe" /usr/local/bin/ffprobe chmod +x "$BIN_PATH" /usr/local/bin/ffprobe local FINAL_VERSION=$($BIN_PATH -version | head -n1 | awk '{print $3}') rm -rf "$TMP_DIR" cache_installed_version "ffmpeg" "$FINAL_VERSION" ensure_usr_local_bin_persist msg_ok "Installed FFmpeg binary $FINAL_VERSION" return 0 fi ensure_dependencies jq # Auto-detect latest stable version if none specified if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then VERSION=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/tags" | jq -r '.[].name' | grep -E '^n[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1) fi if [[ -z "$VERSION" ]]; then msg_error "Could not determine FFmpeg version" rm -rf "$TMP_DIR" return 1 fi msg_info "Installing FFmpeg ${VERSION} ($TYPE)" # Dependency selection local DEPS=(build-essential yasm nasm pkg-config) case "$TYPE" in minimal) DEPS+=(libx264-dev libvpx-dev libmp3lame-dev) ;; medium) DEPS+=(libx264-dev libvpx-dev libmp3lame-dev libfreetype6-dev libass-dev libopus-dev libvorbis-dev) ;; full) DEPS+=( libx264-dev libx265-dev libvpx-dev libmp3lame-dev libfreetype6-dev libass-dev libopus-dev libvorbis-dev libdav1d-dev libsvtav1-dev zlib1g-dev libnuma-dev libva-dev libdrm-dev ) ;; *) msg_error "Invalid FFMPEG_TYPE: $TYPE" rm -rf "$TMP_DIR" return 1 ;; esac ensure_dependencies "${DEPS[@]}" curl -fsSL "https://github.com/${GITHUB_REPO}/archive/refs/tags/${VERSION}.tar.gz" -o "$TMP_DIR/ffmpeg.tar.gz" || { msg_error "Failed to download FFmpeg source" rm -rf "$TMP_DIR" return 1 } tar -xzf "$TMP_DIR/ffmpeg.tar.gz" -C "$TMP_DIR" || { msg_error "Failed to extract FFmpeg" rm -rf "$TMP_DIR" return 1 } cd "$TMP_DIR/FFmpeg-"* || { msg_error "Source extraction failed" rm -rf "$TMP_DIR" return 1 } local args=( --enable-gpl --enable-shared --enable-nonfree --disable-static --enable-libx264 --enable-libvpx --enable-libmp3lame ) if [[ "$TYPE" != "minimal" ]]; then args+=(--enable-libfreetype --enable-libass --enable-libopus --enable-libvorbis) fi if [[ "$TYPE" == "full" ]]; then args+=(--enable-libx265 --enable-libdav1d --enable-zlib) args+=(--enable-vaapi --enable-libdrm) fi if [[ ${#args[@]} -eq 0 ]]; then msg_error "FFmpeg configure args array is empty" rm -rf "$TMP_DIR" return 1 fi $STD ./configure "${args[@]}" $STD make -j"$(nproc)" $STD make install echo "/usr/local/lib" >/etc/ld.so.conf.d/ffmpeg.conf $STD ldconfig ldconfig -p | grep libavdevice >/dev/null || { msg_error "libavdevice not registered with dynamic linker" rm -rf "$TMP_DIR" return 1 } if ! command -v ffmpeg &>/dev/null; then msg_error "FFmpeg installation failed" rm -rf "$TMP_DIR" return 1 fi local FINAL_VERSION FINAL_VERSION=$(ffmpeg -version | head -n1 | awk '{print $3}') rm -rf "$TMP_DIR" cache_installed_version "ffmpeg" "$FINAL_VERSION" ensure_usr_local_bin_persist msg_ok "Installed FFmpeg $FINAL_VERSION" } # ------------------------------------------------------------------------------ # Installs Go (Golang) from official tarball. # # Description: # - Determines system architecture # - Downloads latest version if GO_VERSION not set # # Variables: # GO_VERSION - Version to install (e.g. 1.22.2 or latest) # ------------------------------------------------------------------------------ function setup_go() { local ARCH case "$(uname -m)" in x86_64) ARCH="amd64" ;; aarch64) ARCH="arm64" ;; *) msg_error "Unsupported architecture: $(uname -m)" return 1 ;; esac # Determine version if [[ -z "${GO_VERSION:-}" || "${GO_VERSION}" == "latest" ]]; then GO_VERSION=$(curl -fsSL https://go.dev/VERSION?m=text | head -n1 | sed 's/^go//') if [[ -z "$GO_VERSION" ]]; then msg_error "Could not determine latest Go version" return 1 fi fi local GO_BIN="/usr/local/bin/go" local GO_INSTALL_DIR="/usr/local/go" local CACHED_VERSION CACHED_VERSION=$(get_cached_version "go") if [[ -x "$GO_BIN" ]]; then local CURRENT_VERSION CURRENT_VERSION=$("$GO_BIN" version | awk '{print $3}' | sed 's/go//') if [[ "$CURRENT_VERSION" == "$GO_VERSION" ]]; then if [[ "$CACHED_VERSION" == "$GO_VERSION" ]]; then return 0 fi cache_installed_version "go" "$GO_VERSION" return 0 else msg_info "Upgrading Go from $CURRENT_VERSION to $GO_VERSION" rm -rf "$GO_INSTALL_DIR" fi else msg_info "Installing Go $GO_VERSION" fi local TARBALL="go${GO_VERSION}.linux-${ARCH}.tar.gz" local URL="https://go.dev/dl/${TARBALL}" local TMP_TAR=$(mktemp) curl -fsSL "$URL" -o "$TMP_TAR" || { msg_error "Failed to download $TARBALL" rm -f "$TMP_TAR" return 1 } $STD tar -C /usr/local -xzf "$TMP_TAR" ln -sf /usr/local/go/bin/go /usr/local/bin/go ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt rm -f "$TMP_TAR" cache_installed_version "go" "$GO_VERSION" ensure_usr_local_bin_persist msg_ok "Installed Go $GO_VERSION" } # ------------------------------------------------------------------------------ # Installs or updates Ghostscript (gs) from source. # # Description: # - Fetches latest release # - Builds and installs system-wide # ------------------------------------------------------------------------------ function setup_gs() { local TMP_DIR=$(mktemp -d) local CURRENT_VERSION=$(gs --version 2>/dev/null || echo "0") local CACHED_VERSION CACHED_VERSION=$(get_cached_version "ghostscript") ensure_dependencies jq local RELEASE_JSON RELEASE_JSON=$(curl -fsSL https://api.github.com/repos/ArtifexSoftware/ghostpdl-downloads/releases/latest) local LATEST_VERSION LATEST_VERSION=$(echo "$RELEASE_JSON" | jq -r '.tag_name' | sed 's/^gs//') local LATEST_VERSION_DOTTED LATEST_VERSION_DOTTED=$(echo "$RELEASE_JSON" | jq -r '.name' | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+') if [[ -z "$LATEST_VERSION" || -z "$LATEST_VERSION_DOTTED" ]]; then msg_error "Could not determine latest Ghostscript version" rm -rf "$TMP_DIR" return 1 fi if dpkg --compare-versions "$CURRENT_VERSION" ge "$LATEST_VERSION_DOTTED" 2>/dev/null; then if [[ "$CACHED_VERSION" == "$LATEST_VERSION_DOTTED" ]]; then rm -rf "$TMP_DIR" return 0 fi cache_installed_version "ghostscript" "$LATEST_VERSION_DOTTED" rm -rf "$TMP_DIR" return 0 fi msg_info "Installing Ghostscript $LATEST_VERSION_DOTTED" curl -fsSL "https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs${LATEST_VERSION}/ghostscript-${LATEST_VERSION_DOTTED}.tar.gz" -o "$TMP_DIR/ghostscript.tar.gz" || { msg_error "Failed to download Ghostscript" rm -rf "$TMP_DIR" return 1 } if ! tar -xzf "$TMP_DIR/ghostscript.tar.gz" -C "$TMP_DIR"; then msg_error "Failed to extract Ghostscript archive" rm -rf "$TMP_DIR" return 1 fi cd "$TMP_DIR/ghostscript-${LATEST_VERSION_DOTTED}" || { msg_error "Failed to enter Ghostscript source directory" rm -rf "$TMP_DIR" return 1 } ensure_dependencies build-essential libpng-dev zlib1g-dev $STD ./configure $STD make -j"$(nproc)" $STD make install hash -r if [[ ! -x "$(command -v gs)" ]]; then if [[ -x /usr/local/bin/gs ]]; then ln -sf /usr/local/bin/gs /usr/bin/gs fi fi rm -rf "$TMP_DIR" cache_installed_version "ghostscript" "$LATEST_VERSION_DOTTED" ensure_usr_local_bin_persist msg_ok "Installed Ghostscript $LATEST_VERSION_DOTTED" } # ------------------------------------------------------------------------------ # Installs ImageMagick 7 from source (Debian/Ubuntu only). # # Description: # - Downloads the latest ImageMagick source tarball # - Builds and installs ImageMagick to /usr/local # - Configures dynamic linker (ldconfig) # # Notes: # - Requires: build-essential, libtool, libjpeg-dev, libpng-dev, etc. # ------------------------------------------------------------------------------ function setup_imagemagick() { local TMP_DIR=$(mktemp -d) local VERSION="" local BINARY_PATH="/usr/local/bin/magick" local CACHED_VERSION CACHED_VERSION=$(get_cached_version "imagemagick") if command -v magick &>/dev/null; then VERSION=$(magick -version | awk '/^Version/ {print $3}') if [[ "$CACHED_VERSION" == "$VERSION" ]]; then rm -rf "$TMP_DIR" return 0 fi cache_installed_version "imagemagick" "$VERSION" rm -rf "$TMP_DIR" return 0 fi msg_info "Installing ImageMagick (Patience)" ensure_dependencies \ build-essential \ libtool \ libjpeg-dev \ libpng-dev \ libtiff-dev \ libwebp-dev \ libheif-dev \ libde265-dev \ libopenjp2-7-dev \ libxml2-dev \ liblcms2-dev \ libfreetype6-dev \ libraw-dev \ libfftw3-dev \ liblqr-1-0-dev \ libgsl-dev \ pkg-config \ ghostscript curl -fsSL https://imagemagick.org/archive/ImageMagick.tar.gz -o "$TMP_DIR/ImageMagick.tar.gz" || { msg_error "Failed to download ImageMagick" rm -rf "$TMP_DIR" return 1 } tar -xzf "$TMP_DIR/ImageMagick.tar.gz" -C "$TMP_DIR" || { msg_error "Failed to extract ImageMagick" rm -rf "$TMP_DIR" return 1 } cd "$TMP_DIR"/ImageMagick-* || { msg_error "Source extraction failed" rm -rf "$TMP_DIR" return 1 } $STD ./configure --disable-static $STD make -j"$(nproc)" $STD make install $STD ldconfig /usr/local/lib if [[ ! -x "$BINARY_PATH" ]]; then msg_error "ImageMagick installation failed" rm -rf "$TMP_DIR" return 1 fi VERSION=$("$BINARY_PATH" -version | awk '/^Version/ {print $3}') rm -rf "$TMP_DIR" cache_installed_version "imagemagick" "$VERSION" ensure_usr_local_bin_persist msg_ok "Installed ImageMagick $VERSION" } # ------------------------------------------------------------------------------ # Installs Temurin JDK via Adoptium APT repository. # # Description: # - Removes previous JDK if version mismatch # - Installs or upgrades to specified JAVA_VERSION # # Variables: # JAVA_VERSION - Temurin JDK version to install (e.g. 17, 21) # ------------------------------------------------------------------------------ function setup_java() { local JAVA_VERSION="${JAVA_VERSION:-21}" local DISTRO_ID DISTRO_CODENAME DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') DISTRO_CODENAME=$(awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release) local DESIRED_PACKAGE="temurin-${JAVA_VERSION}-jdk" # Check cached version local CACHED_VERSION CACHED_VERSION=$(get_cached_version "temurin-jdk") # Add repo nur wenn nötig if [[ ! -f /etc/apt/sources.list.d/adoptium.sources ]]; then # Cleanup old repository files cleanup_old_repo_files "adoptium" # Use helper function to get fallback suite local SUITE SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://packages.adoptium.net/artifactory/deb") # Use standardized repo setup setup_deb822_repo \ "adoptium" \ "https://packages.adoptium.net/artifactory/api/gpg/key/public" \ "https://packages.adoptium.net/artifactory/deb" \ "$SUITE" \ "main" \ "amd64 arm64" fi local INSTALLED_VERSION="" if dpkg -l | grep -q "temurin-.*-jdk"; then INSTALLED_VERSION=$(dpkg -l | awk '/temurin-.*-jdk/{print $2}' | grep -oP 'temurin-\K[0-9]+') fi if [[ "$INSTALLED_VERSION" == "$JAVA_VERSION" ]]; then if [[ "$CACHED_VERSION" == "$JAVA_VERSION" ]]; then # Already at correct version, just upgrade if available upgrade_package "$DESIRED_PACKAGE" else msg_info "Upgrading Temurin JDK $JAVA_VERSION" $STD apt update $STD apt install --only-upgrade -y "$DESIRED_PACKAGE" cache_installed_version "temurin-jdk" "$JAVA_VERSION" msg_ok "Upgraded Temurin JDK $JAVA_VERSION" fi else if [[ -n "$INSTALLED_VERSION" ]]; then msg_info "Removing old Temurin JDK $INSTALLED_VERSION" $STD apt purge -y "temurin-${INSTALLED_VERSION}-jdk" msg_ok "Removed old Temurin JDK" fi msg_info "Installing Temurin JDK $JAVA_VERSION" $STD apt install -y "$DESIRED_PACKAGE" cache_installed_version "temurin-jdk" "$JAVA_VERSION" msg_ok "Installed Temurin JDK $JAVA_VERSION" fi } # ------------------------------------------------------------------------------ # Installs a local IP updater script using networkd-dispatcher. # # Description: # - Stores current IP in /run/local-ip.env # - Automatically runs on network changes # ------------------------------------------------------------------------------ function setup_local_ip_helper() { local BASE_DIR="/usr/local/community-scripts/ip-management" local SCRIPT_PATH="$BASE_DIR/update_local_ip.sh" local IP_FILE="/run/local-ip.env" local DISPATCHER_SCRIPT="/etc/networkd-dispatcher/routable.d/10-update-local-ip.sh" mkdir -p "$BASE_DIR" # Install networkd-dispatcher if not present if ! dpkg -s networkd-dispatcher >/dev/null 2>&1; then $STD apt update $STD apt install -y networkd-dispatcher fi # Write update_local_ip.sh cat <<'EOF' >"$SCRIPT_PATH" #!/bin/bash set -euo pipefail IP_FILE="/run/local-ip.env" mkdir -p "$(dirname "$IP_FILE")" get_current_ip() { local targets=("8.8.8.8" "1.1.1.1" "192.168.1.1" "10.0.0.1" "172.16.0.1" "default") local ip for target in "${targets[@]}"; do if [[ "$target" == "default" ]]; then ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') else ip=$(ip route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') fi if [[ -n "$ip" ]]; then echo "$ip" return 0 fi done return 1 } current_ip="$(get_current_ip)" if [[ -z "$current_ip" ]]; then echo "[ERROR] Could not detect local IP" >&2 exit 1 fi if [[ -f "$IP_FILE" ]]; then source "$IP_FILE" [[ "$LOCAL_IP" == "$current_ip" ]] && exit 0 fi echo "LOCAL_IP=$current_ip" > "$IP_FILE" echo "[INFO] LOCAL_IP updated to $current_ip" EOF chmod +x "$SCRIPT_PATH" # Install dispatcher hook mkdir -p "$(dirname "$DISPATCHER_SCRIPT")" cat <"$DISPATCHER_SCRIPT" #!/bin/bash $SCRIPT_PATH EOF chmod +x "$DISPATCHER_SCRIPT" systemctl enable -q --now networkd-dispatcher.service } # ------------------------------------------------------------------------------ # Installs or updates MariaDB from official repo. # # Description: # - Detects current MariaDB version and replaces it if necessary # - Preserves existing database data # - Dynamically determines latest GA version if "latest" is given # # Variables: # MARIADB_VERSION - MariaDB version to install (e.g. 10.11, latest) (default: latest) # ------------------------------------------------------------------------------ setup_mariadb() { local MARIADB_VERSION="${MARIADB_VERSION:-latest}" local DISTRO_ID DISTRO_CODENAME DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) if ! curl -fsI http://mirror.mariadb.org/repo/ >/dev/null; then msg_error "MariaDB mirror not reachable" return 1 fi if [[ "$MARIADB_VERSION" == "latest" ]]; then MARIADB_VERSION=$(curl -fsSL http://mirror.mariadb.org/repo/ | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+/' | grep -vE 'rc/|rolling/' | sed 's|/||' | sort -Vr | head -n1) if [[ -z "$MARIADB_VERSION" ]]; then msg_error "Could not determine latest GA MariaDB version" return 1 fi fi local CURRENT_VERSION="" if command -v mariadb >/dev/null; then CURRENT_VERSION=$(mariadb --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') fi local CACHED_VERSION CACHED_VERSION=$(get_cached_version "mariadb") if [[ "$CURRENT_VERSION" == "$MARIADB_VERSION" ]]; then if [[ "$CACHED_VERSION" == "$MARIADB_VERSION" ]]; then upgrade_package mariadb-server upgrade_package mariadb-client else msg_info "Upgrading MariaDB $MARIADB_VERSION" $STD apt update $STD apt install --only-upgrade -y mariadb-server mariadb-client cache_installed_version "mariadb" "$MARIADB_VERSION" msg_ok "Upgraded MariaDB $MARIADB_VERSION" fi return 0 fi msg_info "Installing MariaDB $MARIADB_VERSION" if [[ -n "$CURRENT_VERSION" ]]; then $STD systemctl stop mariadb >/dev/null 2>&1 || true $STD apt purge -y 'mariadb*' || true fi # Cleanup old repository files cleanup_old_repo_files "mariadb" # Use helper function to get fallback suite local SUITE SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "http://mirror.mariadb.org/repo/${MARIADB_VERSION}/${DISTRO_ID}") # Use standardized repo setup setup_deb822_repo \ "mariadb" \ "https://mariadb.org/mariadb_release_signing_key.asc" \ "http://mirror.mariadb.org/repo/${MARIADB_VERSION}/${DISTRO_ID}" \ "$SUITE" \ "main" \ "amd64 arm64" local MARIADB_MAJOR_MINOR MARIADB_MAJOR_MINOR=$(echo "$MARIADB_VERSION" | awk -F. '{print $1"."$2}') if [[ -n "$MARIADB_MAJOR_MINOR" ]]; then echo "mariadb-server-$MARIADB_MAJOR_MINOR mariadb-server/feedback boolean false" | debconf-set-selections fi DEBIAN_FRONTEND=noninteractive $STD apt install -y mariadb-server mariadb-client || { cleanup_old_repo_files "mariadb" $STD apt update DEBIAN_FRONTEND=noninteractive $STD apt install -y mariadb-server mariadb-client } cache_installed_version "mariadb" "$MARIADB_VERSION" msg_ok "Installed MariaDB $MARIADB_VERSION" } # ------------------------------------------------------------------------------ # Installs or updates MongoDB to specified major version. # # Description: # - Preserves data across installations # - Adds official MongoDB repo # # Variables: # MONGO_VERSION - MongoDB major version to install (e.g. 7.0, 8.0) # ------------------------------------------------------------------------------ function setup_mongodb() { local MONGO_VERSION="${MONGO_VERSION:-8.0}" local DISTRO_ID DISTRO_CODENAME DISTRO_ID=$(awk -F= '/^ID=/{ gsub(/"/,"",$2); print $2 }' /etc/os-release) DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{ print $2 }' /etc/os-release) # Check AVX support if ! grep -qm1 'avx[^ ]*' /proc/cpuinfo; then local major="${MONGO_VERSION%%.*}" if ((major > 5)); then msg_error "MongoDB ${MONGO_VERSION} requires AVX support, which is not available on this system." return 1 fi fi case "$DISTRO_ID" in ubuntu) MONGO_BASE_URL="https://repo.mongodb.org/apt/ubuntu" REPO_COMPONENT="multiverse" ;; debian) MONGO_BASE_URL="https://repo.mongodb.org/apt/debian" REPO_COMPONENT="main" ;; *) msg_error "Unsupported distribution: $DISTRO_ID" return 1 ;; esac local INSTALLED_VERSION="" if command -v mongod >/dev/null; then INSTALLED_VERSION=$(mongod --version | awk '/db version/{print $3}' | cut -d. -f1,2) fi local CACHED_VERSION CACHED_VERSION=$(get_cached_version "mongodb") if [[ "$INSTALLED_VERSION" == "$MONGO_VERSION" ]]; then if [[ "$CACHED_VERSION" == "$MONGO_VERSION" ]]; then upgrade_package mongodb-org else msg_info "Upgrading MongoDB $MONGO_VERSION" $STD apt update $STD apt install --only-upgrade -y mongodb-org cache_installed_version "mongodb" "$MONGO_VERSION" msg_ok "Upgraded MongoDB $MONGO_VERSION" fi return 0 fi msg_info "Installing MongoDB $MONGO_VERSION" if [[ -n "$INSTALLED_VERSION" ]]; then $STD systemctl stop mongod || true $STD apt purge -y mongodb-org || true fi # Cleanup old repository files cleanup_old_repo_files "mongodb-org-${MONGO_VERSION}" # Use helper function to get fallback suite local SUITE SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "$MONGO_BASE_URL") # Use standardized repo setup mkdir -p /etc/apt/keyrings curl -fsSL "https://pgp.mongodb.com/server-${MONGO_VERSION}.asc" | gpg --dearmor --yes -o "/etc/apt/keyrings/mongodb-${MONGO_VERSION}.gpg" cat </etc/apt/sources.list.d/mongodb-org-${MONGO_VERSION}.sources Types: deb URIs: ${MONGO_BASE_URL} Suites: ${SUITE}/mongodb-org/${MONGO_VERSION} Components: ${REPO_COMPONENT} Architectures: amd64 arm64 Signed-By: /etc/apt/keyrings/mongodb-${MONGO_VERSION}.gpg EOF $STD apt update || { msg_error "APT update failed — invalid MongoDB repo for ${DISTRO_ID}-${DISTRO_CODENAME}?" return 1 } $STD apt install -y mongodb-org mkdir -p /var/lib/mongodb chown -R mongodb:mongodb /var/lib/mongodb $STD systemctl enable mongod safe_service_restart mongod cache_installed_version "mongodb" "$MONGO_VERSION" msg_ok "Installed MongoDB $MONGO_VERSION" } # ------------------------------------------------------------------------------ # Installs or upgrades MySQL and configures APT repo. # # Description: # - Detects existing MySQL installation # - Purges conflicting packages before installation # - Supports clean upgrade # # Variables: # MYSQL_VERSION - MySQL version to install (e.g. 5.7, 8.0) (default: 8.0) # ------------------------------------------------------------------------------ function setup_mysql() { local MYSQL_VERSION="${MYSQL_VERSION:-8.0}" local CURRENT_VERSION="" local NEED_INSTALL=false local DISTRO_ID DISTRO_CODENAME DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) if command -v mysql >/dev/null; then CURRENT_VERSION="$(mysql --version | grep -oP '[0-9]+\.[0-9]+' | head -n1)" if [[ "$CURRENT_VERSION" != "$MYSQL_VERSION" ]]; then msg_info "MySQL $CURRENT_VERSION will be upgraded to $MYSQL_VERSION" NEED_INSTALL=true else if apt list --upgradable 2>/dev/null | grep -q '^mysql-server/'; then msg_info "MySQL $CURRENT_VERSION available for upgrade" $STD apt update $STD apt install --only-upgrade -y mysql-server msg_ok "MySQL upgraded" fi return fi else msg_info "Setup MySQL $MYSQL_VERSION" NEED_INSTALL=true fi if [[ "$NEED_INSTALL" == true ]]; then $STD systemctl stop mysql || true $STD apt purge -y "^mysql-server.*" "^mysql-client.*" "^mysql-common.*" || true # Cleanup old repository files cleanup_old_repo_files "mysql" # Use helper function to get fallback suite local SUITE SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://repo.mysql.com/apt/${DISTRO_ID}") # Use standardized repo setup setup_deb822_repo \ "mysql" \ "https://repo.mysql.com/RPM-GPG-KEY-mysql-2023" \ "https://repo.mysql.com/apt/${DISTRO_ID}" \ "$SUITE" \ "mysql-${MYSQL_VERSION}" \ "amd64 arm64" export DEBIAN_FRONTEND=noninteractive $STD apt install -y mysql-server cache_installed_version "mysql" "$MYSQL_VERSION" msg_ok "Installed MySQL $MYSQL_VERSION" fi } # ------------------------------------------------------------------------------ # Installs Node.js and optional global modules. # # Description: # - Installs specified Node.js version using NodeSource APT repo # - Optionally installs or updates global npm modules # # Variables: # NODE_VERSION - Node.js version to install (default: 22) # NODE_MODULE - Comma-separated list of global modules (e.g. "yarn,@vue/cli@5.0.0") # ------------------------------------------------------------------------------ function setup_nodejs() { local NODE_VERSION="${NODE_VERSION:-22}" local NODE_MODULE="${NODE_MODULE:-}" local CURRENT_NODE_VERSION="" local NEED_NODE_INSTALL=false local DISTRO_ID DISTRO_CODENAME DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) if command -v node >/dev/null; then CURRENT_NODE_VERSION="$(node -v | grep -oP '^v\K[0-9]+')" if [[ "$CURRENT_NODE_VERSION" != "$NODE_VERSION" ]]; then msg_info "Old Node.js $CURRENT_NODE_VERSION found, replacing with $NODE_VERSION" NEED_NODE_INSTALL=true fi else msg_info "Setup Node.js $NODE_VERSION" NEED_NODE_INSTALL=true fi ensure_dependencies jq if [[ "$NEED_NODE_INSTALL" == true ]]; then $STD apt purge -y nodejs # Cleanup old repository files cleanup_old_repo_files "nodesource" # NodeSource uses 'nodistro' for all distributions - no fallback needed # Use standardized repo setup setup_deb822_repo \ "nodesource" \ "https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key" \ "https://deb.nodesource.com/node_${NODE_VERSION}.x" \ "nodistro" \ "main" \ "amd64 arm64" if ! $STD apt install -y nodejs; then msg_error "Failed to install Node.js ${NODE_VERSION} from NodeSource" return 1 fi $STD npm install -g npm@latest || { msg_warn "Failed to update npm to latest version" } cache_installed_version "nodejs" "$NODE_VERSION" msg_ok "Installed Node.js ${NODE_VERSION}" fi export NODE_OPTIONS="--max-old-space-size=4096" if [[ ! -d /opt ]]; then mkdir -p /opt fi cd /opt || { msg_error "Failed to set safe working directory before npm install" return 1 } if [[ -n "$NODE_MODULE" ]]; then IFS=',' read -ra MODULES <<<"$NODE_MODULE" for mod in "${MODULES[@]}"; do local MODULE_NAME MODULE_REQ_VERSION MODULE_INSTALLED_VERSION if [[ "$mod" == @*/*@* ]]; then MODULE_NAME="${mod%@*}" MODULE_REQ_VERSION="${mod##*@}" elif [[ "$mod" == *"@"* ]]; then MODULE_NAME="${mod%@*}" MODULE_REQ_VERSION="${mod##*@}" else MODULE_NAME="$mod" MODULE_REQ_VERSION="latest" fi if npm list -g --depth=0 "$MODULE_NAME" >/dev/null 2>&1; then MODULE_INSTALLED_VERSION="$(npm list -g --depth=0 "$MODULE_NAME" | grep "$MODULE_NAME@" | awk -F@ '{print $2}' | tr -d '[:space:]')" if [[ "$MODULE_REQ_VERSION" != "latest" && "$MODULE_REQ_VERSION" != "$MODULE_INSTALLED_VERSION" ]]; then msg_info "Updating $MODULE_NAME from v$MODULE_INSTALLED_VERSION to v$MODULE_REQ_VERSION" if ! $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}"; then msg_error "Failed to update $MODULE_NAME to version $MODULE_REQ_VERSION" return 1 fi elif [[ "$MODULE_REQ_VERSION" == "latest" ]]; then msg_info "Updating $MODULE_NAME to latest version" if ! $STD npm install -g "${MODULE_NAME}@latest"; then msg_error "Failed to update $MODULE_NAME to latest version" return 1 fi fi else msg_info "Installing $MODULE_NAME@$MODULE_REQ_VERSION" if ! $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}"; then msg_error "Failed to install $MODULE_NAME@$MODULE_REQ_VERSION" return 1 fi fi done msg_ok "Installed Node.js modules: $NODE_MODULE" fi } # ------------------------------------------------------------------------------ # Installs PHP with selected modules and configures Apache/FPM support. # # Description: # - Adds Sury PHP repo if needed # - Installs default and user-defined modules # - Patches php.ini for CLI, Apache, and FPM as needed # # Variables: # PHP_VERSION - PHP version to install (default: 8.4) # PHP_MODULE - Additional comma-separated modules # PHP_APACHE - Set YES to enable PHP with Apache # PHP_FPM - Set YES to enable PHP-FPM # PHP_MEMORY_LIMIT - (default: 512M) # PHP_UPLOAD_MAX_FILESIZE - (default: 128M) # PHP_POST_MAX_SIZE - (default: 128M) # PHP_MAX_EXECUTION_TIME - (default: 300) # ------------------------------------------------------------------------------ function setup_php() { local PHP_VERSION="${PHP_VERSION:-8.4}" local PHP_MODULE="${PHP_MODULE:-}" local PHP_APACHE="${PHP_APACHE:-NO}" local PHP_FPM="${PHP_FPM:-NO}" local DISTRO_ID DISTRO_CODENAME DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) local DEFAULT_MODULES="bcmath,cli,curl,gd,intl,mbstring,opcache,readline,xml,zip" local COMBINED_MODULES local PHP_MEMORY_LIMIT="${PHP_MEMORY_LIMIT:-512M}" local PHP_UPLOAD_MAX_FILESIZE="${PHP_UPLOAD_MAX_FILESIZE:-128M}" local PHP_POST_MAX_SIZE="${PHP_POST_MAX_SIZE:-128M}" local PHP_MAX_EXECUTION_TIME="${PHP_MAX_EXECUTION_TIME:-300}" # Merge default + user-defined modules if [[ -n "$PHP_MODULE" ]]; then COMBINED_MODULES="${DEFAULT_MODULES},${PHP_MODULE}" else COMBINED_MODULES="${DEFAULT_MODULES}" fi # Deduplicate COMBINED_MODULES=$(echo "$COMBINED_MODULES" | tr ',' '\n' | awk '!seen[$0]++' | paste -sd, -) # Get current PHP-CLI version local CURRENT_PHP="" if command -v php >/dev/null 2>&1; then CURRENT_PHP=$(php -v 2>/dev/null | awk '/^PHP/{print $2}' | cut -d. -f1,2) fi if [[ -z "$CURRENT_PHP" ]]; then msg_info "Setup PHP $PHP_VERSION" elif [[ "$CURRENT_PHP" != "$PHP_VERSION" ]]; then msg_info "Old PHP $CURRENT_PHP detected, Setup new PHP $PHP_VERSION" $STD apt purge -y "php${CURRENT_PHP//./}"* || true fi # Ensure Sury repo is available if [[ ! -f /etc/apt/sources.list.d/php.sources ]]; then # Cleanup old repository files cleanup_old_repo_files "php" $STD curl -fsSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb $STD dpkg -i /tmp/debsuryorg-archive-keyring.deb # Use helper function to get fallback suite local SUITE SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://packages.sury.org/php") cat </etc/apt/sources.list.d/php.sources Types: deb URIs: https://packages.sury.org/php/ Suites: $SUITE Components: main Architectures: amd64 arm64 Signed-By: /usr/share/keyrings/deb.sury.org-php.gpg EOF $STD apt update fi # Build module list local MODULE_LIST="php${PHP_VERSION}" IFS=',' read -ra MODULES <<<"$COMBINED_MODULES" for mod in "${MODULES[@]}"; do if apt-cache show "php${PHP_VERSION}-${mod}" >/dev/null 2>&1; then MODULE_LIST+=" php${PHP_VERSION}-${mod}" else msg_warn "PHP-Module ${mod} for PHP ${PHP_VERSION} not found – skipping" fi done if [[ "$PHP_FPM" == "YES" ]]; then MODULE_LIST+=" php${PHP_VERSION}-fpm" fi # install apache2 with PHP support if requested if [[ "$PHP_APACHE" == "YES" ]]; then if ! dpkg -l | grep -q "libapache2-mod-php${PHP_VERSION}"; then msg_info "Installing Apache with PHP${PHP_VERSION} support" $STD apt install -y apache2 libapache2-mod-php${PHP_VERSION} else msg_info "Apache with PHP${PHP_VERSION} already installed – skipping install" fi fi # setup / update PHP modules $STD apt install -y $MODULE_LIST cache_installed_version "php" "$PHP_VERSION" msg_ok "Installed PHP $PHP_VERSION" # optional stop old PHP-FPM service if [[ "$PHP_FPM" == "YES" && -n "$CURRENT_PHP" && "$CURRENT_PHP" != "$PHP_VERSION" ]]; then $STD systemctl stop php"${CURRENT_PHP}"-fpm || true $STD systemctl disable php"${CURRENT_PHP}"-fpm || true fi # Patch all relevant php.ini files (silent) local PHP_INI_PATHS=("/etc/php/${PHP_VERSION}/cli/php.ini") [[ "$PHP_FPM" == "YES" ]] && PHP_INI_PATHS+=("/etc/php/${PHP_VERSION}/fpm/php.ini") [[ "$PHP_APACHE" == "YES" ]] && PHP_INI_PATHS+=("/etc/php/${PHP_VERSION}/apache2/php.ini") for ini in "${PHP_INI_PATHS[@]}"; do if [[ -f "$ini" ]]; then $STD sed -i "s|^memory_limit = .*|memory_limit = ${PHP_MEMORY_LIMIT}|" "$ini" $STD sed -i "s|^upload_max_filesize = .*|upload_max_filesize = ${PHP_UPLOAD_MAX_FILESIZE}|" "$ini" $STD sed -i "s|^post_max_size = .*|post_max_size = ${PHP_POST_MAX_SIZE}|" "$ini" $STD sed -i "s|^max_execution_time = .*|max_execution_time = ${PHP_MAX_EXECUTION_TIME}|" "$ini" fi done # patch Apache configuration if needed if [[ "$PHP_APACHE" == "YES" ]]; then for mod in $(ls /etc/apache2/mods-enabled/ 2>/dev/null | grep -E '^php[0-9]\.[0-9]\.conf$' | sed 's/\.conf//'); do if [[ "$mod" != "php${PHP_VERSION}" ]]; then $STD a2dismod "$mod" || true fi done $STD a2enmod mpm_prefork $STD a2enmod "php${PHP_VERSION}" safe_service_restart apache2 || true fi # enable and restart PHP-FPM if requested if [[ "$PHP_FPM" == "YES" ]]; then if systemctl list-unit-files | grep -q "php${PHP_VERSION}-fpm.service"; then $STD systemctl enable php${PHP_VERSION}-fpm safe_service_restart php${PHP_VERSION}-fpm else msg_warn "FPM requested but service php${PHP_VERSION}-fpm not found" fi fi } # ------------------------------------------------------------------------------ # Installs or upgrades PostgreSQL and optional extensions/modules. # # Description: # - Detects existing PostgreSQL version # - Dumps all databases before upgrade # - Adds PGDG repo and installs specified version # - Installs optional PG_MODULES (e.g. postgis, contrib) # - Restores dumped data post-upgrade # # Variables: # PG_VERSION - Major PostgreSQL version (e.g. 15, 16) (default: 16) # PG_MODULES - Comma-separated list of extensions (e.g. "postgis,contrib") # ------------------------------------------------------------------------------ function setup_postgresql() { local PG_VERSION="${PG_VERSION:-16}" local PG_MODULES="${PG_MODULES:-}" local CURRENT_PG_VERSION="" local DISTRO_ID DISTRO_CODENAME local NEED_PG_INSTALL=false DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) if command -v psql >/dev/null; then CURRENT_PG_VERSION="$(psql -V | awk '{print $3}' | cut -d. -f1)" [[ "$CURRENT_PG_VERSION" != "$PG_VERSION" ]] && NEED_PG_INSTALL=true else NEED_PG_INSTALL=true fi if [[ "$NEED_PG_INSTALL" == true ]]; then msg_info "Installing PostgreSQL $PG_VERSION" if [[ -n "$CURRENT_PG_VERSION" ]]; then $STD su - postgres -c "pg_dumpall > /var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql" $STD systemctl stop postgresql fi # Cleanup old repository files cleanup_old_repo_files "pgdg" # Use helper function to get fallback suite local SUITE SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://apt.postgresql.org/pub/repos/apt") # PGDG uses special suite naming: ${SUITE}-pgdg SUITE="${SUITE}-pgdg" # Use standardized repo setup setup_deb822_repo \ "pgdg" \ "https://www.postgresql.org/media/keys/ACCC4CF8.asc" \ "https://apt.postgresql.org/pub/repos/apt" \ "$SUITE" \ "main" \ "amd64 arm64" $STD apt install -y "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}" if [[ -n "$CURRENT_PG_VERSION" ]]; then $STD apt purge -y "postgresql-${CURRENT_PG_VERSION}" "postgresql-client-${CURRENT_PG_VERSION}" || true $STD su - postgres -c "psql < /var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql" fi $STD systemctl enable --now postgresql cache_installed_version "postgresql" "$PG_VERSION" msg_ok "Installed PostgreSQL $PG_VERSION" fi if [[ -n "$PG_MODULES" ]]; then msg_info "Installing PostgreSQL modules" IFS=',' read -ra MODULES <<<"$PG_MODULES" for module in "${MODULES[@]}"; do $STD apt install -y "postgresql-${PG_VERSION}-${module}" || { msg_warn "Failed to install postgresql-${PG_VERSION}-${module}" continue } done msg_ok "Installed PostgreSQL modules" fi } # ------------------------------------------------------------------------------ # Installs rbenv and ruby-build, installs Ruby and optionally Rails. # # Description: # - Downloads rbenv and ruby-build from GitHub # - Compiles and installs target Ruby version # - Optionally installs Rails via gem # # Variables: # RUBY_VERSION - Ruby version to install (default: 3.4.4) # RUBY_INSTALL_RAILS - true/false to install Rails (default: true) # ------------------------------------------------------------------------------ function setup_ruby() { local RUBY_VERSION="${RUBY_VERSION:-3.4.4}" local RUBY_INSTALL_RAILS="${RUBY_INSTALL_RAILS:-true}" local RBENV_DIR="$HOME/.rbenv" local RBENV_BIN="$RBENV_DIR/bin/rbenv" local PROFILE_FILE="$HOME/.profile" local TMP_DIR=$(mktemp -d) local CACHED_VERSION CACHED_VERSION=$(get_cached_version "ruby") msg_info "Installing Ruby $RUBY_VERSION" ensure_dependencies jq local RBENV_RELEASE RBENV_RELEASE=$(curl -fsSL https://api.github.com/repos/rbenv/rbenv/releases/latest | jq -r '.tag_name' | sed 's/^v//') if [[ -z "$RBENV_RELEASE" ]]; then msg_error "Failed to fetch latest rbenv version" rm -rf "$TMP_DIR" return 1 fi curl -fsSL "https://github.com/rbenv/rbenv/archive/refs/tags/v${RBENV_RELEASE}.tar.gz" -o "$TMP_DIR/rbenv.tar.gz" || { msg_error "Failed to download rbenv" rm -rf "$TMP_DIR" return 1 } tar -xzf "$TMP_DIR/rbenv.tar.gz" -C "$TMP_DIR" || { msg_error "Failed to extract rbenv" rm -rf "$TMP_DIR" return 1 } mkdir -p "$RBENV_DIR" cp -r "$TMP_DIR/rbenv-${RBENV_RELEASE}/." "$RBENV_DIR/" cd "$RBENV_DIR" && src/configure && $STD make -C src local RUBY_BUILD_RELEASE RUBY_BUILD_RELEASE=$(curl -fsSL https://api.github.com/repos/rbenv/ruby-build/releases/latest | jq -r '.tag_name' | sed 's/^v//') if [[ -z "$RUBY_BUILD_RELEASE" ]]; then msg_error "Failed to fetch latest ruby-build version" rm -rf "$TMP_DIR" return 1 fi curl -fsSL "https://github.com/rbenv/ruby-build/archive/refs/tags/v${RUBY_BUILD_RELEASE}.tar.gz" -o "$TMP_DIR/ruby-build.tar.gz" || { msg_error "Failed to download ruby-build" rm -rf "$TMP_DIR" return 1 } tar -xzf "$TMP_DIR/ruby-build.tar.gz" -C "$TMP_DIR" || { msg_error "Failed to extract ruby-build" rm -rf "$TMP_DIR" return 1 } mkdir -p "$RBENV_DIR/plugins/ruby-build" cp -r "$TMP_DIR/ruby-build-${RUBY_BUILD_RELEASE}/." "$RBENV_DIR/plugins/ruby-build/" echo "$RUBY_BUILD_RELEASE" >"$RBENV_DIR/plugins/ruby-build/RUBY_BUILD_version.txt" if ! grep -q 'rbenv init' "$PROFILE_FILE"; then echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >>"$PROFILE_FILE" echo 'eval "$(rbenv init -)"' >>"$PROFILE_FILE" fi export PATH="$RBENV_DIR/bin:$PATH" eval "$("$RBENV_BIN" init - bash)" if ! "$RBENV_BIN" versions --bare | grep -qx "$RUBY_VERSION"; then $STD "$RBENV_BIN" install "$RUBY_VERSION" fi "$RBENV_BIN" global "$RUBY_VERSION" hash -r if [[ "$RUBY_INSTALL_RAILS" == "true" ]]; then $STD gem install rails local RAILS_VERSION=$(rails -v 2>/dev/null | awk '{print $2}') msg_ok "Installed Rails $RAILS_VERSION" fi rm -rf "$TMP_DIR" cache_installed_version "ruby" "$RUBY_VERSION" msg_ok "Installed Ruby $RUBY_VERSION" } # ------------------------------------------------------------------------------ # Installs or upgrades ClickHouse database server. # # Description: # - Adds ClickHouse official repository # - Installs specified version # - Configures systemd service # - Supports Debian/Ubuntu with fallback mechanism # # Variables: # CLICKHOUSE_VERSION - ClickHouse version to install (default: latest) # ------------------------------------------------------------------------------ function setup_clickhouse() { local CLICKHOUSE_VERSION="${CLICKHOUSE_VERSION:-latest}" local DISTRO_ID DISTRO_CODENAME DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) # Determine latest version if needed if [[ "$CLICKHOUSE_VERSION" == "latest" ]]; then CLICKHOUSE_VERSION=$(curl -fsSL https://packages.clickhouse.com/tgz/stable/ | grep -oP 'clickhouse-common-static-\K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | sort -V | tail -n1) if [[ -z "$CLICKHOUSE_VERSION" ]]; then msg_error "Could not determine latest ClickHouse version" return 1 fi fi local CURRENT_VERSION="" if command -v clickhouse-server >/dev/null 2>&1; then CURRENT_VERSION=$(clickhouse-server --version 2>/dev/null | grep -oP 'version \K[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -n1) fi local CACHED_VERSION CACHED_VERSION=$(get_cached_version "clickhouse") # Check if already at target version if [[ "$CURRENT_VERSION" == "$CLICKHOUSE_VERSION" ]]; then if [[ "$CACHED_VERSION" == "$CLICKHOUSE_VERSION" ]]; then upgrade_package clickhouse-server upgrade_package clickhouse-client else msg_info "Upgrading ClickHouse $CLICKHOUSE_VERSION" $STD apt update $STD apt install --only-upgrade -y clickhouse-server clickhouse-client cache_installed_version "clickhouse" "$CLICKHOUSE_VERSION" msg_ok "Upgraded ClickHouse $CLICKHOUSE_VERSION" fi return 0 fi msg_info "Installing ClickHouse $CLICKHOUSE_VERSION" # Stop existing service if upgrading if [[ -n "$CURRENT_VERSION" ]]; then $STD systemctl stop clickhouse-server || true fi # Cleanup old repository files cleanup_old_repo_files "clickhouse" # Ensure dependencies ensure_dependencies apt-transport-https ca-certificates dirmngr gnupg # Use helper function to get fallback suite local SUITE SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://packages.clickhouse.com/deb") # Use standardized repo setup setup_deb822_repo \ "clickhouse" \ "https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key" \ "https://packages.clickhouse.com/deb" \ "$SUITE" \ "main" \ "amd64 arm64" # Install ClickHouse packages export DEBIAN_FRONTEND=noninteractive $STD apt install -y clickhouse-server clickhouse-client # Create data directory if it doesn't exist mkdir -p /var/lib/clickhouse chown -R clickhouse:clickhouse /var/lib/clickhouse # Enable and start service $STD systemctl enable clickhouse-server safe_service_restart clickhouse-server cache_installed_version "clickhouse" "$CLICKHOUSE_VERSION" msg_ok "Installed ClickHouse $CLICKHOUSE_VERSION" } # ------------------------------------------------------------------------------ # Installs Rust toolchain and optional global crates via cargo. # # Description: # - Installs rustup (if missing) # - Installs or updates desired Rust toolchain (stable, nightly, or versioned) # - Installs or updates specified global crates using `cargo install` # # Notes: # - Skips crate install if exact version is already present # - Updates crate if newer version or different version is requested # # Variables: # RUST_TOOLCHAIN - Rust toolchain to install (default: stable) # RUST_CRATES - Comma-separated list of crates (e.g. "cargo-edit,wasm-pack@0.12.1") # ------------------------------------------------------------------------------ function setup_rust() { local RUST_TOOLCHAIN="${RUST_TOOLCHAIN:-stable}" local RUST_CRATES="${RUST_CRATES:-}" local CARGO_BIN="${HOME}/.cargo/bin" local CACHED_VERSION CACHED_VERSION=$(get_cached_version "rust") if ! command -v rustup &>/dev/null; then msg_info "Installing Rust" curl -fsSL https://sh.rustup.rs | $STD sh -s -- -y --default-toolchain "$RUST_TOOLCHAIN" || { msg_error "Failed to install Rust" return 1 } export PATH="$CARGO_BIN:$PATH" echo 'export PATH="$HOME/.cargo/bin:$PATH"' >>"$HOME/.profile" local RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}') cache_installed_version "rust" "$RUST_VERSION" msg_ok "Installed Rust $RUST_VERSION" else $STD rustup install "$RUST_TOOLCHAIN" $STD rustup default "$RUST_TOOLCHAIN" $STD rustup update "$RUST_TOOLCHAIN" local RUST_VERSION=$(rustc --version 2>/dev/null | awk '{print $2}') cache_installed_version "rust" "$RUST_VERSION" msg_ok "Updated Rust toolchain to $RUST_TOOLCHAIN ($RUST_VERSION)" fi if [[ -n "$RUST_CRATES" ]]; then IFS=',' read -ra CRATES <<<"$RUST_CRATES" for crate in "${CRATES[@]}"; do local NAME VER INSTALLED_VER if [[ "$crate" == *"@"* ]]; then NAME="${crate%@*}" VER="${crate##*@}" else NAME="$crate" VER="" fi INSTALLED_VER=$(cargo install --list 2>/dev/null | awk "/^$NAME v[0-9]/ {print \$2}" | tr -d 'v') if [[ -n "$INSTALLED_VER" ]]; then if [[ -n "$VER" && "$VER" != "$INSTALLED_VER" ]]; then msg_info "Updating $NAME: $INSTALLED_VER → $VER" $STD cargo install "$NAME" --version "$VER" --force msg_ok "Updated $NAME to $VER" elif [[ -z "$VER" ]]; then msg_info "Updating $NAME: $INSTALLED_VER → latest" $STD cargo install "$NAME" --force local NEW_VER=$(cargo install --list 2>/dev/null | awk "/^$NAME v[0-9]/ {print \$2}" | tr -d 'v') msg_ok "Updated $NAME to $NEW_VER" fi else msg_info "Installing $NAME ${VER:+$VER}" $STD cargo install "$NAME" ${VER:+--version "$VER"} msg_ok "Installed $NAME ${VER:-latest}" fi done fi } # ------------------------------------------------------------------------------ # Installs or upgrades uv (Python package manager) from GitHub releases. # - Downloads platform-specific tarball (no install.sh!) # - Extracts uv binary # - Places it in /usr/local/bin # - Optionally installs a specific Python version via uv # ------------------------------------------------------------------------------ function setup_uv() { local UV_BIN="/usr/local/bin/uv" local TMP_DIR=$(mktemp -d) local CACHED_VERSION CACHED_VERSION=$(get_cached_version "uv") local ARCH=$(uname -m) local UV_TAR case "$ARCH" in x86_64) if grep -qi "alpine" /etc/os-release; then UV_TAR="uv-x86_64-unknown-linux-musl.tar.gz" else UV_TAR="uv-x86_64-unknown-linux-gnu.tar.gz" fi ;; aarch64) if grep -qi "alpine" /etc/os-release; then UV_TAR="uv-aarch64-unknown-linux-musl.tar.gz" else UV_TAR="uv-aarch64-unknown-linux-gnu.tar.gz" fi ;; *) msg_error "Unsupported architecture: $ARCH" rm -rf "$TMP_DIR" return 1 ;; esac ensure_dependencies jq local LATEST_VERSION LATEST_VERSION=$(curl -fsSL https://api.github.com/repos/astral-sh/uv/releases/latest | jq -r '.tag_name' | sed 's/^v//') if [[ -z "$LATEST_VERSION" ]]; then msg_error "Could not fetch latest uv version" rm -rf "$TMP_DIR" return 1 fi if [[ -x "$UV_BIN" ]]; then local INSTALLED_VERSION INSTALLED_VERSION=$($UV_BIN -V 2>/dev/null | awk '{print $2}') if [[ "$INSTALLED_VERSION" == "$LATEST_VERSION" ]]; then if [[ "$CACHED_VERSION" == "$LATEST_VERSION" ]]; then rm -rf "$TMP_DIR" return 0 fi cache_installed_version "uv" "$LATEST_VERSION" rm -rf "$TMP_DIR" return 0 else msg_info "Updating uv from $INSTALLED_VERSION to $LATEST_VERSION" fi else msg_info "Installing uv $LATEST_VERSION" fi local UV_URL="https://github.com/astral-sh/uv/releases/latest/download/${UV_TAR}" curl -fsSL "$UV_URL" -o "$TMP_DIR/uv.tar.gz" || { msg_error "Failed to download uv" rm -rf "$TMP_DIR" return 1 } tar -xzf "$TMP_DIR/uv.tar.gz" -C "$TMP_DIR" || { msg_error "Failed to extract uv" rm -rf "$TMP_DIR" return 1 } install -m 755 "$TMP_DIR"/*/uv "$UV_BIN" || { msg_error "Failed to install uv binary" rm -rf "$TMP_DIR" return 1 } rm -rf "$TMP_DIR" ensure_usr_local_bin_persist export PATH="/usr/local/bin:$PATH" $STD uv python update-shell || true cache_installed_version "uv" "$LATEST_VERSION" msg_ok "Installed uv $LATEST_VERSION" if [[ -n "${PYTHON_VERSION:-}" ]]; then local VERSION_MATCH VERSION_MATCH=$(uv python list --only-downloads | grep -E "^cpython-${PYTHON_VERSION//./\\.}\.[0-9]+-linux" | cut -d'-' -f2 | sort -V | tail -n1) if [[ -z "$VERSION_MATCH" ]]; then msg_error "No matching Python $PYTHON_VERSION.x version found" return 1 fi if ! uv python list | grep -q "cpython-${VERSION_MATCH}-linux.*uv/python"; then $STD uv python install "$VERSION_MATCH" || { msg_error "Failed to install Python $VERSION_MATCH" return 1 } msg_ok "Installed Python $VERSION_MATCH" fi fi } # ------------------------------------------------------------------------------ # Installs or updates yq (mikefarah/yq - Go version). # # Description: # - Checks if yq is installed and from correct source # - Compares with latest release on GitHub # - Updates if outdated or wrong implementation # ------------------------------------------------------------------------------ function setup_yq() { local TMP_DIR=$(mktemp -d) local BINARY_PATH="/usr/local/bin/yq" local GITHUB_REPO="mikefarah/yq" local CURRENT_VERSION="" local CACHED_VERSION CACHED_VERSION=$(get_cached_version "yq") ensure_dependencies jq ensure_usr_local_bin_persist if command -v yq &>/dev/null; then if ! yq --version 2>&1 | grep -q 'mikefarah'; then rm -f "$(command -v yq)" else CURRENT_VERSION=$(yq --version 2>/dev/null | awk '{print $NF}' | sed 's/^v//') fi fi local LATEST_VERSION LATEST_VERSION=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | jq -r '.tag_name' | sed 's/^v//') if [[ -z "$LATEST_VERSION" ]]; then msg_error "Could not determine latest yq version" rm -rf "$TMP_DIR" return 1 fi if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$LATEST_VERSION" ]]; then if [[ "$CACHED_VERSION" == "$LATEST_VERSION" ]]; then rm -rf "$TMP_DIR" return 0 fi cache_installed_version "yq" "$LATEST_VERSION" rm -rf "$TMP_DIR" return 0 fi msg_info "Installing yq $LATEST_VERSION" curl -fsSL "https://github.com/${GITHUB_REPO}/releases/download/v${LATEST_VERSION}/yq_linux_amd64" -o "$TMP_DIR/yq" || { msg_error "Failed to download yq" rm -rf "$TMP_DIR" return 1 } chmod +x "$TMP_DIR/yq" mv "$TMP_DIR/yq" "$BINARY_PATH" if [[ ! -x "$BINARY_PATH" ]]; then msg_error "Failed to install yq" rm -rf "$TMP_DIR" return 1 fi rm -rf "$TMP_DIR" hash -r local FINAL_VERSION FINAL_VERSION=$("$BINARY_PATH" --version 2>/dev/null | awk '{print $NF}' | sed 's/^v//') cache_installed_version "yq" "$FINAL_VERSION" msg_ok "Installed yq $FINAL_VERSION" }