install.sh 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232
  1. #!/bin/bash
  2. #
  3. # Sub2API Installation Script
  4. # Sub2API 安装脚本
  5. # Usage: curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | bash
  6. #
  7. set -e
  8. # Bash 4+ is required for associative arrays used by the localized message table.
  9. # Keep this guard before any Bash 4-only syntax so older shells fail with a clear hint.
  10. if [ -z "${BASH_VERSION:-}" ]; then
  11. echo "Error: This installer must be run with Bash 4.0 or later." >&2
  12. echo "Please install Bash 4+ and run it with that interpreter." >&2
  13. exit 1
  14. fi
  15. BASH_MAJOR_VERSION="${BASH_VERSION%%.*}"
  16. if [ "$BASH_MAJOR_VERSION" -lt 4 ]; then
  17. echo "Error: Bash 4.0 or later is required. Current version: $BASH_VERSION" >&2
  18. echo "Please install Bash 4+ and retry with that interpreter." >&2
  19. exit 1
  20. fi
  21. # Colors
  22. RED='\033[0;31m'
  23. GREEN='\033[0;32m'
  24. YELLOW='\033[1;33m'
  25. BLUE='\033[0;34m'
  26. CYAN='\033[0;36m'
  27. NC='\033[0m' # No Color
  28. # Configuration
  29. GITHUB_REPO="Wei-Shaw/sub2api"
  30. INSTALL_DIR="/opt/sub2api"
  31. SERVICE_NAME="sub2api"
  32. SERVICE_USER="sub2api"
  33. CONFIG_DIR="/etc/sub2api"
  34. # Server configuration (will be set by user)
  35. SERVER_HOST="0.0.0.0"
  36. SERVER_PORT="8080"
  37. # Language (default: zh = Chinese)
  38. LANG_CHOICE="zh"
  39. # ============================================================
  40. # Language strings / 语言字符串
  41. # ============================================================
  42. # Chinese strings
  43. declare -A MSG_ZH=(
  44. # General
  45. ["info"]="信息"
  46. ["success"]="成功"
  47. ["warning"]="警告"
  48. ["error"]="错误"
  49. # Language selection
  50. ["select_lang"]="请选择语言 / Select language"
  51. ["lang_zh"]="中文"
  52. ["lang_en"]="English"
  53. ["enter_choice"]="请输入选择 (默认: 1)"
  54. # Installation
  55. ["install_title"]="Sub2API 安装脚本"
  56. ["run_as_root"]="请使用 root 权限运行 (使用 sudo)"
  57. ["detected_platform"]="检测到平台"
  58. ["unsupported_arch"]="不支持的架构"
  59. ["unsupported_os"]="不支持的操作系统"
  60. ["missing_deps"]="缺少依赖"
  61. ["install_deps_first"]="请先安装以下依赖"
  62. ["fetching_version"]="正在获取最新版本..."
  63. ["latest_version"]="最新版本"
  64. ["failed_get_version"]="获取最新版本失败"
  65. ["downloading"]="正在下载"
  66. ["download_failed"]="下载失败"
  67. ["verifying_checksum"]="正在校验文件..."
  68. ["checksum_verified"]="校验通过"
  69. ["checksum_failed"]="校验失败"
  70. ["checksum_not_found"]="无法验证校验和(checksums.txt 未找到)"
  71. ["extracting"]="正在解压..."
  72. ["binary_installed"]="二进制文件已安装到"
  73. ["user_exists"]="用户已存在"
  74. ["creating_user"]="正在创建系统用户"
  75. ["user_created"]="用户已创建"
  76. ["setting_up_dirs"]="正在设置目录..."
  77. ["dirs_configured"]="目录配置完成"
  78. ["installing_service"]="正在安装 systemd 服务..."
  79. ["service_installed"]="systemd 服务已安装"
  80. ["ready_for_setup"]="准备就绪,可以启动设置向导"
  81. # Completion
  82. ["install_complete"]="Sub2API 安装完成!"
  83. ["install_dir"]="安装目录"
  84. ["next_steps"]="后续步骤"
  85. ["step1_check_services"]="确保 PostgreSQL 和 Redis 正在运行:"
  86. ["step2_start_service"]="启动 Sub2API 服务:"
  87. ["step3_enable_autostart"]="设置开机自启:"
  88. ["step4_open_wizard"]="在浏览器中打开设置向导:"
  89. ["wizard_guide"]="设置向导将引导您完成:"
  90. ["wizard_db"]="数据库配置"
  91. ["wizard_redis"]="Redis 配置"
  92. ["wizard_admin"]="管理员账号创建"
  93. ["useful_commands"]="常用命令"
  94. ["cmd_status"]="查看状态"
  95. ["cmd_logs"]="查看日志"
  96. ["cmd_restart"]="重启服务"
  97. ["cmd_stop"]="停止服务"
  98. # Upgrade
  99. ["upgrading"]="正在升级 Sub2API..."
  100. ["current_version"]="当前版本"
  101. ["stopping_service"]="正在停止服务..."
  102. ["backup_created"]="备份已创建"
  103. ["starting_service"]="正在启动服务..."
  104. ["upgrade_complete"]="升级完成!"
  105. # Version install
  106. ["installing_version"]="正在安装指定版本"
  107. ["version_not_found"]="指定版本不存在"
  108. ["same_version"]="已经是该版本,无需操作"
  109. ["rollback_complete"]="版本回退完成!"
  110. ["install_version_complete"]="指定版本安装完成!"
  111. ["validating_version"]="正在验证版本..."
  112. ["available_versions"]="可用版本列表"
  113. ["fetching_versions"]="正在获取可用版本..."
  114. ["not_installed"]="Sub2API 尚未安装,请先执行全新安装"
  115. ["fresh_install_hint"]="用法"
  116. # Uninstall
  117. ["uninstall_confirm"]="这将从系统中移除 Sub2API。"
  118. ["are_you_sure"]="确定要继续吗?(y/N)"
  119. ["uninstall_cancelled"]="卸载已取消"
  120. ["removing_files"]="正在移除文件..."
  121. ["removing_install_dir"]="正在移除安装目录..."
  122. ["removing_user"]="正在移除用户..."
  123. ["config_not_removed"]="配置目录未被移除"
  124. ["remove_manually"]="如不再需要,请手动删除"
  125. ["removing_install_lock"]="正在移除安装锁文件..."
  126. ["install_lock_removed"]="安装锁文件已移除,重新安装时将进入设置向导"
  127. ["purge_prompt"]="是否同时删除配置目录?这将清除所有配置和数据 [y/N]: "
  128. ["removing_config_dir"]="正在移除配置目录..."
  129. ["uninstall_complete"]="Sub2API 已卸载"
  130. # Help
  131. ["usage"]="用法"
  132. ["cmd_none"]="(无参数)"
  133. ["cmd_install"]="安装 Sub2API"
  134. ["cmd_upgrade"]="升级到最新版本"
  135. ["cmd_uninstall"]="卸载 Sub2API"
  136. ["cmd_install_version"]="安装/回退到指定版本"
  137. ["cmd_list_versions"]="列出可用版本"
  138. ["opt_version"]="指定要安装的版本号 (例如: v1.0.0)"
  139. # Server configuration
  140. ["server_config_title"]="服务器配置"
  141. ["server_config_desc"]="配置 Sub2API 服务监听地址"
  142. ["server_host_prompt"]="服务器监听地址"
  143. ["server_host_hint"]="0.0.0.0 表示监听所有网卡,127.0.0.1 仅本地访问"
  144. ["server_port_prompt"]="服务器端口"
  145. ["server_port_hint"]="建议使用 1024-65535 之间的端口"
  146. ["server_config_summary"]="服务器配置"
  147. ["invalid_port"]="无效端口号,请输入 1-65535 之间的数字"
  148. # Service management
  149. ["starting_service"]="正在启动服务..."
  150. ["service_started"]="服务已启动"
  151. ["service_start_failed"]="服务启动失败,请检查日志"
  152. ["enabling_autostart"]="正在设置开机自启..."
  153. ["autostart_enabled"]="开机自启已启用"
  154. ["getting_public_ip"]="正在获取公网 IP..."
  155. ["public_ip_failed"]="无法获取公网 IP,使用本地 IP"
  156. )
  157. # English strings
  158. declare -A MSG_EN=(
  159. # General
  160. ["info"]="INFO"
  161. ["success"]="SUCCESS"
  162. ["warning"]="WARNING"
  163. ["error"]="ERROR"
  164. # Language selection
  165. ["select_lang"]="请选择语言 / Select language"
  166. ["lang_zh"]="中文"
  167. ["lang_en"]="English"
  168. ["enter_choice"]="Enter your choice (default: 1)"
  169. # Installation
  170. ["install_title"]="Sub2API Installation Script"
  171. ["run_as_root"]="Please run as root (use sudo)"
  172. ["detected_platform"]="Detected platform"
  173. ["unsupported_arch"]="Unsupported architecture"
  174. ["unsupported_os"]="Unsupported OS"
  175. ["missing_deps"]="Missing dependencies"
  176. ["install_deps_first"]="Please install them first"
  177. ["fetching_version"]="Fetching latest version..."
  178. ["latest_version"]="Latest version"
  179. ["failed_get_version"]="Failed to get latest version"
  180. ["downloading"]="Downloading"
  181. ["download_failed"]="Download failed"
  182. ["verifying_checksum"]="Verifying checksum..."
  183. ["checksum_verified"]="Checksum verified"
  184. ["checksum_failed"]="Checksum verification failed"
  185. ["checksum_not_found"]="Could not verify checksum (checksums.txt not found)"
  186. ["extracting"]="Extracting..."
  187. ["binary_installed"]="Binary installed to"
  188. ["user_exists"]="User already exists"
  189. ["creating_user"]="Creating system user"
  190. ["user_created"]="User created"
  191. ["setting_up_dirs"]="Setting up directories..."
  192. ["dirs_configured"]="Directories configured"
  193. ["installing_service"]="Installing systemd service..."
  194. ["service_installed"]="Systemd service installed"
  195. ["ready_for_setup"]="Ready for Setup Wizard"
  196. # Completion
  197. ["install_complete"]="Sub2API installation completed!"
  198. ["install_dir"]="Installation directory"
  199. ["next_steps"]="NEXT STEPS"
  200. ["step1_check_services"]="Make sure PostgreSQL and Redis are running:"
  201. ["step2_start_service"]="Start Sub2API service:"
  202. ["step3_enable_autostart"]="Enable auto-start on boot:"
  203. ["step4_open_wizard"]="Open the Setup Wizard in your browser:"
  204. ["wizard_guide"]="The Setup Wizard will guide you through:"
  205. ["wizard_db"]="Database configuration"
  206. ["wizard_redis"]="Redis configuration"
  207. ["wizard_admin"]="Admin account creation"
  208. ["useful_commands"]="USEFUL COMMANDS"
  209. ["cmd_status"]="Check status"
  210. ["cmd_logs"]="View logs"
  211. ["cmd_restart"]="Restart"
  212. ["cmd_stop"]="Stop"
  213. # Upgrade
  214. ["upgrading"]="Upgrading Sub2API..."
  215. ["current_version"]="Current version"
  216. ["stopping_service"]="Stopping service..."
  217. ["backup_created"]="Backup created"
  218. ["starting_service"]="Starting service..."
  219. ["upgrade_complete"]="Upgrade completed!"
  220. # Version install
  221. ["installing_version"]="Installing specified version"
  222. ["version_not_found"]="Specified version not found"
  223. ["same_version"]="Already at this version, no action needed"
  224. ["rollback_complete"]="Version rollback completed!"
  225. ["install_version_complete"]="Specified version installed!"
  226. ["validating_version"]="Validating version..."
  227. ["available_versions"]="Available versions"
  228. ["fetching_versions"]="Fetching available versions..."
  229. ["not_installed"]="Sub2API is not installed. Please run a fresh install first"
  230. ["fresh_install_hint"]="Usage"
  231. # Uninstall
  232. ["uninstall_confirm"]="This will remove Sub2API from your system."
  233. ["are_you_sure"]="Are you sure? (y/N)"
  234. ["uninstall_cancelled"]="Uninstall cancelled"
  235. ["removing_files"]="Removing files..."
  236. ["removing_install_dir"]="Removing installation directory..."
  237. ["removing_user"]="Removing user..."
  238. ["config_not_removed"]="Config directory was NOT removed."
  239. ["remove_manually"]="Remove it manually if you no longer need it."
  240. ["removing_install_lock"]="Removing install lock file..."
  241. ["install_lock_removed"]="Install lock removed. Setup wizard will appear on next install."
  242. ["purge_prompt"]="Also remove config directory? This will delete all config and data [y/N]: "
  243. ["removing_config_dir"]="Removing config directory..."
  244. ["uninstall_complete"]="Sub2API has been uninstalled"
  245. # Help
  246. ["usage"]="Usage"
  247. ["cmd_none"]="(none)"
  248. ["cmd_install"]="Install Sub2API"
  249. ["cmd_upgrade"]="Upgrade to the latest version"
  250. ["cmd_uninstall"]="Remove Sub2API"
  251. ["cmd_install_version"]="Install/rollback to a specific version"
  252. ["cmd_list_versions"]="List available versions"
  253. ["opt_version"]="Specify version to install (e.g., v1.0.0)"
  254. # Server configuration
  255. ["server_config_title"]="Server Configuration"
  256. ["server_config_desc"]="Configure Sub2API server listen address"
  257. ["server_host_prompt"]="Server listen address"
  258. ["server_host_hint"]="0.0.0.0 listens on all interfaces, 127.0.0.1 for local only"
  259. ["server_port_prompt"]="Server port"
  260. ["server_port_hint"]="Recommended range: 1024-65535"
  261. ["server_config_summary"]="Server configuration"
  262. ["invalid_port"]="Invalid port number, please enter a number between 1-65535"
  263. # Service management
  264. ["starting_service"]="Starting service..."
  265. ["service_started"]="Service started"
  266. ["service_start_failed"]="Service failed to start, please check logs"
  267. ["enabling_autostart"]="Enabling auto-start on boot..."
  268. ["autostart_enabled"]="Auto-start enabled"
  269. ["getting_public_ip"]="Getting public IP..."
  270. ["public_ip_failed"]="Failed to get public IP, using local IP"
  271. )
  272. # Get message based on current language
  273. msg() {
  274. local key="$1"
  275. if [ "$LANG_CHOICE" = "en" ]; then
  276. echo "${MSG_EN[$key]}"
  277. else
  278. echo "${MSG_ZH[$key]}"
  279. fi
  280. }
  281. # Print functions
  282. print_info() {
  283. echo -e "${BLUE}[$(msg 'info')]${NC} $1"
  284. }
  285. print_success() {
  286. echo -e "${GREEN}[$(msg 'success')]${NC} $1"
  287. }
  288. print_warning() {
  289. echo -e "${YELLOW}[$(msg 'warning')]${NC} $1"
  290. }
  291. print_error() {
  292. echo -e "${RED}[$(msg 'error')]${NC} $1"
  293. }
  294. # Check if running interactively (can access terminal)
  295. # When piped (curl | bash), stdin is not a terminal, but /dev/tty may still be available
  296. is_interactive() {
  297. # Check if /dev/tty is available (works even when piped)
  298. [ -e /dev/tty ] && [ -r /dev/tty ] && [ -w /dev/tty ]
  299. }
  300. # Select language
  301. select_language() {
  302. # If not interactive (piped), use default language
  303. if ! is_interactive; then
  304. LANG_CHOICE="zh"
  305. return
  306. fi
  307. echo ""
  308. echo -e "${CYAN}=============================================="
  309. echo " $(msg 'select_lang')"
  310. echo "==============================================${NC}"
  311. echo ""
  312. echo " 1) $(msg 'lang_zh') (默认/default)"
  313. echo " 2) $(msg 'lang_en')"
  314. echo ""
  315. read -p "$(msg 'enter_choice'): " lang_input < /dev/tty
  316. case "$lang_input" in
  317. 2|en|EN|english|English)
  318. LANG_CHOICE="en"
  319. ;;
  320. *)
  321. LANG_CHOICE="zh"
  322. ;;
  323. esac
  324. echo ""
  325. }
  326. # Validate port number
  327. validate_port() {
  328. local port="$1"
  329. if [[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ]; then
  330. return 0
  331. fi
  332. return 1
  333. }
  334. # Configure server settings
  335. configure_server() {
  336. # If not interactive (piped), use default settings
  337. if ! is_interactive; then
  338. print_info "$(msg 'server_config_summary'): ${SERVER_HOST}:${SERVER_PORT} (default)"
  339. return
  340. fi
  341. echo ""
  342. echo -e "${CYAN}=============================================="
  343. echo " $(msg 'server_config_title')"
  344. echo "==============================================${NC}"
  345. echo ""
  346. echo -e "${BLUE}$(msg 'server_config_desc')${NC}"
  347. echo ""
  348. # Server host
  349. echo -e "${YELLOW}$(msg 'server_host_hint')${NC}"
  350. read -p "$(msg 'server_host_prompt') [${SERVER_HOST}]: " input_host < /dev/tty
  351. if [ -n "$input_host" ]; then
  352. SERVER_HOST="$input_host"
  353. fi
  354. echo ""
  355. # Server port
  356. echo -e "${YELLOW}$(msg 'server_port_hint')${NC}"
  357. while true; do
  358. read -p "$(msg 'server_port_prompt') [${SERVER_PORT}]: " input_port < /dev/tty
  359. if [ -z "$input_port" ]; then
  360. # Use default
  361. break
  362. elif validate_port "$input_port"; then
  363. SERVER_PORT="$input_port"
  364. break
  365. else
  366. print_error "$(msg 'invalid_port')"
  367. fi
  368. done
  369. echo ""
  370. print_info "$(msg 'server_config_summary'): ${SERVER_HOST}:${SERVER_PORT}"
  371. echo ""
  372. }
  373. # Check if running as root
  374. check_root() {
  375. # Use 'id -u' instead of $EUID for better compatibility
  376. # $EUID may not work reliably when script is piped to bash
  377. if [ "$(id -u)" -ne 0 ]; then
  378. print_error "$(msg 'run_as_root')"
  379. exit 1
  380. fi
  381. }
  382. # Detect OS and architecture
  383. detect_platform() {
  384. OS=$(uname -s | tr '[:upper:]' '[:lower:]')
  385. ARCH=$(uname -m)
  386. case "$ARCH" in
  387. x86_64)
  388. ARCH="amd64"
  389. ;;
  390. aarch64|arm64)
  391. ARCH="arm64"
  392. ;;
  393. *)
  394. print_error "$(msg 'unsupported_arch'): $ARCH"
  395. exit 1
  396. ;;
  397. esac
  398. case "$OS" in
  399. linux)
  400. OS="linux"
  401. ;;
  402. darwin)
  403. OS="darwin"
  404. ;;
  405. *)
  406. print_error "$(msg 'unsupported_os'): $OS"
  407. exit 1
  408. ;;
  409. esac
  410. print_info "$(msg 'detected_platform'): ${OS}_${ARCH}"
  411. }
  412. # Check dependencies
  413. check_dependencies() {
  414. local missing=()
  415. if ! command -v curl &> /dev/null; then
  416. missing+=("curl")
  417. fi
  418. if ! command -v tar &> /dev/null; then
  419. missing+=("tar")
  420. fi
  421. if [ ${#missing[@]} -gt 0 ]; then
  422. print_error "$(msg 'missing_deps'): ${missing[*]}"
  423. print_info "$(msg 'install_deps_first')"
  424. exit 1
  425. fi
  426. }
  427. # Authenticate only GitHub REST API requests. Release asset downloads must stay anonymous.
  428. github_api_curl() {
  429. local arg
  430. local expect_value=false
  431. local url
  432. if [ "$#" -lt 1 ]; then
  433. echo "github_api_curl requires exactly one GitHub API URL" >&2
  434. return 2
  435. fi
  436. url="${!#}"
  437. # Keep authenticated invocations constrained to the options used below. In
  438. # particular, curl config, --url, and --next could add another destination.
  439. for arg in "${@:1:$#-1}"; do
  440. if [ "$expect_value" = true ]; then
  441. expect_value=false
  442. continue
  443. fi
  444. case "$arg" in
  445. -s|--silent)
  446. ;;
  447. --connect-timeout|--max-time|-o|--output|-w|--write-out)
  448. expect_value=true
  449. ;;
  450. *)
  451. echo "Unsafe github_api_curl argument: $arg" >&2
  452. return 2
  453. ;;
  454. esac
  455. done
  456. if [ "$expect_value" = true ] || [[ "$url" != https://api.github.com/* ]]; then
  457. echo "github_api_curl requires exactly one GitHub API URL" >&2
  458. return 2
  459. fi
  460. if [ -n "${UPDATE_GITHUB_TOKEN:-}" ]; then
  461. if [[ "$UPDATE_GITHUB_TOKEN" == *$'\n'* || "$UPDATE_GITHUB_TOKEN" == *$'\r'* || "$UPDATE_GITHUB_TOKEN" == *'"'* || "$UPDATE_GITHUB_TOKEN" == *'\'* ]]; then
  462. echo "UPDATE_GITHUB_TOKEN contains unsupported characters" >&2
  463. return 2
  464. fi
  465. printf 'header = "Authorization: Bearer %s"\n' "$UPDATE_GITHUB_TOKEN" | UPDATE_GITHUB_TOKEN= GITHUB_TOKEN= GH_TOKEN= curl -q --globoff --config - "$@"
  466. else
  467. UPDATE_GITHUB_TOKEN= GITHUB_TOKEN= GH_TOKEN= curl -q --globoff "$@"
  468. fi
  469. }
  470. # Get latest release version
  471. get_latest_version() {
  472. print_info "$(msg 'fetching_version')"
  473. 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/')
  474. if [ -z "$LATEST_VERSION" ]; then
  475. print_error "$(msg 'failed_get_version')"
  476. print_info "Please check your network connection or try again later."
  477. exit 1
  478. fi
  479. print_info "$(msg 'latest_version'): $LATEST_VERSION"
  480. }
  481. # List available versions
  482. list_versions() {
  483. print_info "$(msg 'fetching_versions')"
  484. local versions
  485. 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)
  486. if [ -z "$versions" ]; then
  487. print_error "$(msg 'failed_get_version')"
  488. print_info "Please check your network connection or try again later."
  489. exit 1
  490. fi
  491. echo ""
  492. echo "$(msg 'available_versions'):"
  493. echo "----------------------------------------"
  494. echo "$versions" | while read -r version; do
  495. echo " $version"
  496. done
  497. echo "----------------------------------------"
  498. echo ""
  499. }
  500. # Validate if a version exists
  501. validate_version() {
  502. local version="$1"
  503. # Check for empty version
  504. if [ -z "$version" ]; then
  505. print_error "$(msg 'opt_version')" >&2
  506. exit 1
  507. fi
  508. # Ensure version starts with 'v'
  509. if [[ ! "$version" =~ ^v ]]; then
  510. version="v$version"
  511. fi
  512. print_info "$(msg 'validating_version') $version" >&2
  513. # Check if the release exists
  514. local http_code
  515. 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)
  516. # Check for network errors (empty or non-numeric response)
  517. if [ -z "$http_code" ] || ! [[ "$http_code" =~ ^[0-9]+$ ]]; then
  518. print_error "Network error: Failed to connect to GitHub API" >&2
  519. exit 1
  520. fi
  521. if [ "$http_code" != "200" ]; then
  522. print_error "$(msg 'version_not_found'): $version" >&2
  523. echo "" >&2
  524. list_versions >&2
  525. exit 1
  526. fi
  527. # Return the normalized version (to stdout)
  528. echo "$version"
  529. }
  530. # Get current installed version
  531. get_current_version() {
  532. if [ -f "$INSTALL_DIR/sub2api" ]; then
  533. # Use grep -E for better compatibility (works on macOS and Linux)
  534. "$INSTALL_DIR/sub2api" --version 2>/dev/null | grep -oE 'v?[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown"
  535. else
  536. echo "not_installed"
  537. fi
  538. }
  539. # Download and extract
  540. download_and_extract() {
  541. local version_num=${LATEST_VERSION#v}
  542. local archive_name="sub2api_${version_num}_${OS}_${ARCH}.tar.gz"
  543. local download_url="https://github.com/${GITHUB_REPO}/releases/download/${LATEST_VERSION}/${archive_name}"
  544. local checksum_url="https://github.com/${GITHUB_REPO}/releases/download/${LATEST_VERSION}/checksums.txt"
  545. print_info "$(msg 'downloading') ${archive_name}..."
  546. # Create temp directory
  547. TEMP_DIR=$(mktemp -d)
  548. trap "rm -rf $TEMP_DIR" EXIT
  549. # Download archive
  550. if ! curl -sL "$download_url" -o "$TEMP_DIR/$archive_name"; then
  551. print_error "$(msg 'download_failed')"
  552. exit 1
  553. fi
  554. # Download and verify checksum
  555. print_info "$(msg 'verifying_checksum')"
  556. if curl -sL "$checksum_url" -o "$TEMP_DIR/checksums.txt" 2>/dev/null; then
  557. local expected_checksum=$(grep "$archive_name" "$TEMP_DIR/checksums.txt" | awk '{print $1}')
  558. local actual_checksum=$(sha256sum "$TEMP_DIR/$archive_name" | awk '{print $1}')
  559. if [ "$expected_checksum" != "$actual_checksum" ]; then
  560. print_error "$(msg 'checksum_failed')"
  561. print_error "Expected: $expected_checksum"
  562. print_error "Actual: $actual_checksum"
  563. exit 1
  564. fi
  565. print_success "$(msg 'checksum_verified')"
  566. else
  567. print_warning "$(msg 'checksum_not_found')"
  568. fi
  569. # Extract
  570. print_info "$(msg 'extracting')"
  571. tar -xzf "$TEMP_DIR/$archive_name" -C "$TEMP_DIR"
  572. # Create install directory
  573. mkdir -p "$INSTALL_DIR"
  574. # Copy binary
  575. cp "$TEMP_DIR/sub2api" "$INSTALL_DIR/sub2api"
  576. chmod +x "$INSTALL_DIR/sub2api"
  577. # Copy deploy files if they exist in the archive
  578. if [ -d "$TEMP_DIR/deploy" ]; then
  579. cp -r "$TEMP_DIR/deploy/"* "$INSTALL_DIR/" 2>/dev/null || true
  580. fi
  581. print_success "$(msg 'binary_installed') $INSTALL_DIR/sub2api"
  582. }
  583. # Create system user
  584. create_user() {
  585. if id "$SERVICE_USER" &>/dev/null; then
  586. print_info "$(msg 'user_exists'): $SERVICE_USER"
  587. # Fix: Ensure existing user has /bin/sh shell for sudo to work
  588. # Previous versions used /bin/false which prevents sudo execution
  589. local current_shell
  590. current_shell=$(getent passwd "$SERVICE_USER" 2>/dev/null | cut -d: -f7)
  591. if [ "$current_shell" = "/bin/false" ] || [ "$current_shell" = "/sbin/nologin" ]; then
  592. print_info "Fixing user shell for sudo compatibility..."
  593. if usermod -s /bin/sh "$SERVICE_USER" 2>/dev/null; then
  594. print_success "User shell updated to /bin/sh"
  595. else
  596. print_warning "Failed to update user shell. Service restart may not work automatically."
  597. print_warning "Manual fix: sudo usermod -s /bin/sh $SERVICE_USER"
  598. fi
  599. fi
  600. else
  601. print_info "$(msg 'creating_user') $SERVICE_USER..."
  602. # Use /bin/sh instead of /bin/false to allow sudo execution
  603. # The user still cannot login interactively (no password set)
  604. useradd -r -s /bin/sh -d "$INSTALL_DIR" "$SERVICE_USER"
  605. print_success "$(msg 'user_created')"
  606. fi
  607. }
  608. # Setup directories and permissions
  609. setup_directories() {
  610. print_info "$(msg 'setting_up_dirs')"
  611. # Create directories
  612. mkdir -p "$INSTALL_DIR"
  613. mkdir -p "$INSTALL_DIR/data"
  614. mkdir -p "$CONFIG_DIR"
  615. # Set ownership
  616. chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR"
  617. chown -R "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR"
  618. print_success "$(msg 'dirs_configured')"
  619. }
  620. # Install systemd service
  621. install_service() {
  622. print_info "$(msg 'installing_service')"
  623. # Create service file with configured host and port
  624. cat > /etc/systemd/system/sub2api.service << EOF
  625. [Unit]
  626. Description=Sub2API - AI API Gateway Platform
  627. Documentation=https://github.com/Wei-Shaw/sub2api
  628. After=network.target postgresql.service redis.service
  629. Wants=postgresql.service redis.service
  630. [Service]
  631. Type=simple
  632. User=sub2api
  633. Group=sub2api
  634. WorkingDirectory=/opt/sub2api
  635. ExecStart=/opt/sub2api/sub2api
  636. Restart=always
  637. RestartSec=5
  638. StandardOutput=journal
  639. StandardError=journal
  640. SyslogIdentifier=sub2api
  641. # Security hardening
  642. NoNewPrivileges=true
  643. ProtectSystem=strict
  644. ProtectHome=true
  645. PrivateTmp=true
  646. ReadWritePaths=/opt/sub2api
  647. # Environment - Server configuration
  648. Environment=GIN_MODE=release
  649. Environment=SERVER_HOST=${SERVER_HOST}
  650. Environment=SERVER_PORT=${SERVER_PORT}
  651. [Install]
  652. WantedBy=multi-user.target
  653. EOF
  654. # Reload systemd
  655. systemctl daemon-reload
  656. print_success "$(msg 'service_installed')"
  657. }
  658. # Prepare for setup wizard (no config file needed - setup wizard will create it)
  659. prepare_for_setup() {
  660. print_success "$(msg 'ready_for_setup')"
  661. }
  662. # Get public IP address
  663. get_public_ip() {
  664. print_info "$(msg 'getting_public_ip')"
  665. # Try to get public IP from ipinfo.io
  666. local response
  667. response=$(curl -s --connect-timeout 5 --max-time 10 "https://ipinfo.io/json" 2>/dev/null)
  668. if [ -n "$response" ]; then
  669. # Extract IP from JSON response using grep and sed (no jq dependency)
  670. PUBLIC_IP=$(echo "$response" | grep -o '"ip": *"[^"]*"' | sed 's/"ip": *"\([^"]*\)"/\1/')
  671. if [ -n "$PUBLIC_IP" ]; then
  672. print_success "Public IP: $PUBLIC_IP"
  673. return 0
  674. fi
  675. fi
  676. # Fallback to local IP
  677. print_warning "$(msg 'public_ip_failed')"
  678. PUBLIC_IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "YOUR_SERVER_IP")
  679. return 1
  680. }
  681. # Start service
  682. start_service() {
  683. print_info "$(msg 'starting_service')"
  684. if systemctl start sub2api; then
  685. print_success "$(msg 'service_started')"
  686. return 0
  687. else
  688. print_error "$(msg 'service_start_failed')"
  689. print_info "sudo journalctl -u sub2api -n 50"
  690. return 1
  691. fi
  692. }
  693. # Enable service auto-start
  694. enable_autostart() {
  695. print_info "$(msg 'enabling_autostart')"
  696. if systemctl enable sub2api 2>/dev/null; then
  697. print_success "$(msg 'autostart_enabled')"
  698. return 0
  699. else
  700. print_warning "Failed to enable auto-start"
  701. return 1
  702. fi
  703. }
  704. # Print completion message
  705. print_completion() {
  706. # Use PUBLIC_IP which was set by get_public_ip()
  707. # Determine display address
  708. local display_host="${PUBLIC_IP:-YOUR_SERVER_IP}"
  709. if [ "$SERVER_HOST" = "127.0.0.1" ]; then
  710. display_host="127.0.0.1"
  711. fi
  712. echo ""
  713. echo "=============================================="
  714. print_success "$(msg 'install_complete')"
  715. echo "=============================================="
  716. echo ""
  717. echo "$(msg 'install_dir'): $INSTALL_DIR"
  718. echo "$(msg 'server_config_summary'): ${SERVER_HOST}:${SERVER_PORT}"
  719. echo ""
  720. echo "=============================================="
  721. echo " $(msg 'step4_open_wizard')"
  722. echo "=============================================="
  723. echo ""
  724. print_info " http://${display_host}:${SERVER_PORT}"
  725. echo ""
  726. echo " $(msg 'wizard_guide')"
  727. echo " - $(msg 'wizard_db')"
  728. echo " - $(msg 'wizard_redis')"
  729. echo " - $(msg 'wizard_admin')"
  730. echo ""
  731. echo "=============================================="
  732. echo " $(msg 'useful_commands')"
  733. echo "=============================================="
  734. echo ""
  735. echo " $(msg 'cmd_status'): sudo systemctl status sub2api"
  736. echo " $(msg 'cmd_logs'): sudo journalctl -u sub2api -f"
  737. echo " $(msg 'cmd_restart'): sudo systemctl restart sub2api"
  738. echo " $(msg 'cmd_stop'): sudo systemctl stop sub2api"
  739. echo ""
  740. echo "=============================================="
  741. }
  742. # Upgrade function
  743. upgrade() {
  744. # Check if Sub2API is installed
  745. if [ ! -f "$INSTALL_DIR/sub2api" ]; then
  746. print_error "$(msg 'not_installed')"
  747. print_info "$(msg 'fresh_install_hint'): $0 install"
  748. exit 1
  749. fi
  750. print_info "$(msg 'upgrading')"
  751. # Get current version
  752. CURRENT_VERSION=$("$INSTALL_DIR/sub2api" --version 2>/dev/null | grep -oE 'v?[0-9]+\.[0-9]+\.[0-9]+' || echo "unknown")
  753. print_info "$(msg 'current_version'): $CURRENT_VERSION"
  754. # Stop service
  755. if systemctl is-active --quiet sub2api; then
  756. print_info "$(msg 'stopping_service')"
  757. systemctl stop sub2api
  758. fi
  759. # Backup current binary
  760. cp "$INSTALL_DIR/sub2api" "$INSTALL_DIR/sub2api.backup"
  761. print_info "$(msg 'backup_created'): $INSTALL_DIR/sub2api.backup"
  762. # Download and install new version
  763. get_latest_version
  764. download_and_extract
  765. # Set permissions
  766. chown "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR/sub2api"
  767. # Start service
  768. print_info "$(msg 'starting_service')"
  769. systemctl start sub2api
  770. print_success "$(msg 'upgrade_complete')"
  771. }
  772. # Install specific version (for upgrade or rollback)
  773. # Requires: Sub2API must already be installed
  774. install_version() {
  775. local target_version="$1"
  776. # Check if Sub2API is installed
  777. if [ ! -f "$INSTALL_DIR/sub2api" ]; then
  778. print_error "$(msg 'not_installed')"
  779. print_info "$(msg 'fresh_install_hint'): $0 install -v $target_version"
  780. exit 1
  781. fi
  782. # Validate and normalize version
  783. target_version=$(validate_version "$target_version")
  784. print_info "$(msg 'installing_version'): $target_version"
  785. # Get current version
  786. local current_version
  787. current_version=$(get_current_version)
  788. print_info "$(msg 'current_version'): $current_version"
  789. # Check if same version
  790. if [ "$current_version" = "$target_version" ] || [ "$current_version" = "${target_version#v}" ]; then
  791. print_warning "$(msg 'same_version')"
  792. exit 0
  793. fi
  794. # Stop service if running
  795. if systemctl is-active --quiet sub2api; then
  796. print_info "$(msg 'stopping_service')"
  797. systemctl stop sub2api
  798. fi
  799. # Backup current binary (for potential recovery)
  800. if [ -f "$INSTALL_DIR/sub2api" ]; then
  801. local backup_name
  802. if [ "$current_version" != "unknown" ] && [ "$current_version" != "not_installed" ]; then
  803. backup_name="sub2api.backup.${current_version}"
  804. else
  805. backup_name="sub2api.backup.$(date +%Y%m%d%H%M%S)"
  806. fi
  807. cp "$INSTALL_DIR/sub2api" "$INSTALL_DIR/$backup_name"
  808. print_info "$(msg 'backup_created'): $INSTALL_DIR/$backup_name"
  809. fi
  810. # Set LATEST_VERSION to the target version for download_and_extract
  811. LATEST_VERSION="$target_version"
  812. # Download and install
  813. download_and_extract
  814. # Set permissions
  815. chown "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR/sub2api"
  816. # Start service
  817. print_info "$(msg 'starting_service')"
  818. if systemctl start sub2api; then
  819. print_success "$(msg 'service_started')"
  820. else
  821. print_error "$(msg 'service_start_failed')"
  822. print_info "sudo journalctl -u sub2api -n 50"
  823. fi
  824. # Print completion message
  825. local new_version
  826. new_version=$(get_current_version)
  827. echo ""
  828. echo "=============================================="
  829. print_success "$(msg 'install_version_complete')"
  830. echo "=============================================="
  831. echo ""
  832. echo " $(msg 'current_version'): $new_version"
  833. echo ""
  834. }
  835. # Uninstall function
  836. uninstall() {
  837. print_warning "$(msg 'uninstall_confirm')"
  838. # If not interactive (piped), require -y flag or skip confirmation
  839. if ! is_interactive; then
  840. if [ "${FORCE_YES:-}" != "true" ]; then
  841. print_error "Non-interactive mode detected. Use 'curl ... | bash -s -- uninstall -y' to confirm."
  842. exit 1
  843. fi
  844. else
  845. read -p "$(msg 'are_you_sure') " -n 1 -r < /dev/tty
  846. echo
  847. if [[ ! $REPLY =~ ^[Yy]$ ]]; then
  848. print_info "$(msg 'uninstall_cancelled')"
  849. exit 0
  850. fi
  851. fi
  852. print_info "$(msg 'stopping_service')"
  853. systemctl stop sub2api 2>/dev/null || true
  854. systemctl disable sub2api 2>/dev/null || true
  855. print_info "$(msg 'removing_files')"
  856. rm -f /etc/systemd/system/sub2api.service
  857. systemctl daemon-reload
  858. print_info "$(msg 'removing_install_dir')"
  859. rm -rf "$INSTALL_DIR"
  860. print_info "$(msg 'removing_user')"
  861. userdel "$SERVICE_USER" 2>/dev/null || true
  862. # Remove install lock file (.installed) to allow fresh setup on reinstall
  863. print_info "$(msg 'removing_install_lock')"
  864. rm -f "$CONFIG_DIR/.installed" 2>/dev/null || true
  865. rm -f "$INSTALL_DIR/.installed" 2>/dev/null || true
  866. print_success "$(msg 'install_lock_removed')"
  867. # Ask about config directory removal (interactive mode only)
  868. local remove_config=false
  869. if [ "${PURGE:-}" = "true" ]; then
  870. remove_config=true
  871. elif is_interactive; then
  872. read -p "$(msg 'purge_prompt')" -n 1 -r < /dev/tty
  873. echo
  874. if [[ $REPLY =~ ^[Yy]$ ]]; then
  875. remove_config=true
  876. fi
  877. fi
  878. if [ "$remove_config" = true ]; then
  879. print_info "$(msg 'removing_config_dir')"
  880. rm -rf "$CONFIG_DIR"
  881. else
  882. print_warning "$(msg 'config_not_removed'): $CONFIG_DIR"
  883. print_warning "$(msg 'remove_manually')"
  884. fi
  885. print_success "$(msg 'uninstall_complete')"
  886. }
  887. # Main
  888. main() {
  889. # Parse flags first
  890. local target_version=""
  891. local positional_args=()
  892. while [[ $# -gt 0 ]]; do
  893. case "$1" in
  894. -y|--yes)
  895. FORCE_YES="true"
  896. shift
  897. ;;
  898. --purge)
  899. PURGE="true"
  900. shift
  901. ;;
  902. -v|--version)
  903. if [ -n "${2:-}" ] && [[ ! "$2" =~ ^- ]]; then
  904. target_version="$2"
  905. shift 2
  906. else
  907. echo "Error: --version requires a version argument"
  908. exit 1
  909. fi
  910. ;;
  911. --version=*)
  912. target_version="${1#*=}"
  913. if [ -z "$target_version" ]; then
  914. echo "Error: --version requires a version argument"
  915. exit 1
  916. fi
  917. shift
  918. ;;
  919. *)
  920. positional_args+=("$1")
  921. shift
  922. ;;
  923. esac
  924. done
  925. # Restore positional arguments
  926. set -- "${positional_args[@]}"
  927. # Select language first
  928. select_language
  929. echo ""
  930. echo "=============================================="
  931. echo " $(msg 'install_title')"
  932. echo "=============================================="
  933. echo ""
  934. # Parse commands
  935. case "${1:-}" in
  936. upgrade|update)
  937. check_root
  938. detect_platform
  939. check_dependencies
  940. if [ -n "$target_version" ]; then
  941. # Upgrade to specific version
  942. install_version "$target_version"
  943. else
  944. # Upgrade to latest
  945. upgrade
  946. fi
  947. exit 0
  948. ;;
  949. install)
  950. # Install with optional version
  951. check_root
  952. detect_platform
  953. check_dependencies
  954. if [ -n "$target_version" ]; then
  955. # Install specific version (fresh install or rollback)
  956. if [ -f "$INSTALL_DIR/sub2api" ]; then
  957. # Already installed, treat as version change
  958. install_version "$target_version"
  959. else
  960. # Fresh install with specific version
  961. configure_server
  962. LATEST_VERSION=$(validate_version "$target_version")
  963. download_and_extract
  964. create_user
  965. setup_directories
  966. install_service
  967. prepare_for_setup
  968. get_public_ip
  969. start_service
  970. enable_autostart
  971. print_completion
  972. fi
  973. else
  974. # Fresh install with latest version
  975. configure_server
  976. get_latest_version
  977. download_and_extract
  978. create_user
  979. setup_directories
  980. install_service
  981. prepare_for_setup
  982. get_public_ip
  983. start_service
  984. enable_autostart
  985. print_completion
  986. fi
  987. exit 0
  988. ;;
  989. rollback)
  990. # Rollback to a specific version (alias for install with version)
  991. if [ -z "$target_version" ] && [ -n "${2:-}" ]; then
  992. target_version="$2"
  993. fi
  994. if [ -z "$target_version" ]; then
  995. print_error "$(msg 'opt_version')"
  996. echo ""
  997. echo "Usage: $0 rollback -v <version>"
  998. echo " $0 rollback <version>"
  999. echo ""
  1000. list_versions
  1001. exit 1
  1002. fi
  1003. check_root
  1004. detect_platform
  1005. check_dependencies
  1006. install_version "$target_version"
  1007. exit 0
  1008. ;;
  1009. list-versions|versions)
  1010. list_versions
  1011. exit 0
  1012. ;;
  1013. uninstall|remove)
  1014. check_root
  1015. uninstall
  1016. exit 0
  1017. ;;
  1018. --help|-h)
  1019. echo "$(msg 'usage'): $0 [command] [options]"
  1020. echo ""
  1021. echo "Commands:"
  1022. echo " $(msg 'cmd_none') $(msg 'cmd_install')"
  1023. echo " install $(msg 'cmd_install')"
  1024. echo " upgrade $(msg 'cmd_upgrade')"
  1025. echo " rollback <version> $(msg 'cmd_install_version')"
  1026. echo " list-versions $(msg 'cmd_list_versions')"
  1027. echo " uninstall $(msg 'cmd_uninstall')"
  1028. echo ""
  1029. echo "Options:"
  1030. echo " -v, --version <ver> $(msg 'opt_version')"
  1031. echo " -y, --yes Skip confirmation prompts (for uninstall)"
  1032. echo ""
  1033. echo "Examples:"
  1034. echo " $0 # Install latest version"
  1035. echo " $0 install -v v0.1.0 # Install specific version"
  1036. echo " $0 upgrade # Upgrade to latest"
  1037. echo " $0 upgrade -v v0.2.0 # Upgrade to specific version"
  1038. echo " $0 rollback v0.1.0 # Rollback to v0.1.0"
  1039. echo " $0 list-versions # List available versions"
  1040. echo ""
  1041. exit 0
  1042. ;;
  1043. esac
  1044. # Default: Fresh install with latest version
  1045. check_root
  1046. detect_platform
  1047. check_dependencies
  1048. if [ -n "$target_version" ]; then
  1049. # Install specific version
  1050. if [ -f "$INSTALL_DIR/sub2api" ]; then
  1051. install_version "$target_version"
  1052. else
  1053. configure_server
  1054. LATEST_VERSION=$(validate_version "$target_version")
  1055. download_and_extract
  1056. create_user
  1057. setup_directories
  1058. install_service
  1059. prepare_for_setup
  1060. get_public_ip
  1061. start_service
  1062. enable_autostart
  1063. print_completion
  1064. fi
  1065. else
  1066. # Install latest version
  1067. configure_server
  1068. get_latest_version
  1069. download_and_extract
  1070. create_user
  1071. setup_directories
  1072. install_service
  1073. prepare_for_setup
  1074. get_public_ip
  1075. start_service
  1076. enable_autostart
  1077. print_completion
  1078. fi
  1079. }
  1080. main "$@"