#!/bin/bash # NUPST Installer Script (v6.0+) # Downloads and installs the pre-compiled NUPST binary from Gitea release assets. # Every file this script needs is a release asset; repository source access is not used. # # Usage: # Direct piped installation (recommended): # curl -sSL https://code.foss.global/serve.zone/nupst/releases/download/latest/install.sh | sudo bash # # With version specification: # curl -sSL https://code.foss.global/serve.zone/nupst/releases/download/latest/install.sh | sudo bash -s -- --version v6.0.0 # # Options: # -h, --help Show this help message # --version VERSION Install specific version (e.g., v6.0.0) # --install-dir DIR Installation directory (default: /opt/nupst) # --dry-run Resolve the version, print asset URLs, check availability, change nothing set -e # Default values SHOW_HELP=0 DRY_RUN=0 SPECIFIED_VERSION="" INSTALL_DIR="/opt/nupst" GITEA_BASE_URL="https://code.foss.global" GITEA_REPO="serve.zone/nupst" CHECKSUMS_ASSET="SHA256SUMS.txt" UNINSTALL_ASSET="uninstall.sh" # Every transfer is an HTTPS release asset or the public releases API: refuse other # protocols and bound redirect following. CURL_DOWNLOAD_OPTS=(--proto '=https' --max-redirs 5 -fsSL) CURL_PROBE_OPTS=(--proto '=https' --max-redirs 5 -sSIL) # Shape of a NUPST release tag, kept in sync with RELEASE_TAG_REGEX in ts/release-assets.ts RELEASE_TAG_REGEX='^v?[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$' # Parse command line arguments while [[ $# -gt 0 ]]; do case $1 in -h|--help) SHOW_HELP=1 shift ;; --version) SPECIFIED_VERSION="$2" shift 2 ;; --install-dir) INSTALL_DIR="$2" shift 2 ;; --dry-run) DRY_RUN=1 shift ;; *) echo "Unknown option: $1" echo "Use -h or --help for usage information" exit 1 ;; esac done if [ $SHOW_HELP -eq 1 ]; then echo "NUPST Installer Script (v6.0+)" echo "Downloads and installs the pre-compiled NUPST binary from Gitea release assets" echo "" echo "Usage: $0 [options]" echo "" echo "Options:" echo " -h, --help Show this help message" echo " --version VERSION Install specific version (e.g., v6.0.0)" echo " --install-dir DIR Installation directory (default: /opt/nupst)" echo " --dry-run Resolve version and asset URLs without installing" echo "" echo "Examples:" echo " # Install latest version" echo " curl -sSL ${GITEA_BASE_URL}/${GITEA_REPO}/releases/download/latest/install.sh | sudo bash" echo "" echo " # Install specific version" echo " curl -sSL ${GITEA_BASE_URL}/${GITEA_REPO}/releases/download/latest/install.sh | sudo bash -s -- --version v6.0.0" echo "" echo " # Resolve the latest version and print the asset URLs only" echo " curl -sSL ${GITEA_BASE_URL}/${GITEA_REPO}/releases/download/latest/install.sh | bash -s -- --dry-run" exit 0 fi # Check if running as root (a dry run never touches the system) if [ $DRY_RUN -eq 0 ] && [ "$EUID" -ne 0 ]; then echo "Please run as root (sudo bash install.sh or pipe to sudo bash)" exit 1 fi # Helper function to detect OS and architecture detect_platform() { local os local arch os=$(uname -s) arch=$(uname -m) # Map OS case "$os" in Linux) os_name="linux" ;; Darwin) os_name="macos" ;; MINGW*|MSYS*|CYGWIN*) os_name="windows" ;; *) echo "Error: Unsupported operating system: $os" >&2 echo "Supported: Linux, macOS, Windows" >&2 exit 1 ;; esac # Map architecture case "$arch" in x86_64|amd64) arch_name="x64" ;; aarch64|arm64) arch_name="arm64" ;; *) echo "Error: Unsupported architecture: $arch" >&2 echo "Supported: x86_64/amd64 (x64), aarch64/arm64 (arm64)" >&2 exit 1 ;; esac # Construct binary name if [ "$os_name" = "windows" ]; then echo "nupst-${os_name}-${arch_name}.exe" else echo "nupst-${os_name}-${arch_name}" fi } # Validate and normalize a release tag before it is used in a URL # Mirrors normalizeReleaseTag() in ts/release-assets.ts normalize_release_tag() { local candidate="$1" local source_label="$2" # Trim surrounding whitespace candidate="${candidate#"${candidate%%[![:space:]]*}"}" candidate="${candidate%"${candidate##*[![:space:]]}"}" if [ "$candidate" = "latest" ]; then echo "latest" return 0 fi if [ -z "$candidate" ]; then echo "Error: $source_label is empty" >&2 exit 1 fi if ! [[ "$candidate" =~ $RELEASE_TAG_REGEX ]]; then echo "Error: $source_label is not a NUPST release tag: $candidate" >&2 echo "Expected a tag like v6.0.0, 6.0.0 or the latest alias" >&2 exit 1 fi if [[ "$candidate" == v* ]]; then echo "$candidate" else echo "v$candidate" fi } # Build the public download URL of a release asset asset_url() { local version="$1" local asset_name="$2" echo "${GITEA_BASE_URL}/${GITEA_REPO}/releases/download/${version}/${asset_name}" } # Get latest release version from the public Gitea releases API (no jq required) get_latest_version() { echo "Fetching latest release version from Gitea..." >&2 local api_url="${GITEA_BASE_URL}/api/v1/repos/${GITEA_REPO}/releases/latest" local response if ! response=$(curl "${CURL_DOWNLOAD_OPTS[@]}" "$api_url"); then echo "Error: Failed to fetch latest release information from Gitea API" >&2 echo "URL: $api_url" >&2 exit 1 fi if [ -z "$response" ]; then echo "Error: Empty response from Gitea API" >&2 echo "URL: $api_url" >&2 exit 1 fi # Extract tag_name from the JSON response using only grep/sed local version version=$(printf '%s' "$response" \ | tr ',{}' '\n\n\n' \ | grep -m1 '"tag_name"' \ | sed -e 's/.*"tag_name"[[:space:]]*:[[:space:]]*"//' -e 's/".*//') if [ -z "$version" ]; then echo "Error: Could not determine latest version from API response" >&2 exit 1 fi # Never build a URL from an unvalidated remote value local normalized if ! normalized=$(normalize_release_tag "$version" "tag_name from the Gitea releases API"); then exit 1 fi echo "$normalized" } # Compute the SHA256 checksum of a file on Linux and macOS sha256_of_file() { local file="$1" if command -v sha256sum >/dev/null 2>&1; then sha256sum "$file" | cut -d' ' -f1 elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$file" | cut -d' ' -f1 else echo "" fi } # Verify a downloaded file against the SHA256SUMS.txt release asset verify_checksum() { local file="$1" local asset_name="$2" local checksums_file="$3" local expected expected=$(awk -v name="$asset_name" '$2 == name || $2 == "*" name { print $1 }' "$checksums_file" | head -n1) if [ -z "$expected" ]; then echo "Error: $asset_name is not listed in $CHECKSUMS_ASSET" return 1 fi local actual actual=$(sha256_of_file "$file") if [ -z "$actual" ]; then echo "Error: Neither sha256sum nor shasum is available, cannot verify $asset_name" return 1 fi if [ "$expected" != "$actual" ]; then echo "Error: Checksum mismatch for $asset_name" echo " expected: $expected" echo " actual: $actual" return 1 fi echo "Checksum verified for $asset_name" return 0 } # Main installation process echo "================================================" echo " NUPST Installation Script (v6.0+)" echo "================================================" echo "" # Detect platform BINARY_NAME=$(detect_platform) echo "Detected platform: $BINARY_NAME" echo "" # Determine version to install if [ -n "$SPECIFIED_VERSION" ]; then if ! VERSION=$(normalize_release_tag "$SPECIFIED_VERSION" "--version argument"); then exit 1 fi echo "Installing specified version: $VERSION" else if ! VERSION=$(get_latest_version); then exit 1 fi echo "Installing latest version: $VERSION" fi echo "" # Construct download URLs (release assets only) DOWNLOAD_URL=$(asset_url "$VERSION" "$BINARY_NAME") CHECKSUMS_URL=$(asset_url "$VERSION" "$CHECKSUMS_ASSET") UNINSTALL_URL=$(asset_url "$VERSION" "$UNINSTALL_ASSET") echo "Binary URL: $DOWNLOAD_URL" echo "Checksums URL: $CHECKSUMS_URL" echo "Uninstall URL: $UNINSTALL_URL" echo "" if [ $DRY_RUN -eq 1 ]; then echo "Dry run: checking asset availability (nothing will be installed)" echo "" DRY_RUN_FAILED=0 for url in "$DOWNLOAD_URL" "$CHECKSUMS_URL" "$UNINSTALL_URL"; do status=$(curl "${CURL_PROBE_OPTS[@]}" -o /dev/null -w '%{http_code}' "$url" || echo "000") echo " $status $url" if [ "$status" != "200" ]; then DRY_RUN_FAILED=1 fi done echo "" if [ $DRY_RUN_FAILED -ne 0 ]; then echo "Dry run finished: at least one asset is not publicly available" exit 1 fi echo "Dry run finished: all required assets are publicly available" exit 0 fi # Download everything into a temporary directory first, so a failed download # never leaves a broken installation behind DOWNLOAD_DIR=$(mktemp -d) cleanup_downloads() { rm -rf "$DOWNLOAD_DIR" } trap cleanup_downloads EXIT INT TERM echo "Downloading checksums..." if ! curl "${CURL_DOWNLOAD_OPTS[@]}" "$CHECKSUMS_URL" -o "$DOWNLOAD_DIR/$CHECKSUMS_ASSET"; then echo "Error: Failed to download $CHECKSUMS_ASSET from $CHECKSUMS_URL" echo "" echo "Please check:" echo " 1. Your internet connection" echo " 2. The specified version exists: ${GITEA_BASE_URL}/${GITEA_REPO}/releases" exit 1 fi echo "Downloading NUPST binary..." if ! curl "${CURL_DOWNLOAD_OPTS[@]}" "$DOWNLOAD_URL" -o "$DOWNLOAD_DIR/$BINARY_NAME"; then echo "Error: Failed to download binary from $DOWNLOAD_URL" echo "" echo "Please check:" echo " 1. Your internet connection" echo " 2. The specified version exists: ${GITEA_BASE_URL}/${GITEA_REPO}/releases" echo " 3. The platform binary is available for this release" exit 1 fi if [ ! -s "$DOWNLOAD_DIR/$BINARY_NAME" ]; then echo "Error: Downloaded binary is empty" exit 1 fi echo "Downloading uninstaller..." if ! curl "${CURL_DOWNLOAD_OPTS[@]}" "$UNINSTALL_URL" -o "$DOWNLOAD_DIR/$UNINSTALL_ASSET"; then echo "Error: Failed to download $UNINSTALL_ASSET from $UNINSTALL_URL" echo "" echo "The uninstaller is required so that 'nupst uninstall' keeps working." exit 1 fi # Verify both downloads against the checksums asset if ! verify_checksum "$DOWNLOAD_DIR/$BINARY_NAME" "$BINARY_NAME" "$DOWNLOAD_DIR/$CHECKSUMS_ASSET"; then exit 1 fi if ! verify_checksum "$DOWNLOAD_DIR/$UNINSTALL_ASSET" "$UNINSTALL_ASSET" "$DOWNLOAD_DIR/$CHECKSUMS_ASSET"; then exit 1 fi echo "" # Everything is downloaded and verified: only now is the running installation touched. # A failed download or checksum above leaves the current service and binary untouched. SERVICE_WAS_RUNNING=0 if systemctl is-enabled --quiet nupst 2>/dev/null || systemctl is-active --quiet nupst 2>/dev/null; then SERVICE_WAS_RUNNING=1 if systemctl is-active --quiet nupst 2>/dev/null; then echo "Stopping NUPST service..." systemctl stop nupst fi fi # Clean installation directory - ensure only the released files exist if [ -d "$INSTALL_DIR" ]; then echo "Cleaning installation directory: $INSTALL_DIR" rm -rf "$INSTALL_DIR" fi # Create fresh installation directory echo "Creating installation directory: $INSTALL_DIR" mkdir -p "$INSTALL_DIR" # Move the verified files into place BINARY_PATH="$INSTALL_DIR/nupst" if ! mv "$DOWNLOAD_DIR/$BINARY_NAME" "$BINARY_PATH"; then echo "Error: Failed to move binary to $BINARY_PATH" exit 1 fi UNINSTALL_PATH="$INSTALL_DIR/$UNINSTALL_ASSET" if ! mv "$DOWNLOAD_DIR/$UNINSTALL_ASSET" "$UNINSTALL_PATH"; then echo "Error: Failed to move uninstaller to $UNINSTALL_PATH" exit 1 fi # Keep the verified checksums next to the installation for later inspection mv "$DOWNLOAD_DIR/$CHECKSUMS_ASSET" "$INSTALL_DIR/$CHECKSUMS_ASSET" # Make executable if ! chmod +x "$BINARY_PATH"; then echo "Error: Failed to make binary executable" exit 1 fi chmod +x "$UNINSTALL_PATH" echo "Binary installed successfully to: $BINARY_PATH" echo "Uninstaller installed to: $UNINSTALL_PATH" echo "" # Check if /usr/local/bin is in PATH if [[ ":$PATH:" == *":/usr/local/bin:"* ]]; then BIN_DIR="/usr/local/bin" else BIN_DIR="/usr/bin" fi # Create symlink for global access ln -sf "$BINARY_PATH" "$BIN_DIR/nupst" echo "Symlink created: $BIN_DIR/nupst -> $BINARY_PATH" echo "" # Restart service if it was running before update if [ $SERVICE_WAS_RUNNING -eq 1 ]; then echo "Restarting NUPST service..." systemctl restart nupst echo "Service restarted successfully." echo "" fi echo "================================================" echo " NUPST Installation Complete!" echo "================================================" echo "" echo "Installation details:" echo " Binary location: $BINARY_PATH" echo " Symlink location: $BIN_DIR/nupst" echo " Uninstaller: $UNINSTALL_PATH" echo " Version: $VERSION" echo "" # Check if configuration exists if [ -f "/etc/nupst/config.json" ]; then echo "Configuration: /etc/nupst/config.json (preserved)" echo "" echo "Your existing configuration has been preserved." if [ $SERVICE_WAS_RUNNING -eq 1 ]; then echo "The service has been restarted with your current settings." else echo "Start the service with: sudo nupst service start" fi else echo "Get started:" echo " nupst --version" echo " nupst help" echo " nupst ups add # Add a UPS device" echo " nupst service enable # Enable systemd service" fi echo ""