File size: 37,873 Bytes
8059bf0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 | #!/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 "$@"
|