diff --git a/misc/tools.func b/misc/tools.func index e98332517..a9a385cdd 100644 --- a/misc/tools.func +++ b/misc/tools.func @@ -1,786 +1,939 @@ #!/bin/bash -# ------------------------------------------------------------------------------ -# Installs Microsoft .NET -# -# Description: -# - Installs specified .NET version using Microsoft APT repo -# -# Variables: -# NET_VERSION - .NET version to install (default: "8.0") -# ------------------------------------------------------------------------------ +# ============================================================================== +# HELPER FUNCTIONS FOR PACKAGE MANAGEMENT +# ============================================================================== -function setup_dotnet() { - local NET_VERSION="${NET_VERSION:-8.0}" - local DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{ print $2 }' /etc/os-release) - local NEED_NET_INSTALL=false +# ------------------------------------------------------------------------------ +# 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" +} - if command -v dotnet >/dev/null; then - CURRENT_NET_VERSION=$(dotnet --version | awk -F. '{print $1 "." $2}') - if [[ "$CURRENT_NET_VERSION" == "$NET_VERSION" ]]; then - : - fi - else - msg_info "Setup .NET $NET_VERSION" - NEED_NET_INSTALL=true +get_cached_version() { + local app="$1" + [[ -f "/var/cache/app-versions/${app}_version.txt" ]] && cat "/var/cache/app-versions/${app}_version.txt" +} + +# ------------------------------------------------------------------------------ +# 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 [[ "$NEED_NET_INSTALL" == true ]]; then - curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg - if [[ "$DISTRO_CODENAME" != "trixie" ]]; then - curl -fsSL https://packages.microsoft.com/config/debian/12/prod.list -o /etc/apt/sources.list.d/msprod.list - else - curl -fsSL https://packages.microsoft.com/config/debian/13/prod.list -o /etc/apt/sources.list.d/msprod.list + 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 - $STD apt-get update - $STD apt-get install -y dotnet-sdk-$NET_VERSION - msg_ok "Setup .NET ${NET_VERSION}" + 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 } # ------------------------------------------------------------------------------ -# 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") +# Smart version comparison # ------------------------------------------------------------------------------ +version_gt() { + test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1" +} -function setup_nodejs() { - local NODE_VERSION="${NODE_VERSION:-22}" - local NODE_MODULE="${NODE_MODULE:-}" - local CURRENT_NODE_VERSION="" - local NEED_NODE_INSTALL=false +# ------------------------------------------------------------------------------ +# Get system architecture (normalized) +# ------------------------------------------------------------------------------ +get_system_arch() { + local arch_type="${1:-dpkg}" # dpkg, uname, or both + local arch - 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 + 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 - if ! command -v jq &>/dev/null; then - $STD apt-get update - $STD apt-get install -y jq || { - msg_error "Failed to install jq" + 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 - if [[ "$NEED_NODE_INSTALL" == true ]]; then - $STD apt-get purge -y nodejs - rm -f /etc/apt/sources.list.d/nodesource.list /usr/share/keyrings/nodesource.gpg + 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 +} - mkdir -p /usr/share/keyrings - curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | - gpg --dearmor -o /usr/share/keyrings/nodesource.gpg || { - msg_error "Failed to import NodeSource GPG key" - exit 1 - } - chmod 644 /usr/share/keyrings/nodesource.gpg +# ------------------------------------------------------------------------------ +# Check if running on specific OS +# ------------------------------------------------------------------------------ +is_debian() { + [[ "$(get_os_info id)" == "debian" ]] +} - local ARCH - ARCH=$(dpkg --print-architecture) - if ! [[ "$ARCH" =~ ^(amd64|arm64|armhf)$ ]]; then - msg_error "Unsupported architecture: $ARCH" - exit 1 +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 - echo "deb [arch=$ARCH signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_VERSION}.x nodistro main" >/etc/apt/sources.list.d/nodesource.list + if [[ $attempt -lt $max_retries ]]; then + msg_warn "Download failed, retrying... (attempt $attempt/$max_retries)" + sleep 2 + fi + done - cat </etc/apt/preferences.d/nodejs -Package: nodejs -Pin: origin deb.nodesource.com -Pin-Priority: 700 + 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 + curl -fsSL "$gpg_url" | gpg --dearmor -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 - sleep 2 - if ! apt-get update >/dev/null 2>&1; then - msg_warn "APT update failed – retrying in 5s" - sleep 5 - if ! apt-get update >/dev/null 2>&1; then - msg_error "Failed to update APT repositories after adding NodeSource" - exit 1 - fi - fi + # Use cached apt update + local apt_cache_file="/var/cache/apt-update-timestamp" + local current_time=$(date +%s) + local last_update=0 - if ! apt-get install -y -t nodistro nodejs >/dev/null 2>&1; then - msg_error "Failed to install Node.js ${NODE_VERSION} from NodeSource" - apt-cache policy nodejs | tee "$STD" - exit 1 - fi - - $STD npm install -g npm@latest || { - msg_error "Failed to update npm to latest version" - } - msg_ok "Setup Node.js ${NODE_VERSION}" + if [[ -f "$apt_cache_file" ]]; then + last_update=$(cat "$apt_cache_file" 2>/dev/null || echo 0) fi - export NODE_OPTIONS="--max-old-space-size=4096" - - [[ -d /opt ]] || mkdir -p /opt - cd /opt || { - msg_error "Failed to set safe working directory before npm install" - exit 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" - $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}" || { - msg_error "Failed to update $MODULE_NAME to version $MODULE_REQ_VERSION" - exit 1 - } - elif [[ "$MODULE_REQ_VERSION" == "latest" ]]; then - msg_info "Updating $MODULE_NAME to latest version" - $STD npm install -g "${MODULE_NAME}@latest" || { - msg_error "Failed to update $MODULE_NAME to latest version" - exit 1 - } - fi - else - msg_info "Installing $MODULE_NAME@$MODULE_REQ_VERSION" - $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}" || { - msg_error "Failed to install $MODULE_NAME@$MODULE_REQ_VERSION" - exit 1 - } - fi - done - msg_ok "Installed Node.js modules: $NODE_MODULE" + # 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" } # ------------------------------------------------------------------------------ -# 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") +# Package version hold/unhold helpers # ------------------------------------------------------------------------------ -function setup_postgresql() { - local PG_VERSION="${PG_VERSION:-16}" - local PG_MODULES="${PG_MODULES:-}" - local CURRENT_PG_VERSION="" - local DISTRO - local NEED_PG_INSTALL=false - DISTRO="$(awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release)" +hold_package_version() { + local package="$1" + $STD apt-mark hold "$package" +} - if command -v psql >/dev/null; then - CURRENT_PG_VERSION="$(psql -V | awk '{print $3}' | cut -d. -f1)" - if [[ "$CURRENT_PG_VERSION" == "$PG_VERSION" ]]; then - : # PostgreSQL is already at the desired version – no action needed - else - $STD msg_info "Detected PostgreSQL $CURRENT_PG_VERSION, preparing upgrade to $PG_VERSION" - NEED_PG_INSTALL=true - fi +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 - NEED_PG_INSTALL=true + $STD systemctl start "$service" fi - if [[ "$NEED_PG_INSTALL" == true ]]; then - if [[ -n "$CURRENT_PG_VERSION" ]]; then - $STD msg_info "Dumping PostgreSQL $CURRENT_PG_VERSION data" - su - postgres -c "pg_dumpall > /var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql" - $STD msg_ok "Data dump completed" - - systemctl stop postgresql - fi - - rm -f /etc/apt/sources.list.d/pgdg.list /etc/apt/trusted.gpg.d/postgresql.gpg - - $STD msg_info "Adding PostgreSQL PGDG repository" - curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | - gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg - - echo "deb https://apt.postgresql.org/pub/repos/apt ${DISTRO}-pgdg main" \ - >/etc/apt/sources.list.d/pgdg.list - $STD apt-get update - $STD msg_ok "Repository added" - - msg_info "Setup PostgreSQL $PG_VERSION" - $STD apt-get install -y "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}" - - if [[ -n "$CURRENT_PG_VERSION" ]]; then - $STD apt-get purge -y "postgresql-${CURRENT_PG_VERSION}" "postgresql-client-${CURRENT_PG_VERSION}" || true - fi - systemctl enable -q --now postgresql - - if [[ -n "$CURRENT_PG_VERSION" ]]; then - $STD msg_info "Restoring dumped data" - su - postgres -c "psql < /var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql" - $STD msg_ok "Data restored" - fi - - $STD msg_ok "PostgreSQL $PG_VERSION installed" - fi - - # Install optional PostgreSQL modules - if [[ -n "$PG_MODULES" ]]; then - IFS=',' read -ra MODULES <<<"$PG_MODULES" - for module in "${MODULES[@]}"; do - local pkg="postgresql-${PG_VERSION}-${module}" - $STD msg_info "Setup PostgreSQL module/s: $pkg" - $STD apt-get install -y "$pkg" || { - msg_error "Failed to install $pkg" - continue - } - done - $STD msg_ok "Setup PostgreSQL modules" + if ! systemctl is-active --quiet "$service"; then + msg_error "Failed to start $service" + systemctl status "$service" --no-pager + return 1 fi + return 0 } # ------------------------------------------------------------------------------ -# 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) -# ------------------------------------------------------------------------------ test +# Enable and start service (with error handling) +# ------------------------------------------------------------------------------ +enable_and_start_service() { + local service="$1" -setup_mariadb() { - local MARIADB_VERSION="${MARIADB_VERSION:-latest}" - local DISTRO_CODENAME - DISTRO_CODENAME="$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)" - CURRENT_OS="$(awk -F= '/^ID=/{print $2}' /etc/os-release)" + if ! systemctl enable "$service" &>/dev/null; then + msg_warn "Could not enable $service (may not be installed yet)" + fi - if ! curl -fsI http://mirror.mariadb.org/repo/ >/dev/null; then - msg_error "MariaDB mirror not reachable" + if ! systemctl start "$service" &>/dev/null; then + msg_error "Failed to start $service" + systemctl status "$service" --no-pager return 1 fi - msg_info "Setting up MariaDB $MARIADB_VERSION" - # Grab dynamic latest LTS version - 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 + 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 - local CURRENT_VERSION="" - if command -v mariadb >/dev/null; then - CURRENT_VERSION=$(mariadb --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') + 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 - if [[ "$CURRENT_VERSION" == "$MARIADB_VERSION" ]]; then - $STD msg_info "MariaDB $MARIADB_VERSION, upgrading" - $STD apt-get update - $STD apt-get install --only-upgrade -y mariadb-server mariadb-client - $STD msg_ok "MariaDB upgraded to $MARIADB_VERSION" + 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 - if [[ -n "$CURRENT_VERSION" ]]; then - $STD msg_info "Upgrading MariaDB $CURRENT_VERSION to $MARIADB_VERSION" - $STD systemctl stop mariadb >/dev/null 2>&1 || true - $STD apt-get purge -y 'mariadb*' || true - rm -f /etc/apt/sources.list.d/mariadb.list /etc/apt/trusted.gpg.d/mariadb.gpg - else - $STD msg_info "Setup MariaDB $MARIADB_VERSION" + 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 - curl -fsSL "https://mariadb.org/mariadb_release_signing_key.asc" | - gpg --dearmor -o /etc/apt/trusted.gpg.d/mariadb.gpg + ensure_dependencies jq - echo "deb [signed-by=/etc/apt/trusted.gpg.d/mariadb.gpg] http://mirror.mariadb.org/repo/${MARIADB_VERSION}/${CURRENT_OS} ${DISTRO_CODENAME} main" \ - >/etc/apt/sources.list.d/mariadb.list - - $STD apt-get update - - 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 - else - for ver in 12.1 12.0 11.4 11.3 11.2 11.1 11.0 10.11 10.6 10.5 10.4 10.3; do - echo "mariadb-server-$ver mariadb-server/feedback boolean false" | debconf-set-selections - done - fi - DEBIAN_FRONTEND=noninteractive $STD apt-get install -y mariadb-server mariadb-client || { - msg_warn "Failed to install MariaDB ${MARIADB_VERSION} from upstream repo – trying distro package as fallback..." - # Cleanup, remove upstream repo to avoid conflicts - rm -f /etc/apt/sources.list.d/mariadb.list /etc/apt/trusted.gpg.d/mariadb.gpg - $STD apt-get update - # Final fallback: distro package - DEBIAN_FRONTEND=noninteractive $STD apt-get install -y mariadb-server mariadb-client || { - msg_error "MariaDB installation failed even with distro fallback!" - return 1 - } - msg_ok "Setup MariaDB (distro fallback)" - return 0 + # 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 } - msg_ok "Setup MariaDB $MARIADB_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 - CURRENT_OS="$(awk -F= '/^ID=/{print $2}' /etc/os-release)" - - if command -v mysql >/dev/null; then - CURRENT_VERSION="$(mysql --version | grep -oP 'Distrib\s+\K[0-9]+\.[0-9]+')" - if [[ "$CURRENT_VERSION" != "$MYSQL_VERSION" ]]; then - $STD msg_info "MySQL $CURRENT_VERSION will be upgraded to $MYSQL_VERSION" - NEED_INSTALL=true - else - # Check for patch-level updates - if apt list --upgradable 2>/dev/null | grep -q '^mysql-server/'; then - $STD msg_info "MySQL $CURRENT_VERSION available for upgrade" - $STD apt-get update - $STD apt-get install --only-upgrade -y mysql-server - $STD msg_ok "MySQL upgraded" - fi - return - fi - else - msg_info "Setup MySQL $MYSQL_VERSION" - NEED_INSTALL=true + 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 - if [[ "$NEED_INSTALL" == true ]]; then - $STD systemctl stop mysql || true - $STD apt-get purge -y "^mysql-server.*" "^mysql-client.*" "^mysql-common.*" || true - rm -f /etc/apt/sources.list.d/mysql.list /etc/apt/trusted.gpg.d/mysql.gpg - - local DISTRO_CODENAME - DISTRO_CODENAME="$(awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release)" - curl -fsSL https://repo.mysql.com/RPM-GPG-KEY-mysql-2023 | gpg --dearmor -o /etc/apt/trusted.gpg.d/mysql.gpg - echo "deb [signed-by=/etc/apt/trusted.gpg.d/mysql.gpg] https://repo.mysql.com/apt/${CURRENT_OS}/ ${DISTRO_CODENAME} mysql-${MYSQL_VERSION}" \ - >/etc/apt/sources.list.d/mysql.list - - export DEBIAN_FRONTEND=noninteractive - $STD apt-get update - $STD apt-get install -y mysql-server - msg_ok "Setup MySQL $MYSQL_VERSION" - 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_CODENAME - 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-get purge -y "php${CURRENT_PHP//./}"* || true - fi - - # Ensure Sury repo is available - if [[ ! -f /etc/apt/sources.list.d/php.list ]]; then - $STD curl -fsSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb - $STD dpkg -i /tmp/debsuryorg-archive-keyring.deb - echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ ${DISTRO_CODENAME} main" \ - >/etc/apt/sources.list.d/php.list - $STD apt-get 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-get 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-get install -y $MODULE_LIST - msg_ok "Setup 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 - 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 msg_info "Patching $ini" - sed -i "s|^memory_limit = .*|memory_limit = ${PHP_MEMORY_LIMIT}|" "$ini" - sed -i "s|^upload_max_filesize = .*|upload_max_filesize = ${PHP_UPLOAD_MAX_FILESIZE}|" "$ini" - sed -i "s|^post_max_size = .*|post_max_size = ${PHP_POST_MAX_SIZE}|" "$ini" - sed -i "s|^max_execution_time = .*|max_execution_time = ${PHP_MAX_EXECUTION_TIME}|" "$ini" - $STD msg_ok "Patched $ini" - fi + local clean_tags=() + for t in "${raw_tags[@]}"; do + clean_tags+=("${t#v}") 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 + 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 - $STD a2enmod mpm_prefork - $STD a2enmod "php${PHP_VERSION}" - $STD systemctl 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 - $STD systemctl restart php${PHP_VERSION}-fpm - else - msg_warn "FPM requested but service php${PHP_VERSION}-fpm not found" - fi - fi -} - -# ------------------------------------------------------------------------------ -# Installs or updates Composer globally. -# -# Description: -# - Downloads latest version from getcomposer.org -# - Installs to /usr/local/bin/composer -# ------------------------------------------------------------------------------ - -function setup_composer() { - local COMPOSER_BIN="/usr/local/bin/composer" - export COMPOSER_ALLOW_SUPERUSER=1 - - # Check if composer is already installed - if [[ -x "$COMPOSER_BIN" ]]; then - local CURRENT_VERSION - CURRENT_VERSION=$("$COMPOSER_BIN" --version | awk '{print $3}') - $STD msg_info "Old Composer $CURRENT_VERSION found, updating to latest" - else - msg_info "Setup Composer" - fi - - # Download and install latest composer - curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php - php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer >/dev/null 2>&1 - - if [[ $? -ne 0 ]]; then - msg_error "Failed to install Composer" - return 1 - fi - - chmod +x "$COMPOSER_BIN" - $STD composer diagnose - msg_ok "Setup Composer" -} - -# ------------------------------------------------------------------------------ -# 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" + if [[ -z "$match_raw" ]]; then + msg_error "Pinned version ${pinned_version_in} not found upstream" return 1 fi - fi - local GO_BIN="/usr/local/bin/go" - local GO_INSTALL_DIR="/usr/local/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 [[ "$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 - $STD msg_info "Old Go Installation ($CURRENT_VERSION) found, upgrading to $GO_VERSION" - rm -rf "$GO_INSTALL_DIR" + msg_ok "${app} pinned to ${pinned_version_in} (already installed, upstream ${latest_raw})" fi - else - msg_info "Setup 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" return 1 - } - - 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" - - msg_ok "Setup Go $GO_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_CODENAME - DISTRO_CODENAME=$(awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release) - local DESIRED_PACKAGE="temurin-${JAVA_VERSION}-jdk" - - # Add Adoptium repo if missing - if [[ ! -f /etc/apt/sources.list.d/adoptium.list ]]; then - $STD msg_info "Setting up Adoptium Repository" - mkdir -p /etc/apt/keyrings - curl -fsSL "https://packages.adoptium.net/artifactory/api/gpg/key/public" | gpg --dearmor -o /etc/apt/trusted.gpg.d/adoptium.gpg - echo "deb [signed-by=/etc/apt/trusted.gpg.d/adoptium.gpg] https://packages.adoptium.net/artifactory/deb ${DISTRO_CODENAME} main" \ - >/etc/apt/sources.list.d/adoptium.list - $STD apt-get update - $STD msg_ok "Set up Adoptium Repository" fi - # Detect currently installed temurin version - 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 - $STD msg_info "Upgrading Temurin JDK $JAVA_VERSION" - $STD apt-get update - $STD apt-get install --only-upgrade -y "$DESIRED_PACKAGE" - $STD msg_ok "Upgraded Temurin JDK $JAVA_VERSION" - else - if [[ -n "$INSTALLED_VERSION" ]]; then - $STD msg_info "Removing Temurin JDK $INSTALLED_VERSION" - $STD apt-get purge -y "temurin-${INSTALLED_VERSION}-jdk" - fi - - msg_info "Setup Temurin JDK $JAVA_VERSION" - $STD apt-get install -y "$DESIRED_PACKAGE" - msg_ok "Setup Temurin JDK $JAVA_VERSION" - fi -} - -# ------------------------------------------------------------------------------ -# 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 MONGO_BASE_URL - 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 REPO_LIST="/etc/apt/sources.list.d/mongodb-org-${MONGO_VERSION}.list" - - local INSTALLED_VERSION="" - if command -v mongod >/dev/null; then - INSTALLED_VERSION=$(mongod --version | awk '/db version/{print $3}' | cut -d. -f1,2) - fi - - if [[ "$INSTALLED_VERSION" == "$MONGO_VERSION" ]]; then - $STD msg_info "Upgrading MongoDB $MONGO_VERSION" - $STD apt-get update - $STD apt-get install --only-upgrade -y mongodb-org - $STD msg_ok "Upgraded MongoDB $MONGO_VERSION" + # 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 - if [[ -n "$INSTALLED_VERSION" ]]; then - $STD systemctl stop mongod || true - $STD apt-get purge -y mongodb-org || true - rm -f /etc/apt/sources.list.d/mongodb-org-*.list - rm -f /etc/apt/trusted.gpg.d/mongodb-*.gpg - else - msg_info "Setup MongoDB $MONGO_VERSION" + 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 - curl -fsSL "https://pgp.mongodb.com/server-${MONGO_VERSION}.asc" | gpg --dearmor -o "/etc/apt/trusted.gpg.d/mongodb-${MONGO_VERSION}.gpg" - echo "deb [signed-by=/etc/apt/trusted.gpg.d/mongodb-${MONGO_VERSION}.gpg] ${MONGO_BASE_URL} ${DISTRO_CODENAME}/mongodb-org/${MONGO_VERSION} main" \ - >"$REPO_LIST" + $STD apt update + $STD apt install -y openssl - $STD apt-get update || { - msg_error "APT update failed — invalid MongoDB repo for ${DISTRO_ID}-${DISTRO_CODENAME}?" - return 1 - } + 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" - $STD apt-get install -y mongodb-org + chmod 600 "$CERT_KEY" + chmod 644 "$CERT_CRT" +} - mkdir -p /var/lib/mongodb - chown -R mongodb:mongodb /var/lib/mongodb +# ------------------------------------------------------------------------------ +# Downloads file with optional progress indicator using pv. +# +# Arguments: +# $1 - URL +# $2 - Destination path +# ------------------------------------------------------------------------------ - $STD systemctl enable mongod - $STD systemctl start mongod - msg_ok "Setup MongoDB $MONGO_VERSION" +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 } # ------------------------------------------------------------------------------ @@ -844,9 +997,7 @@ function fetch_and_deploy_gh_release() { local current_version="" [[ -f "$version_file" ]] && current_version=$(<"$version_file") - if ! command -v jq &>/dev/null; then - $STD apt-get install -y jq &>/dev/null - fi + 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" @@ -977,7 +1128,7 @@ function fetch_and_deploy_gh_release() { } chmod 644 "$tmpdir/$filename" - $STD apt-get install -y "$tmpdir/$filename" || { + $STD apt install -y "$tmpdir/$filename" || { $STD dpkg -i "$tmpdir/$filename" || { msg_error "Both apt and dpkg installation failed" rm -rf "$tmpdir" @@ -1027,9 +1178,7 @@ function fetch_and_deploy_gh_release() { fi if [[ "$filename" == *.zip ]]; then - if ! command -v unzip &>/dev/null; then - $STD apt-get install -y unzip - fi + ensure_dependencies unzip unzip -q "$tmpdir/$filename" -d "$unpack_tmp" elif [[ "$filename" == *.tar.* || "$filename" == *.tgz ]]; then tar -xf "$tmpdir/$filename" -C "$unpack_tmp" @@ -1131,6 +1280,606 @@ function fetch_and_deploy_gh_release() { 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. # @@ -1149,8 +1898,8 @@ function setup_local_ip_helper() { # Install networkd-dispatcher if not present if ! dpkg -s networkd-dispatcher >/dev/null 2>&1; then - $STD apt-get update - $STD apt-get install -y networkd-dispatcher + $STD apt update + $STD apt install -y networkd-dispatcher fi # Write update_local_ip.sh @@ -1210,81 +1959,895 @@ EOF } # ------------------------------------------------------------------------------ -# Loads LOCAL_IP from persistent store or detects if missing. +# Installs or updates MariaDB from official repo. # # Description: -# - Loads from /run/local-ip.env or performs runtime lookup +# - 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) # ------------------------------------------------------------------------------ -function import_local_ip() { - local IP_FILE="/run/local-ip.env" - if [[ -f "$IP_FILE" ]]; then - # shellcheck disable=SC1090 - source "$IP_FILE" +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 [[ -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" + 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 - export LOCAL_IP + 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" } # ------------------------------------------------------------------------------ -# Downloads file with optional progress indicator using pv. +# Installs or updates MongoDB to specified major version. # -# Arguments: -# $1 - URL -# $2 - Destination path +# Description: +# - Preserves data across installations +# - Adds official MongoDB repo +# +# Variables: +# MONGO_VERSION - MongoDB major version to install (e.g. 7.0, 8.0) # ------------------------------------------------------------------------------ -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 +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) - if ! command -v pv &>/dev/null; then - $STD apt-get install -y pv - fi - 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" + # 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 -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 - if ! curl -fsSL "$url" | pv -s "$content_length" >"$output"; then - msg_error "Download failed" + 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 } @@ -1298,12 +2861,11 @@ function download_with_progress() { function setup_uv() { local UV_BIN="/usr/local/bin/uv" - local TMP_DIR - TMP_DIR=$(mktemp -d) + local TMP_DIR=$(mktemp -d) + local CACHED_VERSION + CACHED_VERSION=$(get_cached_version "uv") - # Determine system architecture - local ARCH - ARCH=$(uname -m) + local ARCH=$(uname -m) local UV_TAR case "$ARCH" in @@ -1328,45 +2890,48 @@ function setup_uv() { ;; esac - # Get latest version from GitHub + ensure_dependencies jq + local LATEST_VERSION LATEST_VERSION=$(curl -fsSL https://api.github.com/repos/astral-sh/uv/releases/latest | - grep '"tag_name":' | cut -d '"' -f4 | sed 's/^v//') + jq -r '.tag_name' | sed 's/^v//') if [[ -z "$LATEST_VERSION" ]]; then - msg_error "Could not fetch latest uv version from GitHub." + msg_error "Could not fetch latest uv version" rm -rf "$TMP_DIR" return 1 fi - # Check if uv is already up to date if [[ -x "$UV_BIN" ]]; then local INSTALLED_VERSION - INSTALLED_VERSION=$($UV_BIN -V | awk '{print $2}') + 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" - [[ ":$PATH:" != *":/usr/local/bin:"* ]] && export PATH="/usr/local/bin:$PATH" return 0 else msg_info "Updating uv from $INSTALLED_VERSION to $LATEST_VERSION" fi else - msg_info "Setup uv $LATEST_VERSION" + msg_info "Installing uv $LATEST_VERSION" fi - # Download and install manually local UV_URL="https://github.com/astral-sh/uv/releases/latest/download/${UV_TAR}" - if ! curl -fsSL "$UV_URL" -o "$TMP_DIR/uv.tar.gz"; then - msg_error "Failed to download $UV_URL" + curl -fsSL "$UV_URL" -o "$TMP_DIR/uv.tar.gz" || { + msg_error "Failed to download uv" rm -rf "$TMP_DIR" return 1 - fi + } - if ! tar -xzf "$TMP_DIR/uv.tar.gz" -C "$TMP_DIR"; then - msg_error "Failed to extract uv archive" + tar -xzf "$TMP_DIR/uv.tar.gz" -C "$TMP_DIR" || { + msg_error "Failed to extract uv" rm -rf "$TMP_DIR" return 1 - fi + } install -m 755 "$TMP_DIR"/*/uv "$UV_BIN" || { msg_error "Failed to install uv binary" @@ -1375,14 +2940,13 @@ function setup_uv() { } rm -rf "$TMP_DIR" - #ensure_usr_local_bin_persist - $STD uv python update-shell || { - msg_error "Failed to update uv shell integration" - return 1 - } - msg_ok "Setup uv $LATEST_VERSION" + 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" - # Optional: install specific Python version if [[ -n "${PYTHON_VERSION:-}" ]]; then local VERSION_MATCH VERSION_MATCH=$(uv python list --only-downloads | @@ -1390,291 +2954,20 @@ function setup_uv() { cut -d'-' -f2 | sort -V | tail -n1) if [[ -z "$VERSION_MATCH" ]]; then - msg_error "No matching Python $PYTHON_VERSION.x version found via uv" + 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 - if ! $STD uv python install "$VERSION_MATCH"; then - msg_error "Failed to install Python $VERSION_MATCH via uv" + $STD uv python install "$VERSION_MATCH" || { + msg_error "Failed to install Python $VERSION_MATCH" return 1 - fi - msg_ok "Setup Python $VERSION_MATCH via uv" + } + msg_ok "Installed Python $VERSION_MATCH" 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 -} - -# ------------------------------------------------------------------------------ -# Installs or updates Ghostscript (gs) from source. -# -# Description: -# - Fetches latest release -# - Builds and installs system-wide -# ------------------------------------------------------------------------------ - -function setup_gs() { - mkdir -p /tmp - TMP_DIR=$(mktemp -d) - CURRENT_VERSION=$(gs --version 2>/dev/null || echo "0") - - RELEASE_JSON=$(curl -fsSL https://api.github.com/repos/ArtifexSoftware/ghostpdl-downloads/releases/latest) - LATEST_VERSION=$(echo "$RELEASE_JSON" | grep '"tag_name":' | head -n1 | cut -d '"' -f4 | sed 's/^gs//') - LATEST_VERSION_DOTTED=$(echo "$RELEASE_JSON" | grep '"name":' | head -n1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+') - - if [[ -z "$LATEST_VERSION" ]]; then - msg_error "Could not determine latest Ghostscript version from GitHub." - rm -rf "$TMP_DIR" - return - fi - - if dpkg --compare-versions "$CURRENT_VERSION" ge "$LATEST_VERSION_DOTTED"; then - rm -rf "$TMP_DIR" - return - fi - - msg_info "Setup Ghostscript $LATEST_VERSION_DOTTED (Patience)" - 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" - - if ! tar -xzf "$TMP_DIR/ghostscript.tar.gz" -C "$TMP_DIR"; then - msg_error "Failed to extract Ghostscript archive." - rm -rf "$TMP_DIR" - return - fi - - cd "$TMP_DIR/ghostscript-${LATEST_VERSION_DOTTED}" || { - msg_error "Failed to enter Ghostscript source directory." - rm -rf "$TMP_DIR" - } - $STD apt-get install -y build-essential libpng-dev zlib1g-dev - $STD ./configure >/dev/null - $STD make - $STD sudo make install - local EXIT_CODE=$? - 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" - - if [[ $EXIT_CODE -eq 0 ]]; then - msg_ok "Setup Ghostscript $LATEST_VERSION_DOTTED" - else - msg_error "Ghostscript installation failed" - 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 - TMP_DIR=$(mktemp -d) - - msg_info "Setup Ruby $RUBY_VERSION" - - local RBENV_RELEASE - RBENV_RELEASE=$(curl -fsSL https://api.github.com/repos/rbenv/rbenv/releases/latest | grep '"tag_name":' | cut -d '"' -f4 | 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" - tar -xzf "$TMP_DIR/rbenv.tar.gz" -C "$TMP_DIR" - 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 | grep '"tag_name":' | cut -d '"' -f4 | 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" - tar -xzf "$TMP_DIR/ruby-build.tar.gz" -C "$TMP_DIR" - 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 - msg_info "Setup Rails via gem" - gem install rails - msg_ok "Setup Rails $(rails -v)" - fi - - rm -rf "$TMP_DIR" - msg_ok "Setup Ruby $RUBY_VERSION" -} - -# ------------------------------------------------------------------------------ -# 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) -# ------------------------------------------------------------------------------ -function create_selfsigned_certs() { - local app=${APP:-$(echo "${APPLICATION,,}" | tr -d ' ')} - $STD openssl req -x509 -nodes -days 365 -newkey rsa:4096 \ - -keyout /etc/ssl/private/"$app"-selfsigned.key \ - -out /etc/ssl/certs/"$app"-selfsigned.crt \ - -subj "/C=US/O=$app/OU=Domain Control Validated/CN=localhost" -} - -# ------------------------------------------------------------------------------ -# 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" - - # rustup & toolchain - if ! command -v rustup &>/dev/null; then - msg_info "Setup Rust" - curl -fsSL https://sh.rustup.rs | $STD sh -s -- -y --default-toolchain "$RUST_TOOLCHAIN" - export PATH="$CARGO_BIN:$PATH" - echo 'export PATH="$HOME/.cargo/bin:$PATH"' >>"$HOME/.profile" - msg_ok "Setup Rust" - else - $STD rustup install "$RUST_TOOLCHAIN" - $STD rustup default "$RUST_TOOLCHAIN" - $STD rustup update "$RUST_TOOLCHAIN" - msg_ok "Rust toolchain set to $RUST_TOOLCHAIN" - fi - - # install/update crates - 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 "Update $NAME: $INSTALLED_VER → $VER" - $STD cargo install "$NAME" --version "$VER" --force - msg_ok "Updated $NAME to $VER" - elif [[ -z "$VER" ]]; then - msg_info "Update $NAME: $INSTALLED_VER → latest" - $STD cargo install "$NAME" --force - msg_ok "Updated $NAME to latest" - fi - else - msg_info "Setup $NAME ${VER:+($VER)}" - $STD cargo install "$NAME" ${VER:+--version "$VER"} - msg_ok "Setup $NAME ${VER:-latest}" - fi - done - msg_ok "Setup Rust" - fi -} - -# ------------------------------------------------------------------------------ -# 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() { - if grep -qi alpine /etc/os-release; then - msg_info "Setup Adminer (Alpine)" - mkdir -p /var/www/localhost/htdocs/adminer - if ! curl -fsSL https://github.com/vrana/adminer/releases/latest/download/adminer.php \ - -o /var/www/localhost/htdocs/adminer/index.php; then - msg_error "Failed to download Adminer" - return 1 - fi - msg_ok "Adminer available at /adminer (Alpine)" - else - msg_info "Setup Adminer (Debian/Ubuntu)" - $STD apt-get install -y adminer - $STD a2enconf adminer - $STD systemctl reload apache2 - msg_ok "Adminer available at /adminer (Debian/Ubuntu)" - fi -} - # ------------------------------------------------------------------------------ # Installs or updates yq (mikefarah/yq - Go version). # @@ -1685,51 +2978,56 @@ function setup_adminer() { # ------------------------------------------------------------------------------ function setup_yq() { - local TMP_DIR - TMP_DIR=$(mktemp -d) - local CURRENT_VERSION="" + 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") - if ! command -v jq &>/dev/null; then - $STD apt-get update - $STD apt-get install -y jq || { - msg_error "Failed to install jq" - rm -rf "$TMP_DIR" - return 1 - } - fi + 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 | awk '{print $NF}' | sed 's/^v//') + CURRENT_VERSION=$(yq --version 2>/dev/null | awk '{print $NF}' | sed 's/^v//') fi fi - local RELEASE_JSON - RELEASE_JSON=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest") local LATEST_VERSION - LATEST_VERSION=$(echo "$RELEASE_JSON" | jq -r '.tag_name' | sed 's/^v//') + 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 from GitHub." + msg_error "Could not determine latest yq version" rm -rf "$TMP_DIR" return 1 fi if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$LATEST_VERSION" ]]; then - return + 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 "Setup yq ($LATEST_VERSION)" - curl -fsSL "https://github.com/${GITHUB_REPO}/releases/download/v${LATEST_VERSION}/yq_linux_amd64" -o "$TMP_DIR/yq" + 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 to $BINARY_PATH" + msg_error "Failed to install yq" rm -rf "$TMP_DIR" return 1 fi @@ -1738,471 +3036,7 @@ function setup_yq() { hash -r local FINAL_VERSION - FINAL_VERSION=$("$BINARY_PATH" --version 2>/dev/null | awk '{print $NF}') - if [[ "$FINAL_VERSION" == "v$LATEST_VERSION" ]]; then - msg_ok "Setup yq ($LATEST_VERSION)" - else - msg_error "yq installation incomplete or version mismatch" - fi -} - -# ------------------------------------------------------------------------------ -# 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 - TMP_DIR=$(mktemp -d) - local VERSION="" - local BINARY_PATH="/usr/local/bin/magick" - - if command -v magick &>/dev/null; then - VERSION=$(magick -version | awk '/^Version/ {print $3}') - msg_ok "ImageMagick already installed ($VERSION)" - return 0 - fi - - msg_info "Setup ImageMagick (Patience)" - $STD apt-get update - $STD apt-get install -y \ - 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" - tar -xzf "$TMP_DIR/ImageMagick.tar.gz" -C "$TMP_DIR" - cd "$TMP_DIR"/ImageMagick-* || { - msg_error "Source extraction failed" - rm -rf "$TMP_DIR" - return 1 - } - - ./configure --disable-static >/dev/null - $STD make - $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" - ensure_usr_local_bin_persist - msg_ok "Setup ImageMagick $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 - 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" - - # 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" - 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 - rm -rf "$TMP_DIR" - msg_ok "Installed FFmpeg binary ($($BIN_PATH -version | head -n1))" - return - fi - - if ! command -v jq &>/dev/null; then - $STD apt-get update - $STD apt-get install -y jq - fi - - # Auto-detect latest stable version if none specified - if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then - msg_info "Resolving latest FFmpeg tag" - 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 - ) - ;; - *) - msg_error "Invalid FFMPEG_TYPE: $TYPE" - rm -rf "$TMP_DIR" - return 1 - ;; - esac - - $STD apt-get update - $STD apt-get install -y "${DEPS[@]}" - - curl -fsSL "https://github.com/${GITHUB_REPO}/archive/refs/tags/${VERSION}.tar.gz" -o "$TMP_DIR/ffmpeg.tar.gz" - tar -xzf "$TMP_DIR/ffmpeg.tar.gz" -C "$TMP_DIR" - 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) - fi - - if [[ ${#args[@]} -eq 0 ]]; then - msg_error "FFmpeg configure args array is empty – aborting." - rm -rf "$TMP_DIR" - return 1 - fi - - ./configure "${args[@]}" >"$TMP_DIR/configure.log" 2>&1 || { - msg_error "FFmpeg ./configure failed (see $TMP_DIR/configure.log)" - cat "$TMP_DIR/configure.log" | tail -n 20 - rm -rf "$TMP_DIR" - return 1 - } - - $STD make -j"$(nproc)" - $STD make install - echo "/usr/local/lib" >/etc/ld.so.conf.d/ffmpeg.conf - ldconfig - - ldconfig -p | grep libavdevice >/dev/null || { - msg_error "libavdevice not registered with dynamic linker" - 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" - ensure_usr_local_bin_persist - msg_ok "Setup FFmpeg $FINAL_VERSION" -} - -# ------------------------------------------------------------------------------ -# Installs ClickHouse server and client, sets up DB/user with credentials. -# -# Description: -# - Adds official ClickHouse APT repo with GPG key -# - Installs clickhouse-server and clickhouse-client -# - Creates database and user (credentials optionally overrideable via env) -# -# Variables: -# CLICKHOUSE_DB - Database name (default: analytics) -# CLICKHOUSE_USER - Username (default: analytics_user) -# CLICKHOUSE_PASS - Password (default: auto-generated) -# ------------------------------------------------------------------------------ - -function setup_clickhouse() { - local CLICKHOUSE_DB="${CLICKHOUSE_DB:-analytics}" - local CLICKHOUSE_USER="${CLICKHOUSE_USER:-analytics_user}" - local CLICKHOUSE_PASS="${CLICKHOUSE_PASS:-$(openssl rand -base64 18 | cut -c1-13)}" - local GPG_URL="https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key" - local GPG_KEY_PATH="/usr/share/keyrings/clickhouse-keyring.gpg" - local ARCH - ARCH=$(dpkg --print-architecture) - - if ! command -v clickhouse >/dev/null; then - msg_info "Setup ClickHouse" - - if ! curl -fsSL --connect-timeout 5 https://packages.clickhouse.com >/dev/null 2>&1; then - msg_error "Connection to packages.clickhouse.com:443 failed – possibly blocked" - echo "💡 Check AdGuard/Pi-hole or firewall rules" - return 1 - fi - - if ! curl -fsSL --retry 3 --connect-timeout 10 "$GPG_URL" | - gpg --dearmor -o "$GPG_KEY_PATH"; then - msg_error "Failed to fetch ClickHouse GPG key" - return 1 - fi - - echo "deb [signed-by=$GPG_KEY_PATH arch=$ARCH] https://packages.clickhouse.com/deb stable main" \ - >/etc/apt/sources.list.d/clickhouse.list - - env -u CLICKHOUSE_USER $STD apt-get update - env -u CLICKHOUSE_USER DEBIAN_FRONTEND=noninteractive $STD apt-get install -y clickhouse-server clickhouse-client - - $STD systemctl enable --now clickhouse-server - - msg_info "Waiting for ClickHouse to be ready" - for i in {1..10}; do - if clickhouse client --query "SELECT 1" &>/dev/null; then break; fi - sleep 1 - done - - # User anlegen - clickhouse client --query "CREATE DATABASE IF NOT EXISTS $CLICKHOUSE_DB" - clickhouse client --query "CREATE USER IF NOT EXISTS $CLICKHOUSE_USER IDENTIFIED WITH plaintext_password BY '$CLICKHOUSE_PASS'" - clickhouse client --query "GRANT ALL ON $CLICKHOUSE_DB.* TO $CLICKHOUSE_USER" - - # Default-User ggf. deaktivieren - cat </etc/clickhouse-server/users.d/disable-default.xml - - - - - -EOF - systemctl restart clickhouse-server - - msg_ok "Setup ClickHouse (DB: $CLICKHOUSE_DB, User: $CLICKHOUSE_USER)" - - { - echo "ClickHouse DB: $CLICKHOUSE_DB" - echo "ClickHouse User: $CLICKHOUSE_USER" - echo "ClickHouse Pass: $CLICKHOUSE_PASS" - } >>~/clickhouse.creds - else - msg_info "Updating ClickHouse packages" - env -u CLICKHOUSE_USER $STD apt-get update - env -u CLICKHOUSE_USER $STD apt-get install -y --only-upgrade clickhouse-server clickhouse-client - msg_ok "ClickHouse updated" - fi -} - -# ------------------------------------------------------------------------------ -# 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: -# check_for_gh_release "AppName" "user/repo" -# if [[ $? -eq 0 ]]; then -# echo "New version available: $CHECK_UPDATE_RELEASE" -# # trigger update... -# fi -# -# 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="${3:-}" # optional - local current_file="$HOME/.${app,,}" - - msg_info "Check for update: ${app}" - - # DNS check for GitHub - if ! getent hosts api.github.com >/dev/null 2>&1; then - msg_error "Network error: cannot resolve api.github.com" - return 1 - fi - - # jq check - if ! command -v jq &>/dev/null; then - apt-get update -qq &>/dev/null && apt-get install -y jq &>/dev/null || { - msg_error "Failed to install jq" - return 1 - } - fi - - # get latest release - local release - release=$(curl -fsSL "https://api.github.com/repos/${source}/releases/latest" | - jq -r '.tag_name' | sed 's/^v//') - - if [[ -z "$release" ]]; then - msg_error "Unable to determine latest release for ${app}" - return 1 - fi - - local current="" - [[ -f "$current_file" ]] && current=$(<"$current_file") - - # PINNED Releases - if [[ -n "$pinned_version" ]]; then - if [[ "$pinned_version" == "$release" ]]; then - msg_ok "${app} pinned to v${pinned_version} (no update needed)" - return 1 - else - if [[ "$current" == "$pinned_version" ]]; then - msg_ok "${app} pinned to v${pinned_version} (already installed, upstream v${release})" - return 1 - fi - msg_info "${app} pinned to v${pinned_version} (upstream v${release}) → update/downgrade required" - CHECK_UPDATE_RELEASE="$pinned_version" - return 0 - fi - fi - - if [[ "$release" != "$current" ]] || [[ ! -f "$current_file" ]]; then - CHECK_UPDATE_RELEASE="$release" - msg_info "New release available: v${release} (current: v${current:-none})" - return 0 - else - msg_ok "${app} is up to date (v${release})" - return 1 - fi -} - -# ------------------------------------------------------------------------------ -# Hardware acceleration setup inside container -# Works with: Debian 12 (bookworm), Debian 13 (trixie), Ubuntu 24.04 (noble) -# Usage: hwaccel_setup_in_ct [--nonfree-intel] -# ------------------------------------------------------------------------------ - -hwaccel_setup_in_ct() { - local CTTYPE="$1" NONFREE=0 - [[ "$2" == "--nonfree-intel" ]] && NONFREE=1 - - # Detect OS info inside the container - local ID VERSION_CODENAME - if [[ -r /etc/os-release ]]; then - . /etc/os-release - fi - ID="${ID:-debian}" - VERSION_CODENAME="${VERSION_CODENAME:-bookworm}" - - msg_info "Setting up hardware acceleration for ${ID^} ($VERSION_CODENAME)" - - case "$ID" in - debian | ubuntu) - if ((NONFREE)) && [[ "$VERSION_CODENAME" =~ (trixie|noble) ]]; then - # Debian 13 / Ubuntu 24.04 → non-free Intel driver - cat >/etc/apt/sources.list.d/non-free.sources <<'SRC' -Types: deb deb-src -URIs: http://deb.debian.org/debian -Suites: trixie -Components: non-free non-free-firmware - -Types: deb deb-src -URIs: http://deb.debian.org/debian-security -Suites: trixie-security -Components: non-free non-free-firmware - -Types: deb deb-src -URIs: http://deb.debian.org/debian -Suites: trixie-updates -Components: non-free non-free-firmware -SRC - - $STD apt-get update - $STD apt-get install -y \ - intel-media-va-driver-non-free \ - ocl-icd-libopencl1 \ - mesa-opencl-icd \ - mesa-va-drivers \ - libvpl2 \ - vainfo \ - intel-gpu-tools - else - # Debian 12 (bookworm) and fallback for Debian 13/Ubuntu 24.04 without non-free - $STD apt-get update - $STD apt-get install -y \ - va-driver-all \ - ocl-icd-libopencl1 \ - mesa-opencl-icd \ - mesa-va-drivers \ - vainfo \ - intel-gpu-tools - fi - ;; - *) - msg_warn "Unsupported distribution ($ID $VERSION_CODENAME) – skipping HW accel setup" - return 0 - ;; - esac - - # Add current user to video/render groups (only for privileged CTs) - if [[ "$CTTYPE" == "0" ]]; then - $STD adduser "$(id -un)" video || true - $STD adduser "$(id -un)" render || true - fi - - msg_ok "Hardware acceleration is ready" + 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" } diff --git a/misc/tools.func.bak b/misc/tools.func.bak new file mode 100644 index 000000000..e98332517 --- /dev/null +++ b/misc/tools.func.bak @@ -0,0 +1,2208 @@ +#!/bin/bash + +# ------------------------------------------------------------------------------ +# Installs Microsoft .NET +# +# Description: +# - Installs specified .NET version using Microsoft APT repo +# +# Variables: +# NET_VERSION - .NET version to install (default: "8.0") +# ------------------------------------------------------------------------------ + +function setup_dotnet() { + local NET_VERSION="${NET_VERSION:-8.0}" + local DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{ print $2 }' /etc/os-release) + local NEED_NET_INSTALL=false + + if command -v dotnet >/dev/null; then + CURRENT_NET_VERSION=$(dotnet --version | awk -F. '{print $1 "." $2}') + if [[ "$CURRENT_NET_VERSION" == "$NET_VERSION" ]]; then + : + fi + else + msg_info "Setup .NET $NET_VERSION" + NEED_NET_INSTALL=true + fi + + if [[ "$NEED_NET_INSTALL" == true ]]; then + curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg + if [[ "$DISTRO_CODENAME" != "trixie" ]]; then + curl -fsSL https://packages.microsoft.com/config/debian/12/prod.list -o /etc/apt/sources.list.d/msprod.list + else + curl -fsSL https://packages.microsoft.com/config/debian/13/prod.list -o /etc/apt/sources.list.d/msprod.list + fi + $STD apt-get update + $STD apt-get install -y dotnet-sdk-$NET_VERSION + msg_ok "Setup .NET ${NET_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 + + 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 + + if ! command -v jq &>/dev/null; then + $STD apt-get update + $STD apt-get install -y jq || { + msg_error "Failed to install jq" + return 1 + } + fi + + if [[ "$NEED_NODE_INSTALL" == true ]]; then + $STD apt-get purge -y nodejs + rm -f /etc/apt/sources.list.d/nodesource.list /usr/share/keyrings/nodesource.gpg + + mkdir -p /usr/share/keyrings + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | + gpg --dearmor -o /usr/share/keyrings/nodesource.gpg || { + msg_error "Failed to import NodeSource GPG key" + exit 1 + } + chmod 644 /usr/share/keyrings/nodesource.gpg + + local ARCH + ARCH=$(dpkg --print-architecture) + if ! [[ "$ARCH" =~ ^(amd64|arm64|armhf)$ ]]; then + msg_error "Unsupported architecture: $ARCH" + exit 1 + fi + + echo "deb [arch=$ARCH signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_VERSION}.x nodistro main" >/etc/apt/sources.list.d/nodesource.list + + cat </etc/apt/preferences.d/nodejs +Package: nodejs +Pin: origin deb.nodesource.com +Pin-Priority: 700 +EOF + + sleep 2 + if ! apt-get update >/dev/null 2>&1; then + msg_warn "APT update failed – retrying in 5s" + sleep 5 + if ! apt-get update >/dev/null 2>&1; then + msg_error "Failed to update APT repositories after adding NodeSource" + exit 1 + fi + fi + + if ! apt-get install -y -t nodistro nodejs >/dev/null 2>&1; then + msg_error "Failed to install Node.js ${NODE_VERSION} from NodeSource" + apt-cache policy nodejs | tee "$STD" + exit 1 + fi + + $STD npm install -g npm@latest || { + msg_error "Failed to update npm to latest version" + } + msg_ok "Setup Node.js ${NODE_VERSION}" + fi + + export NODE_OPTIONS="--max-old-space-size=4096" + + [[ -d /opt ]] || mkdir -p /opt + cd /opt || { + msg_error "Failed to set safe working directory before npm install" + exit 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" + $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}" || { + msg_error "Failed to update $MODULE_NAME to version $MODULE_REQ_VERSION" + exit 1 + } + elif [[ "$MODULE_REQ_VERSION" == "latest" ]]; then + msg_info "Updating $MODULE_NAME to latest version" + $STD npm install -g "${MODULE_NAME}@latest" || { + msg_error "Failed to update $MODULE_NAME to latest version" + exit 1 + } + fi + else + msg_info "Installing $MODULE_NAME@$MODULE_REQ_VERSION" + $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}" || { + msg_error "Failed to install $MODULE_NAME@$MODULE_REQ_VERSION" + exit 1 + } + fi + done + msg_ok "Installed Node.js modules: $NODE_MODULE" + 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 + local NEED_PG_INSTALL=false + DISTRO="$(awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release)" + + if command -v psql >/dev/null; then + CURRENT_PG_VERSION="$(psql -V | awk '{print $3}' | cut -d. -f1)" + if [[ "$CURRENT_PG_VERSION" == "$PG_VERSION" ]]; then + : # PostgreSQL is already at the desired version – no action needed + else + $STD msg_info "Detected PostgreSQL $CURRENT_PG_VERSION, preparing upgrade to $PG_VERSION" + NEED_PG_INSTALL=true + fi + else + NEED_PG_INSTALL=true + fi + + if [[ "$NEED_PG_INSTALL" == true ]]; then + if [[ -n "$CURRENT_PG_VERSION" ]]; then + $STD msg_info "Dumping PostgreSQL $CURRENT_PG_VERSION data" + su - postgres -c "pg_dumpall > /var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql" + $STD msg_ok "Data dump completed" + + systemctl stop postgresql + fi + + rm -f /etc/apt/sources.list.d/pgdg.list /etc/apt/trusted.gpg.d/postgresql.gpg + + $STD msg_info "Adding PostgreSQL PGDG repository" + curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | + gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg + + echo "deb https://apt.postgresql.org/pub/repos/apt ${DISTRO}-pgdg main" \ + >/etc/apt/sources.list.d/pgdg.list + $STD apt-get update + $STD msg_ok "Repository added" + + msg_info "Setup PostgreSQL $PG_VERSION" + $STD apt-get install -y "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}" + + if [[ -n "$CURRENT_PG_VERSION" ]]; then + $STD apt-get purge -y "postgresql-${CURRENT_PG_VERSION}" "postgresql-client-${CURRENT_PG_VERSION}" || true + fi + systemctl enable -q --now postgresql + + if [[ -n "$CURRENT_PG_VERSION" ]]; then + $STD msg_info "Restoring dumped data" + su - postgres -c "psql < /var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql" + $STD msg_ok "Data restored" + fi + + $STD msg_ok "PostgreSQL $PG_VERSION installed" + fi + + # Install optional PostgreSQL modules + if [[ -n "$PG_MODULES" ]]; then + IFS=',' read -ra MODULES <<<"$PG_MODULES" + for module in "${MODULES[@]}"; do + local pkg="postgresql-${PG_VERSION}-${module}" + $STD msg_info "Setup PostgreSQL module/s: $pkg" + $STD apt-get install -y "$pkg" || { + msg_error "Failed to install $pkg" + continue + } + done + $STD msg_ok "Setup PostgreSQL modules" + fi +} + +# ------------------------------------------------------------------------------ +# 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) +# ------------------------------------------------------------------------------ test + +setup_mariadb() { + local MARIADB_VERSION="${MARIADB_VERSION:-latest}" + local DISTRO_CODENAME + DISTRO_CODENAME="$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)" + CURRENT_OS="$(awk -F= '/^ID=/{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 + + msg_info "Setting up MariaDB $MARIADB_VERSION" + # Grab dynamic latest LTS version + 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 + + if [[ "$CURRENT_VERSION" == "$MARIADB_VERSION" ]]; then + $STD msg_info "MariaDB $MARIADB_VERSION, upgrading" + $STD apt-get update + $STD apt-get install --only-upgrade -y mariadb-server mariadb-client + $STD msg_ok "MariaDB upgraded to $MARIADB_VERSION" + return 0 + fi + + if [[ -n "$CURRENT_VERSION" ]]; then + $STD msg_info "Upgrading MariaDB $CURRENT_VERSION to $MARIADB_VERSION" + $STD systemctl stop mariadb >/dev/null 2>&1 || true + $STD apt-get purge -y 'mariadb*' || true + rm -f /etc/apt/sources.list.d/mariadb.list /etc/apt/trusted.gpg.d/mariadb.gpg + else + $STD msg_info "Setup MariaDB $MARIADB_VERSION" + fi + + curl -fsSL "https://mariadb.org/mariadb_release_signing_key.asc" | + gpg --dearmor -o /etc/apt/trusted.gpg.d/mariadb.gpg + + echo "deb [signed-by=/etc/apt/trusted.gpg.d/mariadb.gpg] http://mirror.mariadb.org/repo/${MARIADB_VERSION}/${CURRENT_OS} ${DISTRO_CODENAME} main" \ + >/etc/apt/sources.list.d/mariadb.list + + $STD apt-get update + + 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 + else + for ver in 12.1 12.0 11.4 11.3 11.2 11.1 11.0 10.11 10.6 10.5 10.4 10.3; do + echo "mariadb-server-$ver mariadb-server/feedback boolean false" | debconf-set-selections + done + fi + DEBIAN_FRONTEND=noninteractive $STD apt-get install -y mariadb-server mariadb-client || { + msg_warn "Failed to install MariaDB ${MARIADB_VERSION} from upstream repo – trying distro package as fallback..." + # Cleanup, remove upstream repo to avoid conflicts + rm -f /etc/apt/sources.list.d/mariadb.list /etc/apt/trusted.gpg.d/mariadb.gpg + $STD apt-get update + # Final fallback: distro package + DEBIAN_FRONTEND=noninteractive $STD apt-get install -y mariadb-server mariadb-client || { + msg_error "MariaDB installation failed even with distro fallback!" + return 1 + } + msg_ok "Setup MariaDB (distro fallback)" + return 0 + } + + msg_ok "Setup MariaDB $MARIADB_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 + CURRENT_OS="$(awk -F= '/^ID=/{print $2}' /etc/os-release)" + + if command -v mysql >/dev/null; then + CURRENT_VERSION="$(mysql --version | grep -oP 'Distrib\s+\K[0-9]+\.[0-9]+')" + if [[ "$CURRENT_VERSION" != "$MYSQL_VERSION" ]]; then + $STD msg_info "MySQL $CURRENT_VERSION will be upgraded to $MYSQL_VERSION" + NEED_INSTALL=true + else + # Check for patch-level updates + if apt list --upgradable 2>/dev/null | grep -q '^mysql-server/'; then + $STD msg_info "MySQL $CURRENT_VERSION available for upgrade" + $STD apt-get update + $STD apt-get install --only-upgrade -y mysql-server + $STD 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-get purge -y "^mysql-server.*" "^mysql-client.*" "^mysql-common.*" || true + rm -f /etc/apt/sources.list.d/mysql.list /etc/apt/trusted.gpg.d/mysql.gpg + + local DISTRO_CODENAME + DISTRO_CODENAME="$(awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release)" + curl -fsSL https://repo.mysql.com/RPM-GPG-KEY-mysql-2023 | gpg --dearmor -o /etc/apt/trusted.gpg.d/mysql.gpg + echo "deb [signed-by=/etc/apt/trusted.gpg.d/mysql.gpg] https://repo.mysql.com/apt/${CURRENT_OS}/ ${DISTRO_CODENAME} mysql-${MYSQL_VERSION}" \ + >/etc/apt/sources.list.d/mysql.list + + export DEBIAN_FRONTEND=noninteractive + $STD apt-get update + $STD apt-get install -y mysql-server + msg_ok "Setup MySQL $MYSQL_VERSION" + 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_CODENAME + 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-get purge -y "php${CURRENT_PHP//./}"* || true + fi + + # Ensure Sury repo is available + if [[ ! -f /etc/apt/sources.list.d/php.list ]]; then + $STD curl -fsSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb + $STD dpkg -i /tmp/debsuryorg-archive-keyring.deb + echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ ${DISTRO_CODENAME} main" \ + >/etc/apt/sources.list.d/php.list + $STD apt-get 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-get 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-get install -y $MODULE_LIST + msg_ok "Setup 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 + 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 msg_info "Patching $ini" + sed -i "s|^memory_limit = .*|memory_limit = ${PHP_MEMORY_LIMIT}|" "$ini" + sed -i "s|^upload_max_filesize = .*|upload_max_filesize = ${PHP_UPLOAD_MAX_FILESIZE}|" "$ini" + sed -i "s|^post_max_size = .*|post_max_size = ${PHP_POST_MAX_SIZE}|" "$ini" + sed -i "s|^max_execution_time = .*|max_execution_time = ${PHP_MAX_EXECUTION_TIME}|" "$ini" + $STD msg_ok "Patched $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}" + $STD systemctl 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 + $STD systemctl restart php${PHP_VERSION}-fpm + else + msg_warn "FPM requested but service php${PHP_VERSION}-fpm not found" + fi + fi +} + +# ------------------------------------------------------------------------------ +# Installs or updates Composer globally. +# +# Description: +# - Downloads latest version from getcomposer.org +# - Installs to /usr/local/bin/composer +# ------------------------------------------------------------------------------ + +function setup_composer() { + local COMPOSER_BIN="/usr/local/bin/composer" + export COMPOSER_ALLOW_SUPERUSER=1 + + # Check if composer is already installed + if [[ -x "$COMPOSER_BIN" ]]; then + local CURRENT_VERSION + CURRENT_VERSION=$("$COMPOSER_BIN" --version | awk '{print $3}') + $STD msg_info "Old Composer $CURRENT_VERSION found, updating to latest" + else + msg_info "Setup Composer" + fi + + # Download and install latest composer + curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php + php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer >/dev/null 2>&1 + + if [[ $? -ne 0 ]]; then + msg_error "Failed to install Composer" + return 1 + fi + + chmod +x "$COMPOSER_BIN" + $STD composer diagnose + msg_ok "Setup Composer" +} + +# ------------------------------------------------------------------------------ +# 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" + + 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 + return 0 + else + $STD msg_info "Old Go Installation ($CURRENT_VERSION) found, upgrading to $GO_VERSION" + rm -rf "$GO_INSTALL_DIR" + fi + else + msg_info "Setup 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" + return 1 + } + + 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" + + msg_ok "Setup Go $GO_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_CODENAME + DISTRO_CODENAME=$(awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release) + local DESIRED_PACKAGE="temurin-${JAVA_VERSION}-jdk" + + # Add Adoptium repo if missing + if [[ ! -f /etc/apt/sources.list.d/adoptium.list ]]; then + $STD msg_info "Setting up Adoptium Repository" + mkdir -p /etc/apt/keyrings + curl -fsSL "https://packages.adoptium.net/artifactory/api/gpg/key/public" | gpg --dearmor -o /etc/apt/trusted.gpg.d/adoptium.gpg + echo "deb [signed-by=/etc/apt/trusted.gpg.d/adoptium.gpg] https://packages.adoptium.net/artifactory/deb ${DISTRO_CODENAME} main" \ + >/etc/apt/sources.list.d/adoptium.list + $STD apt-get update + $STD msg_ok "Set up Adoptium Repository" + fi + + # Detect currently installed temurin version + 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 + $STD msg_info "Upgrading Temurin JDK $JAVA_VERSION" + $STD apt-get update + $STD apt-get install --only-upgrade -y "$DESIRED_PACKAGE" + $STD msg_ok "Upgraded Temurin JDK $JAVA_VERSION" + else + if [[ -n "$INSTALLED_VERSION" ]]; then + $STD msg_info "Removing Temurin JDK $INSTALLED_VERSION" + $STD apt-get purge -y "temurin-${INSTALLED_VERSION}-jdk" + fi + + msg_info "Setup Temurin JDK $JAVA_VERSION" + $STD apt-get install -y "$DESIRED_PACKAGE" + msg_ok "Setup Temurin JDK $JAVA_VERSION" + fi +} + +# ------------------------------------------------------------------------------ +# 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 MONGO_BASE_URL + 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 REPO_LIST="/etc/apt/sources.list.d/mongodb-org-${MONGO_VERSION}.list" + + local INSTALLED_VERSION="" + if command -v mongod >/dev/null; then + INSTALLED_VERSION=$(mongod --version | awk '/db version/{print $3}' | cut -d. -f1,2) + fi + + if [[ "$INSTALLED_VERSION" == "$MONGO_VERSION" ]]; then + $STD msg_info "Upgrading MongoDB $MONGO_VERSION" + $STD apt-get update + $STD apt-get install --only-upgrade -y mongodb-org + $STD msg_ok "Upgraded MongoDB $MONGO_VERSION" + return 0 + fi + + if [[ -n "$INSTALLED_VERSION" ]]; then + $STD systemctl stop mongod || true + $STD apt-get purge -y mongodb-org || true + rm -f /etc/apt/sources.list.d/mongodb-org-*.list + rm -f /etc/apt/trusted.gpg.d/mongodb-*.gpg + else + msg_info "Setup MongoDB $MONGO_VERSION" + fi + + curl -fsSL "https://pgp.mongodb.com/server-${MONGO_VERSION}.asc" | gpg --dearmor -o "/etc/apt/trusted.gpg.d/mongodb-${MONGO_VERSION}.gpg" + echo "deb [signed-by=/etc/apt/trusted.gpg.d/mongodb-${MONGO_VERSION}.gpg] ${MONGO_BASE_URL} ${DISTRO_CODENAME}/mongodb-org/${MONGO_VERSION} main" \ + >"$REPO_LIST" + + $STD apt-get update || { + msg_error "APT update failed — invalid MongoDB repo for ${DISTRO_ID}-${DISTRO_CODENAME}?" + return 1 + } + + $STD apt-get install -y mongodb-org + + mkdir -p /var/lib/mongodb + chown -R mongodb:mongodb /var/lib/mongodb + + $STD systemctl enable mongod + $STD systemctl start mongod + msg_ok "Setup MongoDB $MONGO_VERSION" +} + +# ------------------------------------------------------------------------------ +# 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") + + if ! command -v jq &>/dev/null; then + $STD apt-get install -y jq &>/dev/null + fi + + 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-get 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 + if ! command -v unzip &>/dev/null; then + $STD apt-get install -y unzip + fi + 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" +} + +# ------------------------------------------------------------------------------ +# 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-get update + $STD apt-get 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 +} + +# ------------------------------------------------------------------------------ +# 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 +} + +# ------------------------------------------------------------------------------ +# 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 + + if ! command -v pv &>/dev/null; then + $STD apt-get install -y pv + fi + 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 +} + +# ------------------------------------------------------------------------------ +# 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 + TMP_DIR=$(mktemp -d) + + # Determine system architecture + local ARCH + 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 + + # Get latest version from GitHub + local LATEST_VERSION + LATEST_VERSION=$(curl -fsSL https://api.github.com/repos/astral-sh/uv/releases/latest | + grep '"tag_name":' | cut -d '"' -f4 | sed 's/^v//') + + if [[ -z "$LATEST_VERSION" ]]; then + msg_error "Could not fetch latest uv version from GitHub." + rm -rf "$TMP_DIR" + return 1 + fi + + # Check if uv is already up to date + if [[ -x "$UV_BIN" ]]; then + local INSTALLED_VERSION + INSTALLED_VERSION=$($UV_BIN -V | awk '{print $2}') + if [[ "$INSTALLED_VERSION" == "$LATEST_VERSION" ]]; then + rm -rf "$TMP_DIR" + [[ ":$PATH:" != *":/usr/local/bin:"* ]] && export PATH="/usr/local/bin:$PATH" + return 0 + else + msg_info "Updating uv from $INSTALLED_VERSION to $LATEST_VERSION" + fi + else + msg_info "Setup uv $LATEST_VERSION" + fi + + # Download and install manually + local UV_URL="https://github.com/astral-sh/uv/releases/latest/download/${UV_TAR}" + if ! curl -fsSL "$UV_URL" -o "$TMP_DIR/uv.tar.gz"; then + msg_error "Failed to download $UV_URL" + rm -rf "$TMP_DIR" + return 1 + fi + + if ! tar -xzf "$TMP_DIR/uv.tar.gz" -C "$TMP_DIR"; then + msg_error "Failed to extract uv archive" + rm -rf "$TMP_DIR" + return 1 + fi + + 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 + $STD uv python update-shell || { + msg_error "Failed to update uv shell integration" + return 1 + } + msg_ok "Setup uv $LATEST_VERSION" + + # Optional: install specific Python 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 via uv" + return 1 + fi + + if ! uv python list | grep -q "cpython-${VERSION_MATCH}-linux.*uv/python"; then + if ! $STD uv python install "$VERSION_MATCH"; then + msg_error "Failed to install Python $VERSION_MATCH via uv" + return 1 + fi + msg_ok "Setup Python $VERSION_MATCH via uv" + 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 +} + +# ------------------------------------------------------------------------------ +# Installs or updates Ghostscript (gs) from source. +# +# Description: +# - Fetches latest release +# - Builds and installs system-wide +# ------------------------------------------------------------------------------ + +function setup_gs() { + mkdir -p /tmp + TMP_DIR=$(mktemp -d) + CURRENT_VERSION=$(gs --version 2>/dev/null || echo "0") + + RELEASE_JSON=$(curl -fsSL https://api.github.com/repos/ArtifexSoftware/ghostpdl-downloads/releases/latest) + LATEST_VERSION=$(echo "$RELEASE_JSON" | grep '"tag_name":' | head -n1 | cut -d '"' -f4 | sed 's/^gs//') + LATEST_VERSION_DOTTED=$(echo "$RELEASE_JSON" | grep '"name":' | head -n1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+') + + if [[ -z "$LATEST_VERSION" ]]; then + msg_error "Could not determine latest Ghostscript version from GitHub." + rm -rf "$TMP_DIR" + return + fi + + if dpkg --compare-versions "$CURRENT_VERSION" ge "$LATEST_VERSION_DOTTED"; then + rm -rf "$TMP_DIR" + return + fi + + msg_info "Setup Ghostscript $LATEST_VERSION_DOTTED (Patience)" + 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" + + if ! tar -xzf "$TMP_DIR/ghostscript.tar.gz" -C "$TMP_DIR"; then + msg_error "Failed to extract Ghostscript archive." + rm -rf "$TMP_DIR" + return + fi + + cd "$TMP_DIR/ghostscript-${LATEST_VERSION_DOTTED}" || { + msg_error "Failed to enter Ghostscript source directory." + rm -rf "$TMP_DIR" + } + $STD apt-get install -y build-essential libpng-dev zlib1g-dev + $STD ./configure >/dev/null + $STD make + $STD sudo make install + local EXIT_CODE=$? + 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" + + if [[ $EXIT_CODE -eq 0 ]]; then + msg_ok "Setup Ghostscript $LATEST_VERSION_DOTTED" + else + msg_error "Ghostscript installation failed" + 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 + TMP_DIR=$(mktemp -d) + + msg_info "Setup Ruby $RUBY_VERSION" + + local RBENV_RELEASE + RBENV_RELEASE=$(curl -fsSL https://api.github.com/repos/rbenv/rbenv/releases/latest | grep '"tag_name":' | cut -d '"' -f4 | 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" + tar -xzf "$TMP_DIR/rbenv.tar.gz" -C "$TMP_DIR" + 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 | grep '"tag_name":' | cut -d '"' -f4 | 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" + tar -xzf "$TMP_DIR/ruby-build.tar.gz" -C "$TMP_DIR" + 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 + msg_info "Setup Rails via gem" + gem install rails + msg_ok "Setup Rails $(rails -v)" + fi + + rm -rf "$TMP_DIR" + msg_ok "Setup Ruby $RUBY_VERSION" +} + +# ------------------------------------------------------------------------------ +# 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) +# ------------------------------------------------------------------------------ +function create_selfsigned_certs() { + local app=${APP:-$(echo "${APPLICATION,,}" | tr -d ' ')} + $STD openssl req -x509 -nodes -days 365 -newkey rsa:4096 \ + -keyout /etc/ssl/private/"$app"-selfsigned.key \ + -out /etc/ssl/certs/"$app"-selfsigned.crt \ + -subj "/C=US/O=$app/OU=Domain Control Validated/CN=localhost" +} + +# ------------------------------------------------------------------------------ +# 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" + + # rustup & toolchain + if ! command -v rustup &>/dev/null; then + msg_info "Setup Rust" + curl -fsSL https://sh.rustup.rs | $STD sh -s -- -y --default-toolchain "$RUST_TOOLCHAIN" + export PATH="$CARGO_BIN:$PATH" + echo 'export PATH="$HOME/.cargo/bin:$PATH"' >>"$HOME/.profile" + msg_ok "Setup Rust" + else + $STD rustup install "$RUST_TOOLCHAIN" + $STD rustup default "$RUST_TOOLCHAIN" + $STD rustup update "$RUST_TOOLCHAIN" + msg_ok "Rust toolchain set to $RUST_TOOLCHAIN" + fi + + # install/update crates + 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 "Update $NAME: $INSTALLED_VER → $VER" + $STD cargo install "$NAME" --version "$VER" --force + msg_ok "Updated $NAME to $VER" + elif [[ -z "$VER" ]]; then + msg_info "Update $NAME: $INSTALLED_VER → latest" + $STD cargo install "$NAME" --force + msg_ok "Updated $NAME to latest" + fi + else + msg_info "Setup $NAME ${VER:+($VER)}" + $STD cargo install "$NAME" ${VER:+--version "$VER"} + msg_ok "Setup $NAME ${VER:-latest}" + fi + done + msg_ok "Setup Rust" + fi +} + +# ------------------------------------------------------------------------------ +# 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() { + if grep -qi alpine /etc/os-release; then + msg_info "Setup Adminer (Alpine)" + mkdir -p /var/www/localhost/htdocs/adminer + if ! curl -fsSL https://github.com/vrana/adminer/releases/latest/download/adminer.php \ + -o /var/www/localhost/htdocs/adminer/index.php; then + msg_error "Failed to download Adminer" + return 1 + fi + msg_ok "Adminer available at /adminer (Alpine)" + else + msg_info "Setup Adminer (Debian/Ubuntu)" + $STD apt-get install -y adminer + $STD a2enconf adminer + $STD systemctl reload apache2 + msg_ok "Adminer available at /adminer (Debian/Ubuntu)" + 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 + TMP_DIR=$(mktemp -d) + local CURRENT_VERSION="" + local BINARY_PATH="/usr/local/bin/yq" + local GITHUB_REPO="mikefarah/yq" + + if ! command -v jq &>/dev/null; then + $STD apt-get update + $STD apt-get install -y jq || { + msg_error "Failed to install jq" + rm -rf "$TMP_DIR" + return 1 + } + fi + + 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 | awk '{print $NF}' | sed 's/^v//') + fi + fi + + local RELEASE_JSON + RELEASE_JSON=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest") + local LATEST_VERSION + LATEST_VERSION=$(echo "$RELEASE_JSON" | jq -r '.tag_name' | sed 's/^v//') + + if [[ -z "$LATEST_VERSION" ]]; then + msg_error "Could not determine latest yq version from GitHub." + rm -rf "$TMP_DIR" + return 1 + fi + + if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$LATEST_VERSION" ]]; then + return + fi + + msg_info "Setup yq ($LATEST_VERSION)" + curl -fsSL "https://github.com/${GITHUB_REPO}/releases/download/v${LATEST_VERSION}/yq_linux_amd64" -o "$TMP_DIR/yq" + chmod +x "$TMP_DIR/yq" + mv "$TMP_DIR/yq" "$BINARY_PATH" + + if [[ ! -x "$BINARY_PATH" ]]; then + msg_error "Failed to install yq to $BINARY_PATH" + 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}') + if [[ "$FINAL_VERSION" == "v$LATEST_VERSION" ]]; then + msg_ok "Setup yq ($LATEST_VERSION)" + else + msg_error "yq installation incomplete or version mismatch" + fi +} + +# ------------------------------------------------------------------------------ +# 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 + TMP_DIR=$(mktemp -d) + local VERSION="" + local BINARY_PATH="/usr/local/bin/magick" + + if command -v magick &>/dev/null; then + VERSION=$(magick -version | awk '/^Version/ {print $3}') + msg_ok "ImageMagick already installed ($VERSION)" + return 0 + fi + + msg_info "Setup ImageMagick (Patience)" + $STD apt-get update + $STD apt-get install -y \ + 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" + tar -xzf "$TMP_DIR/ImageMagick.tar.gz" -C "$TMP_DIR" + cd "$TMP_DIR"/ImageMagick-* || { + msg_error "Source extraction failed" + rm -rf "$TMP_DIR" + return 1 + } + + ./configure --disable-static >/dev/null + $STD make + $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" + ensure_usr_local_bin_persist + msg_ok "Setup ImageMagick $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 + 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" + + # 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" + 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 + rm -rf "$TMP_DIR" + msg_ok "Installed FFmpeg binary ($($BIN_PATH -version | head -n1))" + return + fi + + if ! command -v jq &>/dev/null; then + $STD apt-get update + $STD apt-get install -y jq + fi + + # Auto-detect latest stable version if none specified + if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then + msg_info "Resolving latest FFmpeg tag" + 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 + ) + ;; + *) + msg_error "Invalid FFMPEG_TYPE: $TYPE" + rm -rf "$TMP_DIR" + return 1 + ;; + esac + + $STD apt-get update + $STD apt-get install -y "${DEPS[@]}" + + curl -fsSL "https://github.com/${GITHUB_REPO}/archive/refs/tags/${VERSION}.tar.gz" -o "$TMP_DIR/ffmpeg.tar.gz" + tar -xzf "$TMP_DIR/ffmpeg.tar.gz" -C "$TMP_DIR" + 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) + fi + + if [[ ${#args[@]} -eq 0 ]]; then + msg_error "FFmpeg configure args array is empty – aborting." + rm -rf "$TMP_DIR" + return 1 + fi + + ./configure "${args[@]}" >"$TMP_DIR/configure.log" 2>&1 || { + msg_error "FFmpeg ./configure failed (see $TMP_DIR/configure.log)" + cat "$TMP_DIR/configure.log" | tail -n 20 + rm -rf "$TMP_DIR" + return 1 + } + + $STD make -j"$(nproc)" + $STD make install + echo "/usr/local/lib" >/etc/ld.so.conf.d/ffmpeg.conf + ldconfig + + ldconfig -p | grep libavdevice >/dev/null || { + msg_error "libavdevice not registered with dynamic linker" + 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" + ensure_usr_local_bin_persist + msg_ok "Setup FFmpeg $FINAL_VERSION" +} + +# ------------------------------------------------------------------------------ +# Installs ClickHouse server and client, sets up DB/user with credentials. +# +# Description: +# - Adds official ClickHouse APT repo with GPG key +# - Installs clickhouse-server and clickhouse-client +# - Creates database and user (credentials optionally overrideable via env) +# +# Variables: +# CLICKHOUSE_DB - Database name (default: analytics) +# CLICKHOUSE_USER - Username (default: analytics_user) +# CLICKHOUSE_PASS - Password (default: auto-generated) +# ------------------------------------------------------------------------------ + +function setup_clickhouse() { + local CLICKHOUSE_DB="${CLICKHOUSE_DB:-analytics}" + local CLICKHOUSE_USER="${CLICKHOUSE_USER:-analytics_user}" + local CLICKHOUSE_PASS="${CLICKHOUSE_PASS:-$(openssl rand -base64 18 | cut -c1-13)}" + local GPG_URL="https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key" + local GPG_KEY_PATH="/usr/share/keyrings/clickhouse-keyring.gpg" + local ARCH + ARCH=$(dpkg --print-architecture) + + if ! command -v clickhouse >/dev/null; then + msg_info "Setup ClickHouse" + + if ! curl -fsSL --connect-timeout 5 https://packages.clickhouse.com >/dev/null 2>&1; then + msg_error "Connection to packages.clickhouse.com:443 failed – possibly blocked" + echo "💡 Check AdGuard/Pi-hole or firewall rules" + return 1 + fi + + if ! curl -fsSL --retry 3 --connect-timeout 10 "$GPG_URL" | + gpg --dearmor -o "$GPG_KEY_PATH"; then + msg_error "Failed to fetch ClickHouse GPG key" + return 1 + fi + + echo "deb [signed-by=$GPG_KEY_PATH arch=$ARCH] https://packages.clickhouse.com/deb stable main" \ + >/etc/apt/sources.list.d/clickhouse.list + + env -u CLICKHOUSE_USER $STD apt-get update + env -u CLICKHOUSE_USER DEBIAN_FRONTEND=noninteractive $STD apt-get install -y clickhouse-server clickhouse-client + + $STD systemctl enable --now clickhouse-server + + msg_info "Waiting for ClickHouse to be ready" + for i in {1..10}; do + if clickhouse client --query "SELECT 1" &>/dev/null; then break; fi + sleep 1 + done + + # User anlegen + clickhouse client --query "CREATE DATABASE IF NOT EXISTS $CLICKHOUSE_DB" + clickhouse client --query "CREATE USER IF NOT EXISTS $CLICKHOUSE_USER IDENTIFIED WITH plaintext_password BY '$CLICKHOUSE_PASS'" + clickhouse client --query "GRANT ALL ON $CLICKHOUSE_DB.* TO $CLICKHOUSE_USER" + + # Default-User ggf. deaktivieren + cat </etc/clickhouse-server/users.d/disable-default.xml + + + + + +EOF + systemctl restart clickhouse-server + + msg_ok "Setup ClickHouse (DB: $CLICKHOUSE_DB, User: $CLICKHOUSE_USER)" + + { + echo "ClickHouse DB: $CLICKHOUSE_DB" + echo "ClickHouse User: $CLICKHOUSE_USER" + echo "ClickHouse Pass: $CLICKHOUSE_PASS" + } >>~/clickhouse.creds + else + msg_info "Updating ClickHouse packages" + env -u CLICKHOUSE_USER $STD apt-get update + env -u CLICKHOUSE_USER $STD apt-get install -y --only-upgrade clickhouse-server clickhouse-client + msg_ok "ClickHouse updated" + fi +} + +# ------------------------------------------------------------------------------ +# 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: +# check_for_gh_release "AppName" "user/repo" +# if [[ $? -eq 0 ]]; then +# echo "New version available: $CHECK_UPDATE_RELEASE" +# # trigger update... +# fi +# +# 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="${3:-}" # optional + local current_file="$HOME/.${app,,}" + + msg_info "Check for update: ${app}" + + # DNS check for GitHub + if ! getent hosts api.github.com >/dev/null 2>&1; then + msg_error "Network error: cannot resolve api.github.com" + return 1 + fi + + # jq check + if ! command -v jq &>/dev/null; then + apt-get update -qq &>/dev/null && apt-get install -y jq &>/dev/null || { + msg_error "Failed to install jq" + return 1 + } + fi + + # get latest release + local release + release=$(curl -fsSL "https://api.github.com/repos/${source}/releases/latest" | + jq -r '.tag_name' | sed 's/^v//') + + if [[ -z "$release" ]]; then + msg_error "Unable to determine latest release for ${app}" + return 1 + fi + + local current="" + [[ -f "$current_file" ]] && current=$(<"$current_file") + + # PINNED Releases + if [[ -n "$pinned_version" ]]; then + if [[ "$pinned_version" == "$release" ]]; then + msg_ok "${app} pinned to v${pinned_version} (no update needed)" + return 1 + else + if [[ "$current" == "$pinned_version" ]]; then + msg_ok "${app} pinned to v${pinned_version} (already installed, upstream v${release})" + return 1 + fi + msg_info "${app} pinned to v${pinned_version} (upstream v${release}) → update/downgrade required" + CHECK_UPDATE_RELEASE="$pinned_version" + return 0 + fi + fi + + if [[ "$release" != "$current" ]] || [[ ! -f "$current_file" ]]; then + CHECK_UPDATE_RELEASE="$release" + msg_info "New release available: v${release} (current: v${current:-none})" + return 0 + else + msg_ok "${app} is up to date (v${release})" + return 1 + fi +} + +# ------------------------------------------------------------------------------ +# Hardware acceleration setup inside container +# Works with: Debian 12 (bookworm), Debian 13 (trixie), Ubuntu 24.04 (noble) +# Usage: hwaccel_setup_in_ct [--nonfree-intel] +# ------------------------------------------------------------------------------ + +hwaccel_setup_in_ct() { + local CTTYPE="$1" NONFREE=0 + [[ "$2" == "--nonfree-intel" ]] && NONFREE=1 + + # Detect OS info inside the container + local ID VERSION_CODENAME + if [[ -r /etc/os-release ]]; then + . /etc/os-release + fi + ID="${ID:-debian}" + VERSION_CODENAME="${VERSION_CODENAME:-bookworm}" + + msg_info "Setting up hardware acceleration for ${ID^} ($VERSION_CODENAME)" + + case "$ID" in + debian | ubuntu) + if ((NONFREE)) && [[ "$VERSION_CODENAME" =~ (trixie|noble) ]]; then + # Debian 13 / Ubuntu 24.04 → non-free Intel driver + cat >/etc/apt/sources.list.d/non-free.sources <<'SRC' +Types: deb deb-src +URIs: http://deb.debian.org/debian +Suites: trixie +Components: non-free non-free-firmware + +Types: deb deb-src +URIs: http://deb.debian.org/debian-security +Suites: trixie-security +Components: non-free non-free-firmware + +Types: deb deb-src +URIs: http://deb.debian.org/debian +Suites: trixie-updates +Components: non-free non-free-firmware +SRC + + $STD apt-get update + $STD apt-get install -y \ + intel-media-va-driver-non-free \ + ocl-icd-libopencl1 \ + mesa-opencl-icd \ + mesa-va-drivers \ + libvpl2 \ + vainfo \ + intel-gpu-tools + else + # Debian 12 (bookworm) and fallback for Debian 13/Ubuntu 24.04 without non-free + $STD apt-get update + $STD apt-get install -y \ + va-driver-all \ + ocl-icd-libopencl1 \ + mesa-opencl-icd \ + mesa-va-drivers \ + vainfo \ + intel-gpu-tools + fi + ;; + *) + msg_warn "Unsupported distribution ($ID $VERSION_CODENAME) – skipping HW accel setup" + return 0 + ;; + esac + + # Add current user to video/render groups (only for privileged CTs) + if [[ "$CTTYPE" == "0" ]]; then + $STD adduser "$(id -un)" video || true + $STD adduser "$(id -un)" render || true + fi + + msg_ok "Hardware acceleration is ready" +}