Files
lijiaoqiao/llm-gateway-competitors/sub2api-tar/deploy/install.sh

1170 lines
37 KiB
Bash
Raw Normal View History

#!/bin/bash
#
# Sub2API Installation Script
# Sub2API 安装脚本
# Usage: curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | bash
#
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Configuration
GITHUB_REPO="Wei-Shaw/sub2api"
INSTALL_DIR="/opt/sub2api"
SERVICE_NAME="sub2api"
SERVICE_USER="sub2api"
CONFIG_DIR="/etc/sub2api"
# Server configuration (will be set by user)
SERVER_HOST="0.0.0.0"
SERVER_PORT="8080"
# Language (default: zh = Chinese)
LANG_CHOICE="zh"
# ============================================================
# Language strings / 语言字符串
# ============================================================
# Chinese strings
declare -A MSG_ZH=(
# General
["info"]="信息"
["success"]="成功"
["warning"]="警告"
["error"]="错误"
# Language selection
["select_lang"]="请选择语言 / Select language"
["lang_zh"]="中文"
["lang_en"]="English"
["enter_choice"]="请输入选择 (默认: 1)"
# Installation
["install_title"]="Sub2API 安装脚本"
["run_as_root"]="请使用 root 权限运行 (使用 sudo)"
["detected_platform"]="检测到平台"
["unsupported_arch"]="不支持的架构"
["unsupported_os"]="不支持的操作系统"
["missing_deps"]="缺少依赖"
["install_deps_first"]="请先安装以下依赖"
["fetching_version"]="正在获取最新版本..."
["latest_version"]="最新版本"
["failed_get_version"]="获取最新版本失败"
["downloading"]="正在下载"
["download_failed"]="下载失败"
["verifying_checksum"]="正在校验文件..."
["checksum_verified"]="校验通过"
["checksum_failed"]="校验失败"
["checksum_not_found"]="无法验证校验和checksums.txt 未找到)"
["extracting"]="正在解压..."
["binary_installed"]="二进制文件已安装到"
["user_exists"]="用户已存在"
["creating_user"]="正在创建系统用户"
["user_created"]="用户已创建"
["setting_up_dirs"]="正在设置目录..."
["dirs_configured"]="目录配置完成"
["installing_service"]="正在安装 systemd 服务..."
["service_installed"]="systemd 服务已安装"
["ready_for_setup"]="准备就绪,可以启动设置向导"
# Completion
["install_complete"]="Sub2API 安装完成!"
["install_dir"]="安装目录"
["next_steps"]="后续步骤"
["step1_check_services"]="确保 PostgreSQL 和 Redis 正在运行:"
["step2_start_service"]="启动 Sub2API 服务:"
["step3_enable_autostart"]="设置开机自启:"
["step4_open_wizard"]="在浏览器中打开设置向导:"
["wizard_guide"]="设置向导将引导您完成:"
["wizard_db"]="数据库配置"
["wizard_redis"]="Redis 配置"
["wizard_admin"]="管理员账号创建"
["useful_commands"]="常用命令"
["cmd_status"]="查看状态"
["cmd_logs"]="查看日志"
["cmd_restart"]="重启服务"
["cmd_stop"]="停止服务"
# Upgrade
["upgrading"]="正在升级 Sub2API..."
["current_version"]="当前版本"
["stopping_service"]="正在停止服务..."
["backup_created"]="备份已创建"
["starting_service"]="正在启动服务..."
["upgrade_complete"]="升级完成!"
# Version install
["installing_version"]="正在安装指定版本"
["version_not_found"]="指定版本不存在"
["same_version"]="已经是该版本,无需操作"
["rollback_complete"]="版本回退完成!"
["install_version_complete"]="指定版本安装完成!"
["validating_version"]="正在验证版本..."
["available_versions"]="可用版本列表"
["fetching_versions"]="正在获取可用版本..."
["not_installed"]="Sub2API 尚未安装,请先执行全新安装"
["fresh_install_hint"]="用法"
# Uninstall
["uninstall_confirm"]="这将从系统中移除 Sub2API。"
["are_you_sure"]="确定要继续吗?(y/N)"
["uninstall_cancelled"]="卸载已取消"
["removing_files"]="正在移除文件..."
["removing_install_dir"]="正在移除安装目录..."
["removing_user"]="正在移除用户..."
["config_not_removed"]="配置目录未被移除"
["remove_manually"]="如不再需要,请手动删除"
["removing_install_lock"]="正在移除安装锁文件..."
["install_lock_removed"]="安装锁文件已移除,重新安装时将进入设置向导"
["purge_prompt"]="是否同时删除配置目录?这将清除所有配置和数据 [y/N]: "
["removing_config_dir"]="正在移除配置目录..."
["uninstall_complete"]="Sub2API 已卸载"
# Help
["usage"]="用法"
["cmd_none"]="(无参数)"
["cmd_install"]="安装 Sub2API"
["cmd_upgrade"]="升级到最新版本"
["cmd_uninstall"]="卸载 Sub2API"
["cmd_install_version"]="安装/回退到指定版本"
["cmd_list_versions"]="列出可用版本"
["opt_version"]="指定要安装的版本号 (例如: v1.0.0)"
# Server configuration
["server_config_title"]="服务器配置"
["server_config_desc"]="配置 Sub2API 服务监听地址"
["server_host_prompt"]="服务器监听地址"
["server_host_hint"]="0.0.0.0 表示监听所有网卡127.0.0.1 仅本地访问"
["server_port_prompt"]="服务器端口"
["server_port_hint"]="建议使用 1024-65535 之间的端口"
["server_config_summary"]="服务器配置"
["invalid_port"]="无效端口号,请输入 1-65535 之间的数字"
# Service management
["starting_service"]="正在启动服务..."
["service_started"]="服务已启动"
["service_start_failed"]="服务启动失败,请检查日志"
["enabling_autostart"]="正在设置开机自启..."
["autostart_enabled"]="开机自启已启用"
["getting_public_ip"]="正在获取公网 IP..."
["public_ip_failed"]="无法获取公网 IP使用本地 IP"
)
# English strings
declare -A MSG_EN=(
# General
["info"]="INFO"
["success"]="SUCCESS"
["warning"]="WARNING"
["error"]="ERROR"
# Language selection
["select_lang"]="请选择语言 / Select language"
["lang_zh"]="中文"
["lang_en"]="English"
["enter_choice"]="Enter your choice (default: 1)"
# Installation
["install_title"]="Sub2API Installation Script"
["run_as_root"]="Please run as root (use sudo)"
["detected_platform"]="Detected platform"
["unsupported_arch"]="Unsupported architecture"
["unsupported_os"]="Unsupported OS"
["missing_deps"]="Missing dependencies"
["install_deps_first"]="Please install them first"
["fetching_version"]="Fetching latest version..."
["latest_version"]="Latest version"
["failed_get_version"]="Failed to get latest version"
["downloading"]="Downloading"
["download_failed"]="Download failed"
["verifying_checksum"]="Verifying checksum..."
["checksum_verified"]="Checksum verified"
["checksum_failed"]="Checksum verification failed"
["checksum_not_found"]="Could not verify checksum (checksums.txt not found)"
["extracting"]="Extracting..."
["binary_installed"]="Binary installed to"
["user_exists"]="User already exists"
["creating_user"]="Creating system user"
["user_created"]="User created"
["setting_up_dirs"]="Setting up directories..."
["dirs_configured"]="Directories configured"
["installing_service"]="Installing systemd service..."
["service_installed"]="Systemd service installed"
["ready_for_setup"]="Ready for Setup Wizard"
# Completion
["install_complete"]="Sub2API installation completed!"
["install_dir"]="Installation directory"
["next_steps"]="NEXT STEPS"
["step1_check_services"]="Make sure PostgreSQL and Redis are running:"
["step2_start_service"]="Start Sub2API service:"
["step3_enable_autostart"]="Enable auto-start on boot:"
["step4_open_wizard"]="Open the Setup Wizard in your browser:"
["wizard_guide"]="The Setup Wizard will guide you through:"
["wizard_db"]="Database configuration"
["wizard_redis"]="Redis configuration"
["wizard_admin"]="Admin account creation"
["useful_commands"]="USEFUL COMMANDS"
["cmd_status"]="Check status"
["cmd_logs"]="View logs"
["cmd_restart"]="Restart"
["cmd_stop"]="Stop"
# Upgrade
["upgrading"]="Upgrading Sub2API..."
["current_version"]="Current version"
["stopping_service"]="Stopping service..."
["backup_created"]="Backup created"
["starting_service"]="Starting service..."
["upgrade_complete"]="Upgrade completed!"
# Version install
["installing_version"]="Installing specified version"
["version_not_found"]="Specified version not found"
["same_version"]="Already at this version, no action needed"
["rollback_complete"]="Version rollback completed!"
["install_version_complete"]="Specified version installed!"
["validating_version"]="Validating version..."
["available_versions"]="Available versions"
["fetching_versions"]="Fetching available versions..."
["not_installed"]="Sub2API is not installed. Please run a fresh install first"
["fresh_install_hint"]="Usage"
# Uninstall
["uninstall_confirm"]="This will remove Sub2API from your system."
["are_you_sure"]="Are you sure? (y/N)"
["uninstall_cancelled"]="Uninstall cancelled"
["removing_files"]="Removing files..."
["removing_install_dir"]="Removing installation directory..."
["removing_user"]="Removing user..."
["config_not_removed"]="Config directory was NOT removed."
["remove_manually"]="Remove it manually if you no longer need it."
["removing_install_lock"]="Removing install lock file..."
["install_lock_removed"]="Install lock removed. Setup wizard will appear on next install."
["purge_prompt"]="Also remove config directory? This will delete all config and data [y/N]: "
["removing_config_dir"]="Removing config directory..."
["uninstall_complete"]="Sub2API has been uninstalled"
# Help
["usage"]="Usage"
["cmd_none"]="(none)"
["cmd_install"]="Install Sub2API"
["cmd_upgrade"]="Upgrade to the latest version"
["cmd_uninstall"]="Remove Sub2API"
["cmd_install_version"]="Install/rollback to a specific version"
["cmd_list_versions"]="List available versions"
["opt_version"]="Specify version to install (e.g., v1.0.0)"
# Server configuration
["server_config_title"]="Server Configuration"
["server_config_desc"]="Configure Sub2API server listen address"
["server_host_prompt"]="Server listen address"
["server_host_hint"]="0.0.0.0 listens on all interfaces, 127.0.0.1 for local only"
["server_port_prompt"]="Server port"
["server_port_hint"]="Recommended range: 1024-65535"
["server_config_summary"]="Server configuration"
["invalid_port"]="Invalid port number, please enter a number between 1-65535"
# Service management
["starting_service"]="Starting service..."
["service_started"]="Service started"
["service_start_failed"]="Service failed to start, please check logs"
["enabling_autostart"]="Enabling auto-start on boot..."
["autostart_enabled"]="Auto-start enabled"
["getting_public_ip"]="Getting public IP..."
["public_ip_failed"]="Failed to get public IP, using local IP"
)
# Get message based on current language
msg() {
local key="$1"
if [ "$LANG_CHOICE" = "en" ]; then
echo "${MSG_EN[$key]}"
else
echo "${MSG_ZH[$key]}"
fi
}
# Print functions
print_info() {
echo -e "${BLUE}[$(msg 'info')]${NC} $1"
}
print_success() {
echo -e "${GREEN}[$(msg 'success')]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[$(msg 'warning')]${NC} $1"
}
print_error() {
echo -e "${RED}[$(msg 'error')]${NC} $1"
}
# Check if running interactively (can access terminal)
# When piped (curl | bash), stdin is not a terminal, but /dev/tty may still be available
is_interactive() {
# Check if /dev/tty is available (works even when piped)
[ -e /dev/tty ] && [ -r /dev/tty ] && [ -w /dev/tty ]
}
# Select language
select_language() {
# If not interactive (piped), use default language
if ! is_interactive; then
LANG_CHOICE="zh"
return
fi
echo ""
echo -e "${CYAN}=============================================="
echo " $(msg 'select_lang')"
echo "==============================================${NC}"
echo ""
echo " 1) $(msg 'lang_zh') (默认/default)"
echo " 2) $(msg 'lang_en')"
echo ""
read -p "$(msg 'enter_choice'): " lang_input < /dev/tty
case "$lang_input" in
2|en|EN|english|English)
LANG_CHOICE="en"
;;
*)
LANG_CHOICE="zh"
;;
esac
echo ""
}
# Validate port number
validate_port() {
local port="$1"
if [[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ]; then
return 0
fi
return 1
}
# Configure server settings
configure_server() {
# If not interactive (piped), use default settings
if ! is_interactive; then
print_info "$(msg 'server_config_summary'): ${SERVER_HOST}:${SERVER_PORT} (default)"
return
fi
echo ""
echo -e "${CYAN}=============================================="
echo " $(msg 'server_config_title')"
echo "==============================================${NC}"
echo ""
echo -e "${BLUE}$(msg 'server_config_desc')${NC}"
echo ""
# Server host
echo -e "${YELLOW}$(msg 'server_host_hint')${NC}"
read -p "$(msg 'server_host_prompt') [${SERVER_HOST}]: " input_host < /dev/tty
if [ -n "$input_host" ]; then
SERVER_HOST="$input_host"
fi
echo ""
# Server port
echo -e "${YELLOW}$(msg 'server_port_hint')${NC}"
while true; do
read -p "$(msg 'server_port_prompt') [${SERVER_PORT}]: " input_port < /dev/tty
if [ -z "$input_port" ]; then
# Use default
break
elif validate_port "$input_port"; then
SERVER_PORT="$input_port"
break
else
print_error "$(msg 'invalid_port')"
fi
done
echo ""
print_info "$(msg 'server_config_summary'): ${SERVER_HOST}:${SERVER_PORT}"
echo ""
}
# Check if running as root
check_root() {
# Use 'id -u' instead of $EUID for better compatibility
# $EUID may not work reliably when script is piped to bash
if [ "$(id -u)" -ne 0 ]; then
print_error "$(msg 'run_as_root')"
exit 1
fi
}
# Detect OS and architecture
detect_platform() {
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "$ARCH" in
x86_64)
ARCH="amd64"
;;
aarch64|arm64)
ARCH="arm64"
;;
*)
print_error "$(msg 'unsupported_arch'): $ARCH"
exit 1
;;
esac
case "$OS" in
linux)
OS="linux"
;;
darwin)
OS="darwin"
;;
*)
print_error "$(msg 'unsupported_os'): $OS"
exit 1
;;
esac
print_info "$(msg 'detected_platform'): ${OS}_${ARCH}"
}
# Check dependencies
check_dependencies() {
local missing=()
if ! command -v curl &> /dev/null; then
missing+=("curl")
fi
if ! command -v tar &> /dev/null; then
missing+=("tar")
fi
if [ ${#missing[@]} -gt 0 ]; then
print_error "$(msg 'missing_deps'): ${missing[*]}"
print_info "$(msg 'install_deps_first')"
exit 1
fi
}
# Get latest release version
get_latest_version() {
print_info "$(msg 'fetching_version')"
LATEST_VERSION=$(curl -s --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>/dev/null | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
if [ -z "$LATEST_VERSION" ]; then
print_error "$(msg 'failed_get_version')"
print_info "Please check your network connection or try again later."
exit 1
fi
print_info "$(msg 'latest_version'): $LATEST_VERSION"
}
# List available versions
list_versions() {
print_info "$(msg 'fetching_versions')"
local versions
versions=$(curl -s --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${GITHUB_REPO}/releases" 2>/dev/null | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/' | head -20)
if [ -z "$versions" ]; then
print_error "$(msg 'failed_get_version')"
print_info "Please check your network connection or try again later."
exit 1
fi
echo ""
echo "$(msg 'available_versions'):"
echo "----------------------------------------"
echo "$versions" | while read -r version; do
echo " $version"
done
echo "----------------------------------------"
echo ""
}
# Validate if a version exists
validate_version() {
local version="$1"
# Check for empty version
if [ -z "$version" ]; then
print_error "$(msg 'opt_version')" >&2
exit 1
fi
# Ensure version starts with 'v'
if [[ ! "$version" =~ ^v ]]; then
version="v$version"
fi
print_info "$(msg 'validating_version') $version" >&2
# Check if the release exists
local http_code
http_code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${version}" 2>/dev/null)
# Check for network errors (empty or non-numeric response)
if [ -z "$http_code" ] || ! [[ "$http_code" =~ ^[0-9]+$ ]]; then
print_error "Network error: Failed to connect to GitHub API" >&2
exit 1
fi
if [ "$http_code" != "200" ]; then
print_error "$(msg 'version_not_found'): $version" >&2
echo "" >&2
list_versions >&2
exit 1
fi
# Return the normalized version (to stdout)
echo "$version"
}
# Get current installed version
get_current_version() {
if [ -f "$INSTALL_DIR/sub2api" ]; then
# Use grep -E for better compatibility (works on macOS and Linux)
"$INSTALL_DIR/sub2api" --version 2>/dev/null | grep -oE 'v?[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown"
else
echo "not_installed"
fi
}
# Download and extract
download_and_extract() {
local version_num=${LATEST_VERSION#v}
local archive_name="sub2api_${version_num}_${OS}_${ARCH}.tar.gz"
local download_url="https://github.com/${GITHUB_REPO}/releases/download/${LATEST_VERSION}/${archive_name}"
local checksum_url="https://github.com/${GITHUB_REPO}/releases/download/${LATEST_VERSION}/checksums.txt"
print_info "$(msg 'downloading') ${archive_name}..."
# Create temp directory
TEMP_DIR=$(mktemp -d)
trap "rm -rf $TEMP_DIR" EXIT
# Download archive
if ! curl -sL "$download_url" -o "$TEMP_DIR/$archive_name"; then
print_error "$(msg 'download_failed')"
exit 1
fi
# Download and verify checksum
print_info "$(msg 'verifying_checksum')"
if curl -sL "$checksum_url" -o "$TEMP_DIR/checksums.txt" 2>/dev/null; then
local expected_checksum=$(grep "$archive_name" "$TEMP_DIR/checksums.txt" | awk '{print $1}')
local actual_checksum=$(sha256sum "$TEMP_DIR/$archive_name" | awk '{print $1}')
if [ "$expected_checksum" != "$actual_checksum" ]; then
print_error "$(msg 'checksum_failed')"
print_error "Expected: $expected_checksum"
print_error "Actual: $actual_checksum"
exit 1
fi
print_success "$(msg 'checksum_verified')"
else
print_warning "$(msg 'checksum_not_found')"
fi
# Extract
print_info "$(msg 'extracting')"
tar -xzf "$TEMP_DIR/$archive_name" -C "$TEMP_DIR"
# Create install directory
mkdir -p "$INSTALL_DIR"
# Copy binary
cp "$TEMP_DIR/sub2api" "$INSTALL_DIR/sub2api"
chmod +x "$INSTALL_DIR/sub2api"
# Copy deploy files if they exist in the archive
if [ -d "$TEMP_DIR/deploy" ]; then
cp -r "$TEMP_DIR/deploy/"* "$INSTALL_DIR/" 2>/dev/null || true
fi
print_success "$(msg 'binary_installed') $INSTALL_DIR/sub2api"
}
# Create system user
create_user() {
if id "$SERVICE_USER" &>/dev/null; then
print_info "$(msg 'user_exists'): $SERVICE_USER"
# Fix: Ensure existing user has /bin/sh shell for sudo to work
# Previous versions used /bin/false which prevents sudo execution
local current_shell
current_shell=$(getent passwd "$SERVICE_USER" 2>/dev/null | cut -d: -f7)
if [ "$current_shell" = "/bin/false" ] || [ "$current_shell" = "/sbin/nologin" ]; then
print_info "Fixing user shell for sudo compatibility..."
if usermod -s /bin/sh "$SERVICE_USER" 2>/dev/null; then
print_success "User shell updated to /bin/sh"
else
print_warning "Failed to update user shell. Service restart may not work automatically."
print_warning "Manual fix: sudo usermod -s /bin/sh $SERVICE_USER"
fi
fi
else
print_info "$(msg 'creating_user') $SERVICE_USER..."
# Use /bin/sh instead of /bin/false to allow sudo execution
# The user still cannot login interactively (no password set)
useradd -r -s /bin/sh -d "$INSTALL_DIR" "$SERVICE_USER"
print_success "$(msg 'user_created')"
fi
}
# Setup directories and permissions
setup_directories() {
print_info "$(msg 'setting_up_dirs')"
# Create directories
mkdir -p "$INSTALL_DIR"
mkdir -p "$INSTALL_DIR/data"
mkdir -p "$CONFIG_DIR"
# Set ownership
chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR"
chown -R "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR"
print_success "$(msg 'dirs_configured')"
}
# Install systemd service
install_service() {
print_info "$(msg 'installing_service')"
# Create service file with configured host and port
cat > /etc/systemd/system/sub2api.service << EOF
[Unit]
Description=Sub2API - AI API Gateway Platform
Documentation=https://github.com/Wei-Shaw/sub2api
After=network.target postgresql.service redis.service
Wants=postgresql.service redis.service
[Service]
Type=simple
User=sub2api
Group=sub2api
WorkingDirectory=/opt/sub2api
ExecStart=/opt/sub2api/sub2api
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=sub2api
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/opt/sub2api
# Environment - Server configuration
Environment=GIN_MODE=release
Environment=SERVER_HOST=${SERVER_HOST}
Environment=SERVER_PORT=${SERVER_PORT}
[Install]
WantedBy=multi-user.target
EOF
# Reload systemd
systemctl daemon-reload
print_success "$(msg 'service_installed')"
}
# Prepare for setup wizard (no config file needed - setup wizard will create it)
prepare_for_setup() {
print_success "$(msg 'ready_for_setup')"
}
# Get public IP address
get_public_ip() {
print_info "$(msg 'getting_public_ip')"
# Try to get public IP from ipinfo.io
local response
response=$(curl -s --connect-timeout 5 --max-time 10 "https://ipinfo.io/json" 2>/dev/null)
if [ -n "$response" ]; then
# Extract IP from JSON response using grep and sed (no jq dependency)
PUBLIC_IP=$(echo "$response" | grep -o '"ip": *"[^"]*"' | sed 's/"ip": *"\([^"]*\)"/\1/')
if [ -n "$PUBLIC_IP" ]; then
print_success "Public IP: $PUBLIC_IP"
return 0
fi
fi
# Fallback to local IP
print_warning "$(msg 'public_ip_failed')"
PUBLIC_IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "YOUR_SERVER_IP")
return 1
}
# Start service
start_service() {
print_info "$(msg 'starting_service')"
if systemctl start sub2api; then
print_success "$(msg 'service_started')"
return 0
else
print_error "$(msg 'service_start_failed')"
print_info "sudo journalctl -u sub2api -n 50"
return 1
fi
}
# Enable service auto-start
enable_autostart() {
print_info "$(msg 'enabling_autostart')"
if systemctl enable sub2api 2>/dev/null; then
print_success "$(msg 'autostart_enabled')"
return 0
else
print_warning "Failed to enable auto-start"
return 1
fi
}
# Print completion message
print_completion() {
# Use PUBLIC_IP which was set by get_public_ip()
# Determine display address
local display_host="${PUBLIC_IP:-YOUR_SERVER_IP}"
if [ "$SERVER_HOST" = "127.0.0.1" ]; then
display_host="127.0.0.1"
fi
echo ""
echo "=============================================="
print_success "$(msg 'install_complete')"
echo "=============================================="
echo ""
echo "$(msg 'install_dir'): $INSTALL_DIR"
echo "$(msg 'server_config_summary'): ${SERVER_HOST}:${SERVER_PORT}"
echo ""
echo "=============================================="
echo " $(msg 'step4_open_wizard')"
echo "=============================================="
echo ""
print_info " http://${display_host}:${SERVER_PORT}"
echo ""
echo " $(msg 'wizard_guide')"
echo " - $(msg 'wizard_db')"
echo " - $(msg 'wizard_redis')"
echo " - $(msg 'wizard_admin')"
echo ""
echo "=============================================="
echo " $(msg 'useful_commands')"
echo "=============================================="
echo ""
echo " $(msg 'cmd_status'): sudo systemctl status sub2api"
echo " $(msg 'cmd_logs'): sudo journalctl -u sub2api -f"
echo " $(msg 'cmd_restart'): sudo systemctl restart sub2api"
echo " $(msg 'cmd_stop'): sudo systemctl stop sub2api"
echo ""
echo "=============================================="
}
# Upgrade function
upgrade() {
# Check if Sub2API is installed
if [ ! -f "$INSTALL_DIR/sub2api" ]; then
print_error "$(msg 'not_installed')"
print_info "$(msg 'fresh_install_hint'): $0 install"
exit 1
fi
print_info "$(msg 'upgrading')"
# Get current version
CURRENT_VERSION=$("$INSTALL_DIR/sub2api" --version 2>/dev/null | grep -oE 'v?[0-9]+\.[0-9]+\.[0-9]+' || echo "unknown")
print_info "$(msg 'current_version'): $CURRENT_VERSION"
# Stop service
if systemctl is-active --quiet sub2api; then
print_info "$(msg 'stopping_service')"
systemctl stop sub2api
fi
# Backup current binary
cp "$INSTALL_DIR/sub2api" "$INSTALL_DIR/sub2api.backup"
print_info "$(msg 'backup_created'): $INSTALL_DIR/sub2api.backup"
# Download and install new version
get_latest_version
download_and_extract
# Set permissions
chown "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR/sub2api"
# Start service
print_info "$(msg 'starting_service')"
systemctl start sub2api
print_success "$(msg 'upgrade_complete')"
}
# Install specific version (for upgrade or rollback)
# Requires: Sub2API must already be installed
install_version() {
local target_version="$1"
# Check if Sub2API is installed
if [ ! -f "$INSTALL_DIR/sub2api" ]; then
print_error "$(msg 'not_installed')"
print_info "$(msg 'fresh_install_hint'): $0 install -v $target_version"
exit 1
fi
# Validate and normalize version
target_version=$(validate_version "$target_version")
print_info "$(msg 'installing_version'): $target_version"
# Get current version
local current_version
current_version=$(get_current_version)
print_info "$(msg 'current_version'): $current_version"
# Check if same version
if [ "$current_version" = "$target_version" ] || [ "$current_version" = "${target_version#v}" ]; then
print_warning "$(msg 'same_version')"
exit 0
fi
# Stop service if running
if systemctl is-active --quiet sub2api; then
print_info "$(msg 'stopping_service')"
systemctl stop sub2api
fi
# Backup current binary (for potential recovery)
if [ -f "$INSTALL_DIR/sub2api" ]; then
local backup_name
if [ "$current_version" != "unknown" ] && [ "$current_version" != "not_installed" ]; then
backup_name="sub2api.backup.${current_version}"
else
backup_name="sub2api.backup.$(date +%Y%m%d%H%M%S)"
fi
cp "$INSTALL_DIR/sub2api" "$INSTALL_DIR/$backup_name"
print_info "$(msg 'backup_created'): $INSTALL_DIR/$backup_name"
fi
# Set LATEST_VERSION to the target version for download_and_extract
LATEST_VERSION="$target_version"
# Download and install
download_and_extract
# Set permissions
chown "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR/sub2api"
# Start service
print_info "$(msg 'starting_service')"
if systemctl start sub2api; then
print_success "$(msg 'service_started')"
else
print_error "$(msg 'service_start_failed')"
print_info "sudo journalctl -u sub2api -n 50"
fi
# Print completion message
local new_version
new_version=$(get_current_version)
echo ""
echo "=============================================="
print_success "$(msg 'install_version_complete')"
echo "=============================================="
echo ""
echo " $(msg 'current_version'): $new_version"
echo ""
}
# Uninstall function
uninstall() {
print_warning "$(msg 'uninstall_confirm')"
# If not interactive (piped), require -y flag or skip confirmation
if ! is_interactive; then
if [ "${FORCE_YES:-}" != "true" ]; then
print_error "Non-interactive mode detected. Use 'curl ... | bash -s -- uninstall -y' to confirm."
exit 1
fi
else
read -p "$(msg 'are_you_sure') " -n 1 -r < /dev/tty
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
print_info "$(msg 'uninstall_cancelled')"
exit 0
fi
fi
print_info "$(msg 'stopping_service')"
systemctl stop sub2api 2>/dev/null || true
systemctl disable sub2api 2>/dev/null || true
print_info "$(msg 'removing_files')"
rm -f /etc/systemd/system/sub2api.service
systemctl daemon-reload
print_info "$(msg 'removing_install_dir')"
rm -rf "$INSTALL_DIR"
print_info "$(msg 'removing_user')"
userdel "$SERVICE_USER" 2>/dev/null || true
# Remove install lock file (.installed) to allow fresh setup on reinstall
print_info "$(msg 'removing_install_lock')"
rm -f "$CONFIG_DIR/.installed" 2>/dev/null || true
rm -f "$INSTALL_DIR/.installed" 2>/dev/null || true
print_success "$(msg 'install_lock_removed')"
# Ask about config directory removal (interactive mode only)
local remove_config=false
if [ "${PURGE:-}" = "true" ]; then
remove_config=true
elif is_interactive; then
read -p "$(msg 'purge_prompt')" -n 1 -r < /dev/tty
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
remove_config=true
fi
fi
if [ "$remove_config" = true ]; then
print_info "$(msg 'removing_config_dir')"
rm -rf "$CONFIG_DIR"
else
print_warning "$(msg 'config_not_removed'): $CONFIG_DIR"
print_warning "$(msg 'remove_manually')"
fi
print_success "$(msg 'uninstall_complete')"
}
# Main
main() {
# Parse flags first
local target_version=""
local positional_args=()
while [[ $# -gt 0 ]]; do
case "$1" in
-y|--yes)
FORCE_YES="true"
shift
;;
--purge)
PURGE="true"
shift
;;
-v|--version)
if [ -n "${2:-}" ] && [[ ! "$2" =~ ^- ]]; then
target_version="$2"
shift 2
else
echo "Error: --version requires a version argument"
exit 1
fi
;;
--version=*)
target_version="${1#*=}"
if [ -z "$target_version" ]; then
echo "Error: --version requires a version argument"
exit 1
fi
shift
;;
*)
positional_args+=("$1")
shift
;;
esac
done
# Restore positional arguments
set -- "${positional_args[@]}"
# Select language first
select_language
echo ""
echo "=============================================="
echo " $(msg 'install_title')"
echo "=============================================="
echo ""
# Parse commands
case "${1:-}" in
upgrade|update)
check_root
detect_platform
check_dependencies
if [ -n "$target_version" ]; then
# Upgrade to specific version
install_version "$target_version"
else
# Upgrade to latest
upgrade
fi
exit 0
;;
install)
# Install with optional version
check_root
detect_platform
check_dependencies
if [ -n "$target_version" ]; then
# Install specific version (fresh install or rollback)
if [ -f "$INSTALL_DIR/sub2api" ]; then
# Already installed, treat as version change
install_version "$target_version"
else
# Fresh install with specific version
configure_server
LATEST_VERSION=$(validate_version "$target_version")
download_and_extract
create_user
setup_directories
install_service
prepare_for_setup
get_public_ip
start_service
enable_autostart
print_completion
fi
else
# Fresh install with latest version
configure_server
get_latest_version
download_and_extract
create_user
setup_directories
install_service
prepare_for_setup
get_public_ip
start_service
enable_autostart
print_completion
fi
exit 0
;;
rollback)
# Rollback to a specific version (alias for install with version)
if [ -z "$target_version" ] && [ -n "${2:-}" ]; then
target_version="$2"
fi
if [ -z "$target_version" ]; then
print_error "$(msg 'opt_version')"
echo ""
echo "Usage: $0 rollback -v <version>"
echo " $0 rollback <version>"
echo ""
list_versions
exit 1
fi
check_root
detect_platform
check_dependencies
install_version "$target_version"
exit 0
;;
list-versions|versions)
list_versions
exit 0
;;
uninstall|remove)
check_root
uninstall
exit 0
;;
--help|-h)
echo "$(msg 'usage'): $0 [command] [options]"
echo ""
echo "Commands:"
echo " $(msg 'cmd_none') $(msg 'cmd_install')"
echo " install $(msg 'cmd_install')"
echo " upgrade $(msg 'cmd_upgrade')"
echo " rollback <version> $(msg 'cmd_install_version')"
echo " list-versions $(msg 'cmd_list_versions')"
echo " uninstall $(msg 'cmd_uninstall')"
echo ""
echo "Options:"
echo " -v, --version <ver> $(msg 'opt_version')"
echo " -y, --yes Skip confirmation prompts (for uninstall)"
echo ""
echo "Examples:"
echo " $0 # Install latest version"
echo " $0 install -v v0.1.0 # Install specific version"
echo " $0 upgrade # Upgrade to latest"
echo " $0 upgrade -v v0.2.0 # Upgrade to specific version"
echo " $0 rollback v0.1.0 # Rollback to v0.1.0"
echo " $0 list-versions # List available versions"
echo ""
exit 0
;;
esac
# Default: Fresh install with latest version
check_root
detect_platform
check_dependencies
if [ -n "$target_version" ]; then
# Install specific version
if [ -f "$INSTALL_DIR/sub2api" ]; then
install_version "$target_version"
else
configure_server
LATEST_VERSION=$(validate_version "$target_version")
download_and_extract
create_user
setup_directories
install_service
prepare_for_setup
get_public_ip
start_service
enable_autostart
print_completion
fi
else
# Install latest version
configure_server
get_latest_version
download_and_extract
create_user
setup_directories
install_service
prepare_for_setup
get_public_ip
start_service
enable_autostart
print_completion
fi
}
main "$@"