#!/usr/bin/env bash set -euo pipefail ############################################# # GLOBAL VARIABLES ############################################# # Installation settings DEFAULT_INSTALL_DIR="/Users/Shared/TorrServer" DEFAULT_SERVICE_NAME="torrserver" DEFAULT_PORT="8090" # Runtime variables dirInstall="${DEFAULT_INSTALL_DIR}" serviceName="${DEFAULT_SERVICE_NAME}" scriptname=$(basename "$(test -L "$0" && readlink "$0" || echo "$0")") # Flags SILENT_MODE=0 USE_USER_LAUNCHAGENT=0 USER_PROMPTED=0 # Command-line state parsedCommand="" specificVersion="" downgradeRelease="" # Service configuration servicePort="" isAuth="" isRdb="" isLog="" isAuthUser="" isAuthPass="" sysPath="" # Constants readonly REPO_URL="https://github.com/YouROK/TorrServer" readonly REPO_API_URL="https://api.github.com/repos/YouROK/TorrServer" readonly VERSION_PREFIX="MatriX" readonly BINARY_NAME_PREFIX="TorrServer-darwin" # Color support getColorCode() { case "$1" in black) echo 0 ;; red) echo 1 ;; green) echo 2 ;; yellow) echo 3 ;; blue) echo 4 ;; magenta) echo 5 ;; cyan) echo 6 ;; white) echo 7 ;; *) echo 7 ;; # default to white esac } supports_color_output=0 if command -v tput >/dev/null 2>&1 && [[ -t 1 ]]; then if [[ $(tput colors 2>/dev/null) -ge 8 ]]; then supports_color_output=1 fi fi # Language lang="en" ############################################# # TRANSLATION SYSTEM ############################################# # Message dictionary # Message dictionary - bash 3.2 compatible (no associative arrays) # English messages MSG_EN_lang_choice="Choose Language:" MSG_EN_lang_english="English" MSG_EN_lang_russian="Русский" MSG_EN_your_lang="Your language (Ваш язык): " MSG_EN_have_fun="Have Fun!" MSG_EN_script_title="TorrServer install and configuration script for macOS" MSG_EN_unsupported_arch="Unsupported Arch. Can't continue." MSG_EN_unsupported_os="It looks like you are running this installer on a system other than macOS." MSG_EN_downloading="Downloading TorrServer" MSG_EN_target_version="Target version:" MSG_EN_installed_version="installed:" MSG_EN_target_label="target:" MSG_EN_version_not_found="ERROR: Version %s not found in releases" MSG_EN_check_versions="Please check available versions at: $REPO_URL/releases" MSG_EN_already_installed="You already have TorrServer %s installed" MSG_EN_have_latest="You have latest TorrServer %s" MSG_EN_update_found="TorrServer update found!" MSG_EN_will_install="Will install TorrServer version %s" MSG_EN_installing_packages="Installing missing packages…" MSG_EN_install_configure="Install and configure TorrServer…" MSG_EN_starting_service="Starting TorrServer…" MSG_EN_install_complete="TorrServer %s installed to %s" MSG_EN_access_web="You can now open your browser at http://%s:%s to access TorrServer web GUI." MSG_EN_use_auth="Use user \"%s\" with password \"%s\" for authentication" MSG_EN_want_update="Want to update TorrServer?" MSG_EN_want_install="Want to install or configure TorrServer? Type Delete to uninstall." MSG_EN_want_reconfigure="Do you want to reconfigure TorrServer settings?" MSG_EN_change_port="Change TorrServer web-port?" MSG_EN_enter_port="Enter port number: " MSG_EN_enable_auth="Enable server authorization?" MSG_EN_prompt_user="User: " MSG_EN_prompt_password="Password: " MSG_EN_change_auth_credentials="Change authentication username and password?" MSG_EN_enable_rdb="Start TorrServer in public read-only mode?" MSG_EN_enable_log="Enable TorrServer log output to file?" MSG_EN_confirm_delete="Are you sure you want to delete TorrServer?" MSG_EN_prompt_launchagent="Add autostart for current user (1) or all users (2)?" MSG_EN_admin_password="System can ask your admin account password" MSG_EN_install_dir_label="TorrServer install dir -" MSG_EN_uninstall_warning="This action will delete TorrServer including all it's torrents, settings and files on path above!" MSG_EN_uninstalled="TorrServer uninstalled!" MSG_EN_found_in="TorrServer found in" MSG_EN_not_found="TorrServer not found. It's not installed or have zero size." MSG_EN_no_version_info="No version information available. Can be server issue." MSG_EN_config_updated="Configuration updated successfully" MSG_EN_store_auth="Store %s:%s to %s" MSG_EN_use_existing_auth="Use existing auth from %s - %s" MSG_EN_set_readonly="Set database to read-only mode…" MSG_EN_readonly_hint="To change remove --rdb option from %s or rerun install script without parameters" MSG_EN_log_location="TorrServer log stored at %s" MSG_EN_service_added="Autostart service added to %s" MSG_EN_launchctl_missing="launchctl is not available. Skipping service management commands." MSG_EN_launchctl_failed="Warning: launchctl %s failed" MSG_EN_service_start_failed="Warning: TorrServer service failed to start. Check launchctl list for details." MSG_EN_error_version_required="Error: Version number required for downgrade" MSG_EN_error_version_example="Example: %s -d 101" MSG_EN_error_unknown_option="Unknown option: %s" MSG_EN_installing_specific_version="Installing specific version: %s" MSG_EN_install_first_required="Please install TorrServer first using: %s --install" # Russian messages MSG_RU_lang_choice="Choose Language:" MSG_RU_lang_english="English" MSG_RU_lang_russian="Русский" MSG_RU_your_lang="Your language (Ваш язык): " MSG_RU_have_fun="Have Fun!" MSG_RU_script_title="Скрипт установки, удаления и настройки TorrServer для macOS" MSG_RU_unsupported_arch="Не поддерживаемая архитектура. Продолжение невозможно." MSG_RU_unsupported_os="Похоже, что вы запускаете этот установщик в системе отличной от macOS." MSG_RU_downloading="Загружаем TorrServer" MSG_RU_target_version="Устанавливаемая версия:" MSG_RU_installed_version="установлен:" MSG_RU_target_label="устанавливаемая:" MSG_RU_version_not_found="ОШИБКА: Версия %s не найдена в релизах" MSG_RU_check_versions="Проверьте доступные версии по адресу: $REPO_URL/releases" MSG_RU_already_installed="TorrServer %s уже установлен" MSG_RU_have_latest="Установлен TorrServer последней версии %s" MSG_RU_update_found="Доступно обновление сервера" MSG_RU_will_install="Будет установлена версия TorrServer %s" MSG_RU_installing_packages="Устанавливаем недостающие пакеты…" MSG_RU_install_configure="Устанавливаем и настраиваем TorrServer…" MSG_RU_starting_service="Запускаем службу TorrServer…" MSG_RU_install_complete="TorrServer %s установлен в директории %s" MSG_RU_access_web="Теперь вы можете открыть браузер по адресу http://%s:%s для доступа к вебу TorrServer" MSG_RU_use_auth="Для авторизации используйте пользователя «%s» с паролем «%s»" MSG_RU_want_update="Хотите обновить TorrServer?" MSG_RU_want_install="Хотите установить, обновить или настроить TorrServer? Для удаления введите «Delete»" MSG_RU_want_reconfigure="Хотите перенастроить параметры TorrServer?" MSG_RU_change_port="Хотите изменить порт для TorrServer?" MSG_RU_enter_port="Введите номер порта: " MSG_RU_enable_auth="Включить авторизацию на сервере?" MSG_RU_prompt_user="Пользователь: " MSG_RU_prompt_password="Пароль: " MSG_RU_change_auth_credentials="Изменить имя пользователя и пароль для авторизации?" MSG_RU_enable_rdb="Запускать TorrServer в публичном режиме без возможности изменения настроек через веб сервера?" MSG_RU_enable_log="Включить запись журнала работы TorrServer в файл?" MSG_RU_confirm_delete="Вы уверены что хотите удалить программу?" MSG_RU_prompt_launchagent="Добавить автозагрузку для текущего пользователя (1) или для всех (2)?" MSG_RU_admin_password="Система может запросить ваш пароль администратора" MSG_RU_install_dir_label="Директория c TorrServer -" MSG_RU_uninstall_warning="Это действие удалит все данные TorrServer включая базу данных торрентов и настройки по указанному выше пути!" MSG_RU_uninstalled="TorrServer удален из системы!" MSG_RU_found_in="TorrServer найден в директории" MSG_RU_not_found="TorrServer не найден, возможно он не установлен или размер бинарника равен 0." MSG_RU_no_version_info="Информация о версии недоступна. Возможно сервер не доступен." MSG_RU_config_updated="Конфигурация успешно обновлена" MSG_RU_store_auth="Сохраняем %s:%s в %s" MSG_RU_use_existing_auth="Используйте реквизиты из %s для авторизации - %s" MSG_RU_set_readonly="База данных устанавливается в режим «только для чтения»…" MSG_RU_readonly_hint="Для изменения отредактируйте %s, убрав опцию --rdb или запустите интерактивную установку без параметров повторно" MSG_RU_log_location="лог TorrServer располагается по пути %s" MSG_RU_service_added="Сервис автозагрузки записан в %s" MSG_RU_launchctl_missing="launchctl недоступен. Пропускаем команды управления службой." MSG_RU_launchctl_failed="Предупреждение: команда launchctl %s завершилась ошибкой" MSG_RU_service_start_failed="Предупреждение: служба TorrServer не запустилась. Проверьте launchctl list для деталей." MSG_RU_error_version_required="Ошибка: Требуется номер версии для понижения версии" MSG_RU_error_version_example="Пример: %s -d 101" MSG_RU_error_unknown_option="Неизвестная опция: %s" MSG_RU_installing_specific_version="Установка конкретной версии: %s" MSG_RU_install_first_required="Пожалуйста, сначала установите TorrServer используя: %s --install" # Translation function - bash 3.2 compatible msg() { local key="$1" shift local var_name local message="" if [[ $lang == "ru" ]]; then var_name="MSG_RU_${key}" else var_name="MSG_EN_${key}" fi # Use eval to get the variable value (bash 3.2 compatible) eval "message=\"\${${var_name}:-${key}}\"" # Apply printf formatting if additional arguments provided if [[ $# -gt 0 ]]; then # shellcheck disable=SC2059 printf "$message" "$@" else printf '%s\n' "$message" fi } ############################################# # UTILITY FUNCTIONS ############################################# colorize() { if [[ $supports_color_output -eq 1 ]]; then local color_code color_code=$(getColorCode "$1") printf "%s%s%s" "$(tput setaf "$color_code")" "$2" "$(tput op)" else printf "%s" "$2" fi } # Highlight first letter of a word with specified color highlightFirstLetter() { local color="$1" local word="$2" local first_char="${word:0:1}" local rest="${word:1}" printf "%s%s" "$(colorize "$color" "$first_char")" "$rest" } getBinaryName() { echo "${BINARY_NAME_PREFIX}-${architecture}" } getVersionTag() { local version="$1" echo "${VERSION_PREFIX}.${version}" } buildDownloadUrl() { local target_version="$1" local binary_name="$2" if [[ "$target_version" == "latest" ]]; then echo "${REPO_URL}/releases/latest/download/${binary_name}" else echo "${REPO_URL}/releases/download/${target_version}/${binary_name}" fi } getLang() { lang=$(locale | grep LANG | cut -d= -f2 | tr -d '"' | cut -d_ -f1) if [[ $lang != "ru" ]]; then lang="en" fi } getIP() { local ip="localhost" # Try to get local IP address from network interfaces # On macOS, try ipconfig first (most reliable) if command -v ipconfig >/dev/null 2>&1; then # Try common interfaces: en0 (Ethernet/WiFi), en1, etc. for interface in en0 en1 eth0; do ip=$(ipconfig getifaddr "$interface" 2>/dev/null || echo "") if [[ -n "$ip" ]] && [[ "$ip" != "127.0.0.1" ]]; then break fi done fi # Fallback to ifconfig if ipconfig didn't work if [[ -z "$ip" ]] || [[ "$ip" == "127.0.0.1" ]]; then if command -v ifconfig >/dev/null 2>&1; then # Get the first non-loopback inet address ip=$(ifconfig 2>/dev/null | grep -E "inet " | grep -v "127.0.0.1" | head -n1 | awk '{print $2}' | sed 's/addr://' || echo "") fi fi # If still no valid IP, use localhost if [[ -z "$ip" ]] || [[ "$ip" == "127.0.0.1" ]]; then ip="localhost" fi serverIP="$ip" } promptYesNo() { local prompt="$1" local default="${2:-n}" local recommended="${3:-$default}" if [[ $SILENT_MODE -eq 1 ]]; then if [[ "$default" == "y" ]]; then return 0 else return 1 fi fi # Determine colors based on recommendation local yes_color no_color if [[ "$recommended" == "y" ]]; then yes_color="green" no_color="red" else yes_color="red" no_color="green" fi # Define localized Yes/No words local yes_word no_word if [[ $lang == "ru" ]]; then yes_word="Да" no_word="Нет" else yes_word="Yes" no_word="No" fi # Highlight first letter of each word local yes_text local no_text yes_text="$(highlightFirstLetter "$yes_color" "$yes_word")" no_text="$(highlightFirstLetter "$no_color" "$no_word")" local answer IFS= read -r -p " $prompt ($yes_text/$no_text) " answer /dev/null 2>&1; then if [[ $quiet -eq 0 && $SILENT_MODE -eq 0 ]]; then echo " - $(msg launchctl_missing)" fi return 1 fi local rc launchctl "$@" >/dev/null 2>&1 rc=$? if [[ $rc -ne 0 ]]; then if [[ $quiet -eq 0 && $SILENT_MODE -eq 0 ]]; then printf " - %s\n" "$(msg launchctl_failed "$*")" fi return $rc fi return 0 } killRunning() { local self="$(basename "$0")" local runningPid runningPid=$(ps -ax | grep -i torrserver | grep -v grep | grep -v "$self" | awk '{print $1}' || echo "") if [[ -n "$runningPid" ]]; then sudo kill -9 "$runningPid" 2>/dev/null || true fi } ############################################# # VERSION MANAGEMENT ############################################# getLatestRelease() { curl -s "${REPO_API_URL}/releases/latest" | grep -iE '"tag_name":|"version":' | sed -E 's/.*"([^"]+)".*/\1/' | head -n1 } getSpecificRelease() { local version="$1" local tag_name tag_name=$(getVersionTag "$version") local response response=$(curl -s "${REPO_API_URL}/releases/tags/$tag_name") if echo "$response" | grep -q '"tag_name"'; then echo "$tag_name" else echo "" fi } getTargetVersion() { if [[ -n "$specificVersion" ]]; then local target_release target_release=$(getSpecificRelease "$specificVersion") if [[ -z "$target_release" ]]; then echo " - $(colorize red "$(msg version_not_found "$specificVersion")")" echo " - $(msg check_versions)" exit 1 fi echo "$target_release" else getLatestRelease fi } downloadBinary() { local url="$1" local destination="$2" local version_info="$3" local curl_args=(-L) if [[ $SILENT_MODE -eq 0 ]]; then echo " - $(msg downloading) $version_info..." curl_args+=(--progress-bar -#) else curl_args+=(-s -S) fi curl "${curl_args[@]}" -o "$destination" "$url" chmod +x "$destination" xattr -r -d com.apple.quarantine "$destination" 2>/dev/null || true } ############################################# # OS DETECTION & ARCHITECTURE ############################################# checkOS() { if [[ "$(uname)" != "Darwin" ]]; then echo " $(msg unsupported_os)" exit 1 fi } checkArch() { case $(uname -m) in i386|i686) architecture="386" ;; x86_64) architecture="amd64" ;; aarch64|arm64) architecture="arm64" ;; *) echo " $(msg unsupported_arch)" exit 1 ;; esac } initialCheck() { checkOS checkArch } ############################################# # INSTALLATION FUNCTIONS ############################################# checkInstalled() { local binName binName=$(getBinaryName) if [[ -f "$dirInstall/$binName" ]] && [[ $(stat -f%z "$dirInstall/$binName" 2>/dev/null) -ne 0 ]]; then if [[ $SILENT_MODE -eq 0 ]]; then echo " - $(msg found_in) $dirInstall" fi return 0 else if [[ $SILENT_MODE -eq 0 ]]; then echo " - $(msg not_found)" fi return 1 fi } checkInstalledVersion() { local binName binName=$(getBinaryName) local target_version target_version=$(getTargetVersion) local installed_version installed_version="$("$dirInstall/$binName" --version 2>/dev/null | awk '{print $2}')" if [[ -z "$target_version" ]]; then echo " - $(msg no_version_info)" exit 1 fi if [[ "$target_version" == "$installed_version" ]]; then if [[ -n "$specificVersion" ]]; then if [[ $SILENT_MODE -eq 0 ]]; then echo " - $(msg already_installed "$target_version")" fi else if [[ $SILENT_MODE -eq 0 ]]; then echo " - $(msg have_latest "$target_version")" fi fi return 0 else if [[ $SILENT_MODE -eq 0 ]]; then if [[ -n "$specificVersion" ]]; then echo " - $(msg will_install "$target_version")" else echo " - $(msg update_found)" fi echo " $(msg installed_version) \"$installed_version\"" echo " $(msg target_label) \"$target_version\"" fi return 1 fi } createPlistFile() { local daemon_options="--port $servicePort --path $dirInstall" if [[ $isRdb -eq 1 ]]; then daemon_options="$daemon_options --rdb" fi if [[ $isLog -eq 1 ]]; then daemon_options="$daemon_options --logpath $dirInstall/$serviceName.log" fi if [[ $isAuth -eq 1 ]]; then daemon_options="$daemon_options --httpauth" fi # Convert daemon_options to plist array format local plist_args=() local arg for arg in $daemon_options; do plist_args+=(" $arg") done local plist_args_str plist_args_str=$(printf '%s\n' "${plist_args[@]}") cat << EOF > "$dirInstall/$serviceName.plist" Label ${serviceName} ServiceDescription TorrServer service for macOS ProgramArguments ${dirInstall}/$(getBinaryName) ${plist_args_str} RunAtLoad KeepAlive SuccessfulExit ProcessType Background ThrottleInterval 10 AbandonProcessGroup StandardOutPath ${dirInstall}/torrserver.log StandardErrorPath ${dirInstall}/torrserver.log WorkingDirectory ${dirInstall} EOF } readExistingConfig() { local plist_file="$dirInstall/$serviceName.plist" if [[ -f "$plist_file" ]]; then # Extract port if grep -q "--port" "$plist_file"; then servicePort=$(grep -A1 "--port" "$plist_file" | tail -n1 | sed 's/.*\(.*\)<\/string>.*/\1/') fi # Check for auth if grep -q "--httpauth" "$plist_file"; then isAuth=1 else isAuth=0 fi # Check for rdb if grep -q "--rdb" "$plist_file"; then isRdb=1 else isRdb=0 fi # Check for log if grep -q "--logpath" "$plist_file"; then isLog=1 else isLog=0 fi fi } configureService() { # Read existing config if available (for reconfiguration) if [[ -f "$dirInstall/$serviceName.plist" ]]; then readExistingConfig fi # Port configuration if [[ -z "$servicePort" ]]; then local inferred_default="$DEFAULT_PORT" if promptYesNo "$(msg change_port)" "n" "y"; then servicePort=$(promptInput "$(msg enter_port)" "$inferred_default") else servicePort="$inferred_default" fi else # Port exists, ask if user wants to change it if [[ $SILENT_MODE -eq 0 ]]; then if promptYesNo "$(msg change_port)" "n" "y"; then servicePort=$(promptInput "$(msg enter_port)" "$servicePort") fi fi fi # Auth configuration if [[ -z "$isAuth" ]]; then if promptYesNo "$(msg enable_auth)" "n" "y"; then isAuth=1 else isAuth=0 fi else # Auth setting exists, ask if user wants to change it if [[ $SILENT_MODE -eq 0 ]]; then local current_auth_default current_auth_default="$([[ $isAuth -eq 1 ]] && echo 'y' || echo 'n')" if promptYesNo "$(msg enable_auth)" "$current_auth_default" "y"; then isAuth=1 else isAuth=0 fi fi fi # Setup auth if enabled if [[ $isAuth -eq 1 ]]; then if [[ ! -f "$dirInstall/accs.db" ]]; then isAuthUser=$(promptInput "$(msg prompt_user)" "admin") isAuthPass=$(promptInput "$(msg prompt_password)" "admin") if [[ $SILENT_MODE -eq 0 ]]; then printf ' %s\n' "$(msg store_auth "$isAuthUser" "$isAuthPass" "${dirInstall}/accs.db")" fi echo -e "{\n \"$isAuthUser\": \"$isAuthPass\"\n}" > "$dirInstall/accs.db" else local auth auth=$(cat "$dirInstall/accs.db" | head -2 | tail -1 | tr -d '[:space:]' | tr -d '"') if [[ $SILENT_MODE -eq 0 ]]; then printf ' - %s\n' "$(msg use_existing_auth "${dirInstall}/accs.db" "$auth")" # Ask if user wants to change credentials if promptYesNo "$(msg change_auth_credentials)" "n" "n"; then isAuthUser=$(promptInput "$(msg prompt_user)" "admin") isAuthPass=$(promptInput "$(msg prompt_password)" "admin") if [[ $SILENT_MODE -eq 0 ]]; then printf ' %s\n' "$(msg store_auth "$isAuthUser" "$isAuthPass" "${dirInstall}/accs.db")" fi echo -e "{\n \"$isAuthUser\": \"$isAuthPass\"\n}" > "$dirInstall/accs.db" fi fi fi fi # Read-only database configuration if [[ -z "$isRdb" ]]; then if promptYesNo "$(msg enable_rdb)" "n" "n"; then isRdb=1 else isRdb=0 fi else # RDB setting exists, ask if user wants to change it if [[ $SILENT_MODE -eq 0 ]]; then local current_rdb_default current_rdb_default="$([[ $isRdb -eq 1 ]] && echo 'y' || echo 'n')" if promptYesNo "$(msg enable_rdb)" "$current_rdb_default" "n"; then isRdb=1 else isRdb=0 fi fi fi if [[ $isRdb -eq 1 ]] && [[ $SILENT_MODE -eq 0 ]]; then echo " $(msg set_readonly)" printf ' %s\n' "$(msg readonly_hint "$dirInstall/$serviceName.plist")" fi # Logging configuration if [[ -z "$isLog" ]]; then if promptYesNo "$(msg enable_log)" "n" "y"; then isLog=1 else isLog=0 fi else # Log setting exists, ask if user wants to change it if [[ $SILENT_MODE -eq 0 ]]; then local current_log_default current_log_default="$([[ $isLog -eq 1 ]] && echo 'y' || echo 'n')" if promptYesNo "$(msg enable_log)" "$current_log_default" "y"; then isLog=1 else isLog=0 fi fi fi if [[ $isLog -eq 1 ]] && [[ $SILENT_MODE -eq 0 ]]; then printf ' - %s\n' "$(msg log_location "$dirInstall/$serviceName.log")" fi # LaunchAgent/LaunchDaemon selection if [[ $SILENT_MODE -eq 0 && $USER_PROMPTED -eq 0 ]]; then local answer_cu answer_cu=$(promptInput "$(msg prompt_launchagent)" "1") if [[ "$answer_cu" == "1" ]]; then USE_USER_LAUNCHAGENT=1 sysPath="${HOME}/Library/LaunchAgents" else USE_USER_LAUNCHAGENT=0 sysPath="/Library/LaunchDaemons" fi USER_PROMPTED=1 elif [[ $SILENT_MODE -eq 1 ]]; then # Silent mode defaults to user LaunchAgent USE_USER_LAUNCHAGENT=1 sysPath="${HOME}/Library/LaunchAgents" fi } installTorrServer() { if [[ $SILENT_MODE -eq 0 ]]; then echo " $(msg install_configure)" fi # Get target version local target_version target_version=$(getTargetVersion) if [[ $SILENT_MODE -eq 0 ]]; then echo " - $(msg target_version) $target_version" fi # Check if already installed and up to date if checkInstalled; then if ! checkInstalledVersion; then if promptYesNo "$(msg want_update)" "y" "y"; then UpdateVersion return fi else # Already installed and up to date, allow reconfiguration if [[ $SILENT_MODE -eq 0 ]]; then echo "" # Allow user to reconfigure settings if promptYesNo "$(msg want_reconfigure)" "n" "n"; then # Read existing config first if [[ -f "$dirInstall/$serviceName.plist" ]]; then readExistingConfig fi # Reconfigure service configureService # Update plist file createPlistFile # Reload and restart service cleanup installService echo "" echo " - $(msg config_updated)" echo "" fi fi return fi fi # Create directories if [[ ! -d "$dirInstall" ]]; then mkdir -p "$dirInstall" chmod a+rw "$dirInstall" fi # Download binary if needed local binName binName=$(getBinaryName) if [[ ! -f "$dirInstall/$binName" ]] || [[ ! -x "$dirInstall/$binName" ]] || [[ $(stat -f%z "$dirInstall/$binName" 2>/dev/null) -eq 0 ]]; then local urlBin if [[ -n "$specificVersion" ]]; then urlBin=$(buildDownloadUrl "$target_version" "$binName") else urlBin=$(buildDownloadUrl "latest" "$binName") fi downloadBinary "$urlBin" "$dirInstall/$binName" "$target_version" fi # Create plist and configure service configureService createPlistFile # Install service local service_started=0 if installService; then service_started=1 fi # Show completion message getIP local installed_version="$target_version" if [[ $SILENT_MODE -eq 0 ]]; then echo "" printf ' %s\n' "$(msg install_complete "$installed_version" "$dirInstall")" echo "" printf ' %s\n' "$(msg access_web "$serverIP" "$servicePort")" echo "" if [[ $isAuth -eq 1 && -n "$isAuthUser" ]]; then printf ' %s\n' "$(msg use_auth "$isAuthUser" "$isAuthPass")" echo "" fi if [[ $service_started -eq 0 ]]; then echo " $(colorize yellow "$(msg service_start_failed)")" fi echo "" fi if [[ $SILENT_MODE -eq 1 ]]; then printf "%s\n" "$(msg install_complete "$installed_version" "$dirInstall")" printf "%s\n" "$(msg access_web "$serverIP" "$servicePort")" if [[ $isAuth -eq 1 && -n "$isAuthUser" ]]; then printf "%s\n" "$(msg use_auth "$isAuthUser" "$isAuthPass")" fi fi return 0 } installService() { # Cleanup existing services first cleanup if [[ $USE_USER_LAUNCHAGENT -eq 1 ]]; then # User LaunchAgent sysPath="${HOME}/Library/LaunchAgents" [[ ! -d "$sysPath" ]] && mkdir -p "$sysPath" cp "$dirInstall/$serviceName.plist" "$sysPath" chmod 0644 "$sysPath/$serviceName.plist" if launchctlCmd load -w "$sysPath/$serviceName.plist"; then if [[ $SILENT_MODE -eq 0 ]]; then printf ' %s\n' "$(msg service_added "$sysPath")" fi return 0 else if [[ $SILENT_MODE -eq 0 ]]; then printf ' %s\n' "$(msg service_added "$sysPath")" fi return 1 fi else # System LaunchDaemon sysPath="/Library/LaunchDaemons" [[ ! -d "$sysPath" ]] && sudo mkdir -p "$sysPath" sudo cp "$dirInstall/$serviceName.plist" "$sysPath" sudo chown root:wheel "$sysPath/$serviceName.plist" sudo chmod 0644 "$sysPath/$serviceName.plist" if sudo launchctl load -w "$sysPath/$serviceName.plist" >/dev/null 2>&1; then if [[ $SILENT_MODE -eq 0 ]]; then printf ' %s\n' "$(msg service_added "$sysPath")" fi return 0 else if [[ $SILENT_MODE -eq 0 ]]; then printf ' %s\n' "$(msg service_added "$sysPath")" fi return 1 fi fi } # Common function to update/downgrade TorrServer version updateTorrServerVersion() { local target_version="$1" local cancel_message="$2" local use_latest_url="${3:-0}" killRunning local binName binName=$(getBinaryName) local urlBin if [[ $use_latest_url -eq 1 && -z "$specificVersion" ]]; then urlBin=$(buildDownloadUrl "latest" "$binName") else urlBin=$(buildDownloadUrl "$target_version" "$binName") fi downloadBinary "$urlBin" "$dirInstall/$binName" "$target_version" # Update plist file if [[ -f "$dirInstall/$serviceName.plist" ]]; then createPlistFile installService fi return 0 } UpdateVersion() { local target_version target_version=$(getTargetVersion) updateTorrServerVersion "$target_version" "update_cancelled" 1 } DowngradeVersion() { local target_version target_version=$(getVersionTag "$downgradeRelease") updateTorrServerVersion "$target_version" "downgrade_cancelled" 0 } ############################################# # CLEANUP FUNCTIONS ############################################# cleanup() { killRunning launchctl unload "$HOME/Library/LaunchAgents/$serviceName.plist" >/dev/null 2>&1 || true sudo launchctl unload "/Library/LaunchDaemons/$serviceName.plist" >/dev/null 2>&1 || true rm -f "$HOME/Library/LaunchAgents/$serviceName.plist" 2>/dev/null || true sudo rm -f "/Library/LaunchDaemons/$serviceName.plist" 2>/dev/null || true } uninstall() { checkArch checkInstalled if [[ $SILENT_MODE -eq 1 ]]; then cleanup sudo rm -rf "$dirInstall" echo " - $(msg uninstalled)" return fi echo "" echo " $(msg install_dir_label) ${dirInstall}" echo "" echo " $(msg uninstall_warning)" echo "" if promptYesNo "$(msg confirm_delete)" "n" "n"; then cleanup sudo rm -rf "$dirInstall" echo " - $(msg uninstalled)" echo "" else echo "" fi } ############################################# # RECONFIGURATION ############################################# reconfigureTorrServer() { # Check if TorrServer is installed if ! checkInstalled; then echo " - $(msg not_found)" echo " - $(msg install_first_required "$scriptname")" exit 1 fi if [[ $SILENT_MODE -eq 0 ]]; then echo "" fi # Read existing config first if [[ -f "$dirInstall/$serviceName.plist" ]]; then readExistingConfig fi # Reconfigure service configureService # Update plist file createPlistFile # Reload and restart service cleanup installService if [[ $SILENT_MODE -eq 0 ]]; then echo "" echo " - $(msg config_updated)" echo "" else echo " - $(msg config_updated)" fi } ############################################# # HELP & MAIN ############################################# helpUsage() { cat << EOF $scriptname - TorrServer Installation Script Usage: $scriptname [COMMAND] [OPTIONS] Commands: -i, --install [VERSION] Install latest or specific version install [VERSION] -u, --update Update to latest version update -c, --check Check for updates (version info only) check -d, --down VERSION Downgrade to specific version down VERSION -r, --remove Uninstall TorrServer remove --reconfigure Reconfigure TorrServer settings reconfigure -h, --help Show this help message help Options: --silent Non-interactive mode with defaults Examples: # Install latest version interactively $scriptname --install # Install specific version silently $scriptname --install 135 --silent # Update with silent mode $scriptname --update --silent # Check for updates $scriptname --check # Uninstall silently $scriptname --remove --silent # Reconfigure TorrServer settings interactively $scriptname --reconfigure Default Settings (silent mode): - Port: ${DEFAULT_PORT} - LaunchAgent: current user (not system-wide) - Auth: disabled - Read-only mode: disabled - Logging: disabled EOF } parseArguments() { parsedCommand="" while [[ $# -gt 0 ]]; do case $1 in -i|--install|install) parsedCommand="install" shift # Check for version number if [[ $# -gt 0 ]]; then local next_arg="$1" if [[ "$next_arg" =~ ^[0-9]+$ ]]; then specificVersion="$next_arg" shift fi fi ;; -u|--update|update) parsedCommand="update" shift ;; -c|--check|check) parsedCommand="check" shift ;; -d|--down|down) parsedCommand="downgrade" shift if [[ $# -gt 0 ]]; then local next_arg="$1" if [[ "$next_arg" =~ ^[0-9]+$ ]]; then downgradeRelease="$next_arg" shift else echo " $(msg error_version_required)" echo " $(msg error_version_example "$scriptname")" exit 1 fi else echo " $(msg error_version_required)" echo " $(msg error_version_example "$scriptname")" exit 1 fi ;; -r|--remove|remove) parsedCommand="remove" shift ;; --reconfigure|reconfigure) parsedCommand="reconfigure" shift ;; -h|--help|help) getLang # Set language before showing help helpUsage exit 0 ;; --silent) SILENT_MODE=1 shift ;; *) echo " $(msg error_unknown_option "$1")" helpUsage exit 1 ;; esac done } ############################################# # MAIN EXECUTION ############################################# main() { getLang parseArguments "$@" local command="$parsedCommand" case "$command" in install) if [[ $SILENT_MODE -eq 0 && -n "$specificVersion" ]]; then echo " - $(msg installing_specific_version "$specificVersion")" fi initialCheck if [[ $SILENT_MODE -eq 1 ]]; then servicePort="$DEFAULT_PORT" isAuth=0 isRdb=0 isLog=0 USE_USER_LAUNCHAGENT=1 USER_PROMPTED=1 fi if ! checkInstalled; then installTorrServer else createPlistFile installService if [[ $SILENT_MODE -eq 0 ]]; then echo " - $(msg config_updated)" fi fi exit 0 ;; update) initialCheck if checkInstalled; then if ! checkInstalledVersion; then UpdateVersion fi fi exit 0 ;; check) initialCheck if checkInstalled; then checkInstalledVersion fi exit 0 ;; downgrade) initialCheck if checkInstalled; then DowngradeVersion fi exit 0 ;; remove) uninstall exit 0 ;; reconfigure) initialCheck reconfigureTorrServer exit 0 ;; esac # Interactive mode if no command provided and not silent if [[ $SILENT_MODE -eq 0 ]]; then echo "" echo " $(msg lang_choice)" echo " [$(colorize green 1)] $(msg lang_english)" echo " [$(colorize yellow 2)] $(msg lang_russian)" local answer_lang answer_lang=$(promptInput "$(msg your_lang)" "1") if [[ "$answer_lang" == "2" ]]; then lang="ru" fi echo "" echo "=============================================================" echo " $(msg script_title)" echo "=============================================================" echo "" local user_choice user_choice=$(promptYesNoDelete "$(msg want_install)" "n" "y") if [[ "$user_choice" == "delete" ]]; then initialCheck uninstall elif [[ "$user_choice" == "yes" ]]; then initialCheck USER_PROMPTED=0 installTorrServer fi fi echo " $(msg have_fun)" echo "" } # Run main function main "$@"