| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232 |
- #!/bin/bash
- #
- # Sub2API Installation Script
- # Sub2API 安装脚本
- # Usage: curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | bash
- #
- set -e
- # Bash 4+ is required for associative arrays used by the localized message table.
- # Keep this guard before any Bash 4-only syntax so older shells fail with a clear hint.
- if [ -z "${BASH_VERSION:-}" ]; then
- echo "Error: This installer must be run with Bash 4.0 or later." >&2
- echo "Please install Bash 4+ and run it with that interpreter." >&2
- exit 1
- fi
- BASH_MAJOR_VERSION="${BASH_VERSION%%.*}"
- if [ "$BASH_MAJOR_VERSION" -lt 4 ]; then
- echo "Error: Bash 4.0 or later is required. Current version: $BASH_VERSION" >&2
- echo "Please install Bash 4+ and retry with that interpreter." >&2
- exit 1
- fi
- # 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
- }
- # Authenticate only GitHub REST API requests. Release asset downloads must stay anonymous.
- github_api_curl() {
- local arg
- local expect_value=false
- local url
- if [ "$#" -lt 1 ]; then
- echo "github_api_curl requires exactly one GitHub API URL" >&2
- return 2
- fi
- url="${!#}"
- # Keep authenticated invocations constrained to the options used below. In
- # particular, curl config, --url, and --next could add another destination.
- for arg in "${@:1:$#-1}"; do
- if [ "$expect_value" = true ]; then
- expect_value=false
- continue
- fi
- case "$arg" in
- -s|--silent)
- ;;
- --connect-timeout|--max-time|-o|--output|-w|--write-out)
- expect_value=true
- ;;
- *)
- echo "Unsafe github_api_curl argument: $arg" >&2
- return 2
- ;;
- esac
- done
- if [ "$expect_value" = true ] || [[ "$url" != https://api.github.com/* ]]; then
- echo "github_api_curl requires exactly one GitHub API URL" >&2
- return 2
- fi
- if [ -n "${UPDATE_GITHUB_TOKEN:-}" ]; then
- if [[ "$UPDATE_GITHUB_TOKEN" == *$'\n'* || "$UPDATE_GITHUB_TOKEN" == *$'\r'* || "$UPDATE_GITHUB_TOKEN" == *'"'* || "$UPDATE_GITHUB_TOKEN" == *'\'* ]]; then
- echo "UPDATE_GITHUB_TOKEN contains unsupported characters" >&2
- return 2
- fi
- printf 'header = "Authorization: Bearer %s"\n' "$UPDATE_GITHUB_TOKEN" | UPDATE_GITHUB_TOKEN= GITHUB_TOKEN= GH_TOKEN= curl -q --globoff --config - "$@"
- else
- UPDATE_GITHUB_TOKEN= GITHUB_TOKEN= GH_TOKEN= curl -q --globoff "$@"
- fi
- }
- # Get latest release version
- get_latest_version() {
- print_info "$(msg 'fetching_version')"
- LATEST_VERSION=$(github_api_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=$(github_api_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=$(github_api_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 "$@"
|