From bfb40f49475ca6e46ade4c6b1b3766a9ec070dde Mon Sep 17 00:00:00 2001 From: CN-JS-HuiBai Date: Thu, 9 Apr 2026 14:02:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AE=89=E8=A3=85=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E7=9A=84=E6=BD=9C=E5=9C=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- install.sh | 333 ++++++++++++++++++++++++++++++++--------------------- update.sh | 259 +++++++++++++++++++++++++---------------- 2 files changed, 356 insertions(+), 236 deletions(-) diff --git a/install.sh b/install.sh index d8d2204..fa55a6d 100644 --- a/install.sh +++ b/install.sh @@ -1,155 +1,196 @@ #!/bin/bash -# PromdataPanel - Multi-Prometheus Monitoring Dashboard Installer -# This script handles OS detection, Node.js installation, project setup, and Systemd configuration. +set -euo pipefail -# Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' -NC='\033[0m' # No Color +NC='\033[0m' -# 0. Configuration VERSION=${VERSION:-"v0.1.0"} DOWNLOAD_URL="https://git.littlediary.cn/CN-JS-HuiBai/PromdataPanel/archive/${VERSION}.zip" MIN_NODE_VERSION=18 +SERVICE_NAME="promdatapanel" +SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" + +OS_ID="" +OS_VER="" +PROJECT_DIR="" +REAL_USER="" echo -e "${BLUE}================================================${NC}" echo -e "${BLUE} PromdataPanel Auto-Installer ${NC}" echo -e "${BLUE} Version: ${VERSION} ${NC}" echo -e "${BLUE}================================================${NC}" -# 1. OS Detection detect_os() { if [ -f /etc/os-release ]; then + # shellcheck disable=SC1091 . /etc/os-release - OS_ID=$ID - OS_VER=$VERSION_ID + OS_ID="${ID:-}" + OS_VER="${VERSION_ID:-}" else echo -e "${RED}Error: Cannot detect operating system type (/etc/os-release missing).${NC}" exit 1 fi - echo -e "Detected OS: ${GREEN}${OS_ID} ${OS_VER}${NC}" -} -# 2. Node.js Installation/Verification -install_node() { - echo -e "${BLUE}Verifying Node.js environment...${NC}" - - NODE_INSTALLED=false - if command -v node &> /dev/null; then - CURRENT_NODE_VER=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) - if [ "$CURRENT_NODE_VER" -ge "$MIN_NODE_VERSION" ]; then - echo -e "${GREEN}Node.js v$(node -v) is already installed.${NC}" - NODE_INSTALLED=true - else - echo -e "${YELLOW}Existing Node.js version (v$(node -v)) is too old (Requires >= $MIN_NODE_VERSION).${NC}" - fi - fi - - if [ "$NODE_INSTALLED" = false ]; then - echo -e "${BLUE}Installing Node.js 20.x...${NC}" - case "$OS_ID" in - ubuntu|debian|raspbian) - sudo apt-get update - sudo apt-get install -y ca-certificates curl gnupg - curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - - sudo apt-get install -y nodejs - ;; - centos|rhel|almalinux|rocky) - curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash - - sudo yum install -y nodejs - ;; - fedora) - curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash - - sudo dnf install -y nodejs - ;; - *) - echo -e "${RED}Unsupported OS for automatic Node.js installation: $OS_ID${NC}" - echo -e "Please install Node.js >= 18 manually.${NC}" - exit 1 - ;; - esac - fi -} - -# 3. Download and Extract (If needed) -if [ ! -f "server/index.js" ]; then - echo -e "${YELLOW}Project files not found. Starting download...${NC}" - - if ! command -v curl &> /dev/null; then - echo -e "${BLUE}Installing curl...${NC}" - [ "$OS_ID" = "ubuntu" ] || [ "$OS_ID" = "debian" ] && sudo apt-get install -y curl - [ "$OS_ID" = "centos" ] || [ "$OS_ID" = "rhel" ] && sudo yum install -y curl - fi - - if ! command -v unzip &> /dev/null; then - echo -e "${BLUE}Installing unzip...${NC}" - [ "$OS_ID" = "ubuntu" ] || [ "$OS_ID" = "debian" ] && sudo apt-get install -y unzip - [ "$OS_ID" = "centos" ] || [ "$OS_ID" = "rhel" ] && sudo yum install -y unzip - fi - - TEMP_ZIP="promdatapanel_${VERSION}.zip" - echo -e "${BLUE}Downloading ${DOWNLOAD_URL}...${NC}" - curl -L "$DOWNLOAD_URL" -o "$TEMP_ZIP" - - if [ $? -ne 0 ]; then - echo -e "${RED}Download failed.${NC}" + if [ -z "$OS_ID" ]; then + echo -e "${RED}Error: Unable to determine operating system ID.${NC}" exit 1 fi + echo -e "Detected OS: ${GREEN}${OS_ID} ${OS_VER}${NC}" +} + +require_cmd() { + local cmd="$1" + local hint="${2:-}" + if ! command -v "$cmd" >/dev/null 2>&1; then + echo -e "${RED}Missing required command: ${cmd}.${NC}" + if [ -n "$hint" ]; then + echo -e "${YELLOW}${hint}${NC}" + fi + exit 1 + fi +} + +install_packages() { + case "$OS_ID" in + ubuntu|debian|raspbian) + sudo apt-get update + sudo apt-get install -y "$@" + ;; + centos|rhel|almalinux|rocky) + sudo yum install -y "$@" + ;; + fedora) + sudo dnf install -y "$@" + ;; + *) + echo -e "${RED}Unsupported OS for automatic package installation: ${OS_ID}${NC}" + echo -e "${YELLOW}Please install the following packages manually: $*${NC}" + exit 1 + ;; + esac +} + +ensure_tooling() { + if ! command -v curl >/dev/null 2>&1; then + echo -e "${BLUE}Installing curl...${NC}" + install_packages curl + fi + + if ! command -v unzip >/dev/null 2>&1; then + echo -e "${BLUE}Installing unzip...${NC}" + install_packages unzip + fi +} + +configure_nodesource_apt_repo() { + sudo install -d -m 0755 /etc/apt/keyrings + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list >/dev/null +} + +install_node() { + echo -e "${BLUE}Verifying Node.js environment...${NC}" + + local node_installed=false + if command -v node >/dev/null 2>&1; then + local current_node_ver + current_node_ver=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) + if [ "$current_node_ver" -ge "$MIN_NODE_VERSION" ]; then + echo -e "${GREEN}Node.js $(node -v) is already installed.${NC}" + node_installed=true + else + echo -e "${YELLOW}Existing Node.js $(node -v) is too old (requires >= ${MIN_NODE_VERSION}).${NC}" + fi + fi + + if [ "$node_installed" = true ]; then + return + fi + + echo -e "${BLUE}Installing Node.js 20.x...${NC}" + case "$OS_ID" in + ubuntu|debian|raspbian) + install_packages ca-certificates curl gnupg + configure_nodesource_apt_repo + sudo apt-get update + sudo apt-get install -y nodejs + ;; + centos|rhel|almalinux|rocky) + install_packages nodejs + ;; + fedora) + install_packages nodejs + ;; + *) + echo -e "${RED}Unsupported OS for automatic Node.js installation: ${OS_ID}${NC}" + echo -e "${YELLOW}Please install Node.js >= ${MIN_NODE_VERSION} manually.${NC}" + exit 1 + ;; + esac + + require_cmd node "Please install Node.js >= ${MIN_NODE_VERSION} manually and rerun the installer." + local installed_major + installed_major=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) + if [ "$installed_major" -lt "$MIN_NODE_VERSION" ]; then + echo -e "${RED}Installed Node.js $(node -v) is still below the required version.${NC}" + echo -e "${YELLOW}Please upgrade Node.js manually to >= ${MIN_NODE_VERSION}.${NC}" + exit 1 + fi +} + +download_project_if_needed() { + if [ -f "server/index.js" ]; then + return + fi + + echo -e "${YELLOW}Project files not found. Starting download...${NC}" + ensure_tooling + + local temp_dir + temp_dir=$(mktemp -d "${TMPDIR:-/tmp}/promdatapanel-install-XXXXXX") + local temp_zip="${temp_dir}/promdatapanel_${VERSION}.zip" + + echo -e "${BLUE}Downloading ${DOWNLOAD_URL}...${NC}" + curl -fL "$DOWNLOAD_URL" -o "$temp_zip" + echo -e "${BLUE}Extracting files...${NC}" - unzip -q "$TEMP_ZIP" - - EXTRACTED_DIR=$(ls -d */ | grep -E "^PromdataPanel" | head -n 1) - if [ -d "$EXTRACTED_DIR" ]; then - cd "$EXTRACTED_DIR" || exit 1 + unzip -q "$temp_zip" -d "$temp_dir" + + local extracted_dir + extracted_dir=$(find "$temp_dir" -mindepth 1 -maxdepth 1 -type d | head -n 1) + if [ -z "$extracted_dir" ] || [ ! -f "$extracted_dir/server/index.js" ]; then + echo -e "${RED}Download succeeded, but archive structure is invalid.${NC}" + exit 1 + fi + + cd "$extracted_dir" +} + +detect_runtime_user() { + if [ "$EUID" -eq 0 ]; then + REAL_USER="${SUDO_USER:-${USER:-root}}" else - EXTRACTED_DIR=$(ls -d */ | head -n 1) - [ -d "$EXTRACTED_DIR" ] && cd "$EXTRACTED_DIR" || exit 1 + REAL_USER="${USER}" fi - rm "../$TEMP_ZIP" 2>/dev/null || rm "$TEMP_ZIP" 2>/dev/null -fi +} -# 4. Initialize Setup -# Permission check -if [ "$EUID" -eq 0 ]; then - REAL_USER=${SUDO_USER:-$USER} -else - REAL_USER=$USER -fi - -detect_os -install_node - -PROJECT_DIR=$(pwd) -echo -e "Project Directory: ${GREEN}$PROJECT_DIR${NC}" -echo -e "Running User: ${GREEN}$REAL_USER${NC}" - -# Check for .env file -if [ ! -f ".env" ]; then - if [ -f ".env.example" ]; then - echo -e "${BLUE}Creating .env from .env.example...${NC}" - cp .env.example .env +write_service_file() { + local node_path + node_path=$(command -v node) + if [ -z "$node_path" ]; then + echo -e "${RED}Unable to locate node executable after installation.${NC}" + exit 1 fi -fi -# 5. Install Dependencies -echo -e "${BLUE}Installing NPM dependencies...${NC}" -npm install --production + local tmp_service + tmp_service=$(mktemp "${TMPDIR:-/tmp}/${SERVICE_NAME}.service.XXXXXX") -if [ $? -ne 0 ]; then - echo -e "${RED}NPM install failed.${NC}" - exit 1 -fi - -# 6. Create Systemd Service File -SERVICE_FILE="/etc/systemd/system/promdatapanel.service" -NODE_PATH=$(command -v node) - -echo -e "${BLUE}Creating systemd service at $SERVICE_FILE...${NC}" -sudo bash -c "cat < '$SERVICE_FILE' + cat > "$tmp_service" </dev/null | awk '{print $1}') + if [ -n "${IP_ADDR:-}" ]; then + echo -e "Dashboard URL: ${YELLOW}http://${IP_ADDR}:${PORT}${NC}" + fi else echo -e "${RED}FAILED: Service failed to start.${NC}" - echo -e "Check logs with: ${BLUE}journalctl -u promdatapanel -xe${NC}" + echo -e "Check logs with: ${BLUE}journalctl -u ${SERVICE_NAME} -xe${NC}" fi echo -e "${BLUE}================================================${NC}" diff --git a/update.sh b/update.sh index cc2e7b5..2b5584c 100644 --- a/update.sh +++ b/update.sh @@ -1,132 +1,187 @@ #!/bin/bash -# Update Script for PromdataPanel -# ------------------------------- +set -euo pipefail -set -e - -# Config SERVICE_NAME="promdatapanel" DEFAULT_APP_DIR="/opt/promdata-panel" +ZIP_URL="https://git.littlediary.cn/CN-JS-HuiBai/PromdataPanel/archive/main.zip" -# 1. Colors & Banner GREEN='\033[0;32m' BLUE='\033[0;34m' RED='\033[0;31m' -NC='\033[0m' # No Color +YELLOW='\033[1;33m' +NC='\033[0m' + +APP_DIR="" +TEMP_DIR="" +BACKUP_DIR="" +ROLLBACK_REQUIRED=false echo -e "${BLUE}=== Starting PromdataPanel Update ===${NC}" -# 2. Detect APP_DIR automatically from systemd service -if systemctl list-unit-files | grep -q "^$SERVICE_NAME.service"; then - echo "Detecting application directory from systemd service..." - # Get WorkingDirectory from systemd (using env to handle it safely) - SERVICE_DIR=$(systemctl show -p WorkingDirectory "$SERVICE_NAME" | cut -d= -f2-) - if [ -n "$SERVICE_DIR" ] && [ -d "$SERVICE_DIR" ]; then - APP_DIR="$SERVICE_DIR" +cleanup() { + if [ -n "${TEMP_DIR}" ] && [ -d "${TEMP_DIR}" ]; then + rm -rf "${TEMP_DIR}" fi -fi +} -# Fallback 1: Current directory if it contains package.json -if [ -z "$APP_DIR" ]; then - if [ -f "package.json" ]; then - APP_DIR=$(pwd) - else +rollback() { + if [ "$ROLLBACK_REQUIRED" != true ] || [ -z "${BACKUP_DIR}" ] || [ ! -d "${BACKUP_DIR}" ]; then + return + fi + + echo -e "${YELLOW}Update failed. Restoring previous application state...${NC}" + rsync -a --delete --exclude '.env' "${BACKUP_DIR}/" "${APP_DIR}/" +} + +trap 'rollback' ERR +trap cleanup EXIT + +validate_app_dir() { + local dir="$1" + [ -n "$dir" ] || return 1 + [ -d "$dir" ] || return 1 + [ -f "$dir/package.json" ] || return 1 + [ -f "$dir/server/index.js" ] || return 1 + [ -f "$dir/public/index.html" ] || return 1 + return 0 +} + +detect_app_dir() { + local service_dir="" + if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files | grep -q "^${SERVICE_NAME}\.service"; then + echo "Detecting application directory from systemd service..." + service_dir=$(systemctl show -p WorkingDirectory "$SERVICE_NAME" | cut -d= -f2-) + if validate_app_dir "$service_dir"; then + APP_DIR="$service_dir" + return + fi + fi + + local current_dir + current_dir=$(pwd) + if validate_app_dir "$current_dir"; then + APP_DIR="$current_dir" + return + fi + + if validate_app_dir "$DEFAULT_APP_DIR"; then APP_DIR="$DEFAULT_APP_DIR" + return fi -fi -echo -e "${BLUE}Application directory: $APP_DIR${NC}" - -if [ ! -d "$APP_DIR" ]; then - echo -e "${RED}Error: Could not find application directory at $APP_DIR. Check if you installed it correctly.${NC}" + echo -e "${RED}Error: Could not locate a valid PromdataPanel application directory.${NC}" + echo -e "${YELLOW}Expected markers: package.json, server/index.js, public/index.html${NC}" exit 1 -fi +} -cd "$APP_DIR" - -# 2. Update logic -if [ -d ".git" ]; then - echo -e "${BLUE}Git repository detected. Pulling latest code...${NC}" - git pull -else - echo -e "${BLUE}No git repository found. Updating via ZIP archive...${NC}" - - # URL for zip download - ZIP_URL="https://git.littlediary.cn/CN-JS-HuiBai/PromdataPanel/archive/main.zip" - TEMP_DIR="/tmp/promdata_update_$(date +%s)" - - mkdir -p "$TEMP_DIR" - echo "Downloading latest version (main branch)..." - curl -L "$ZIP_URL" -o "$TEMP_DIR/latest.zip" - - # Ensure unzip is available - if ! command -v unzip &> /dev/null; then - echo -e "${BLUE}unzip is not installed. Attempting to install it...${NC}" - if command -v apt-get &> /dev/null; then - sudo apt-get update && sudo apt-get install -y unzip - elif command -v dnf &> /dev/null; then - sudo dnf install -y unzip - elif command -v yum &> /dev/null; then - sudo yum install -y yum-utils && sudo yum install -y unzip - elif command -v apk &> /dev/null; then - sudo apk add unzip - fi +ensure_tool() { + local cmd="$1" + if command -v "$cmd" >/dev/null 2>&1; then + return fi - echo "Extracting archive..." - unzip -q "$TEMP_DIR/latest.zip" -d "$TEMP_DIR" - - # The extracted folder is usually named project-main or similar - EXTRACTED_FOLDER=$(ls -d "$TEMP_DIR"/*/ | head -n 1) - - # Ensure rsync is available (Auto-install if missing) - if ! command -v rsync &> /dev/null; then - echo -e "${BLUE}rsync is not installed. Attempting to install it...${NC}" - if command -v apt-get &> /dev/null; then - sudo apt-get update && sudo apt-get install -y rsync - elif command -v dnf &> /dev/null; then - sudo dnf install -y rsync - elif command -v yum &> /dev/null; then - sudo yum install -y rsync - elif command -v apk &> /dev/null; then - sudo apk add rsync - else - echo -e "${RED}Error: 'rsync' is not installed and could not auto-install. Please install it manually.${NC}" - rm -rf "$TEMP_DIR" - exit 1 - fi - fi - - if [ -n "$EXTRACTED_FOLDER" ]; then - echo "Applying updates (preserving .env)..." - # Copy everything except .env - rsync -av --exclude '.env' --exclude 'node_modules' "$EXTRACTED_FOLDER" ./ + echo -e "${BLUE}${cmd} is not installed. Attempting to install it...${NC}" + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update + sudo apt-get install -y "$cmd" + elif command -v dnf >/dev/null 2>&1; then + sudo dnf install -y "$cmd" + elif command -v yum >/dev/null 2>&1; then + sudo yum install -y "$cmd" + elif command -v apk >/dev/null 2>&1; then + sudo apk add "$cmd" else - echo -e "${RED}Extraction failed. Please check the archive structure.${NC}" + echo -e "${RED}Error: '${cmd}' is not installed and could not be auto-installed.${NC}" exit 1 fi - - # Cleanup - rm -rf "$TEMP_DIR" -fi +} -# 3. Update dependencies -echo -e "${BLUE}Updating npm dependencies...${NC}" -npm install --production +update_from_git() { + echo -e "${BLUE}Git repository detected. Pulling latest code...${NC}" + if [ -n "$(git status --porcelain)" ]; then + echo -e "${RED}Error: Working tree has local changes. Commit or stash them before updating.${NC}" + exit 1 + fi + git pull --ff-only +} -# 4. Restart service -if systemctl is-active --quiet "$SERVICE_NAME"; then - echo -e "${BLUE}Restarting systemd service: $SERVICE_NAME...${NC}" - sudo systemctl restart "$SERVICE_NAME" - echo -e "${GREEN}Update completed and service restarted!${NC}" -elif command -v pm2 &> /dev/null && pm2 list | grep -q "$SERVICE_NAME"; then - echo -e "${BLUE}Restarting with PM2...${NC}" - pm2 restart "$SERVICE_NAME" - echo -e "${GREEN}Update completed and PM2 restarted!${NC}" +update_from_zip() { + echo -e "${BLUE}No git repository found. Updating via ZIP archive with staging and rollback...${NC}" + ensure_tool curl + ensure_tool unzip + ensure_tool rsync + + TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/promdatapanel-update-XXXXXX") + BACKUP_DIR="${TEMP_DIR}/backup" + local archive_path="${TEMP_DIR}/latest.zip" + local extracted_folder="" + local staging_dir="" + + echo "Downloading latest version (main branch)..." + curl -fL "$ZIP_URL" -o "$archive_path" + + echo "Extracting archive..." + unzip -q "$archive_path" -d "$TEMP_DIR" + extracted_folder=$(find "$TEMP_DIR" -mindepth 1 -maxdepth 1 -type d ! -name backup | head -n 1) + + if ! validate_app_dir "$extracted_folder"; then + echo -e "${RED}Extraction failed or archive structure is invalid.${NC}" + exit 1 + fi + + staging_dir="${TEMP_DIR}/staging" + mkdir -p "$staging_dir" + rsync -a --exclude '.git' "$extracted_folder/" "$staging_dir/" + + if [ -f "${APP_DIR}/.env" ]; then + cp "${APP_DIR}/.env" "${staging_dir}/.env" + fi + + echo "Installing dependencies in staging directory..." + ( + cd "$staging_dir" + npm install --production + ) + + echo "Creating rollback backup..." + rsync -a --delete --exclude '.env' "${APP_DIR}/" "${BACKUP_DIR}/" + + echo "Applying staged update..." + ROLLBACK_REQUIRED=true + rsync -a --delete --exclude '.env' "${staging_dir}/" "${APP_DIR}/" +} + +restart_service() { + if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet "$SERVICE_NAME"; then + echo -e "${BLUE}Restarting systemd service: ${SERVICE_NAME}...${NC}" + sudo systemctl restart "$SERVICE_NAME" + return + fi + + if command -v pm2 >/dev/null 2>&1 && pm2 list | grep -q "$SERVICE_NAME"; then + echo -e "${BLUE}Restarting with PM2...${NC}" + pm2 restart "$SERVICE_NAME" + return + fi + + echo -e "${YELLOW}Warning: Could not detect an active systemd service or PM2 process named '${SERVICE_NAME}'.${NC}" + echo -e "${YELLOW}Please restart the application manually.${NC}" +} + +detect_app_dir +echo -e "${BLUE}Application directory: ${APP_DIR}${NC}" +cd "$APP_DIR" + +if [ -d ".git" ]; then + update_from_git + echo -e "${BLUE}Updating npm dependencies...${NC}" + npm install --production else - echo -e "${RED}Warning: Could not detect an active systemd service or PM2 process named '$SERVICE_NAME'.${NC}" - echo -e "${RED}Please restart the application manually.${NC}" + update_from_zip fi +restart_service +ROLLBACK_REQUIRED=false + echo -e "${GREEN}=== Update successfully finished ===${NC}"