Browse Source

feat(bash): prompt for PostgreSQL (#4472)

* feat(install): prompt for SQLite vs PostgreSQL during install

* fix(install): write env file to per-distro path and handle pg-install failure

The env file was hardcoded to /etc/default/x-ui, but RHEL/Fedora units read
/etc/sysconfig/x-ui, Arch reads /etc/conf.d/x-ui, and Alpine OpenRC auto-
sources /etc/conf.d/x-ui. PostgreSQL selection was silently dropped on every
distro except Debian. Also initdb on openSUSE (service wouldn't start) and
prompt the operator on local-install failure instead of silently demoting
to SQLite.

* fix(scripts): make x-ui.sh and update.sh PostgreSQL-aware

update.sh ran setting -show and migrate without sourcing the env file, so
PostgreSQL users had migrations applied to the SQLite default and settings
introspection read the wrong DB. Sourcing the per-distro env file at the
start of update_x-ui exports XUI_DB_TYPE/XUI_DB_DSN to all binary calls.

x-ui.sh now shows the active backend in View Current Settings (password
masked) and removes the env file on uninstall so a later reinstall doesn't
inherit a stale DSN.
Sanaei 22 hours ago
parent
commit
b71ed1e3ee
3 changed files with 205 additions and 0 deletions
  1. 151 0
      install.sh
  2. 27 0
      update.sh
  3. 27 0
      x-ui.sh

+ 151 - 0
install.sh

@@ -110,6 +110,83 @@ gen_random_string() {
         | head -c "$length"
         | head -c "$length"
 }
 }
 
 
+install_postgres_local() {
+    local pg_user="xui"
+    local pg_db="xui"
+    local pg_pass
+    pg_pass=$(gen_random_string 24)
+
+    case "${release}" in
+        ubuntu | debian | armbian)
+            apt-get update >&2 && apt-get install -y -q postgresql >&2 || return 1
+            ;;
+        fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
+            dnf install -y -q postgresql-server postgresql-contrib >&2 || return 1
+            [[ -d /var/lib/pgsql/data && -f /var/lib/pgsql/data/PG_VERSION ]] || postgresql-setup --initdb >&2 || return 1
+            ;;
+        centos)
+            if [[ "${VERSION_ID}" =~ ^7 ]]; then
+                yum install -y postgresql-server postgresql-contrib >&2 || return 1
+            else
+                dnf install -y -q postgresql-server postgresql-contrib >&2 || return 1
+            fi
+            [[ -d /var/lib/pgsql/data && -f /var/lib/pgsql/data/PG_VERSION ]] || postgresql-setup --initdb >&2 || return 1
+            ;;
+        arch | manjaro | parch)
+            pacman -Syu --noconfirm postgresql >&2 || return 1
+            if [[ ! -f /var/lib/postgres/data/PG_VERSION ]]; then
+                sudo -u postgres initdb -D /var/lib/postgres/data >&2 || return 1
+            fi
+            ;;
+        opensuse-tumbleweed | opensuse-leap)
+            zypper -q install -y postgresql-server postgresql-contrib >&2 || return 1
+            if [[ ! -f /var/lib/pgsql/data/PG_VERSION ]]; then
+                install -d -o postgres -g postgres -m 700 /var/lib/pgsql/data >&2 || return 1
+                su - postgres -c "initdb -D /var/lib/pgsql/data" >&2 || return 1
+            fi
+            ;;
+        alpine)
+            apk add --no-cache postgresql postgresql-contrib >&2 || return 1
+            if [[ ! -f /var/lib/postgresql/data/PG_VERSION ]]; then
+                /etc/init.d/postgresql setup >&2 || return 1
+            fi
+            rc-update add postgresql default >&2 2> /dev/null || true
+            rc-service postgresql start >&2 || return 1
+            ;;
+        *)
+            echo -e "${red}Unsupported distro for automatic PostgreSQL install: ${release}${plain}" >&2
+            return 1
+            ;;
+    esac
+
+    if [[ "${release}" != "alpine" ]]; then
+        systemctl enable --now postgresql >&2 || return 1
+    fi
+
+    # Wait briefly for the server to accept connections.
+    local i
+    for i in 1 2 3 4 5; do
+        sudo -u postgres psql -tAc 'SELECT 1' > /dev/null 2>&1 && break
+        sleep 1
+    done
+
+    # Idempotent role/db creation.
+    sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${pg_user}'" 2> /dev/null \
+        | grep -q 1 \
+        || sudo -u postgres psql -c "CREATE USER ${pg_user} WITH PASSWORD '${pg_pass}';" >&2 || return 1
+
+    sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${pg_db}'" 2> /dev/null \
+        | grep -q 1 \
+        || sudo -u postgres psql -c "CREATE DATABASE ${pg_db} OWNER ${pg_user};" >&2 || return 1
+
+    sudo -u postgres psql -c "ALTER USER ${pg_user} WITH PASSWORD '${pg_pass}';" >&2 || return 1
+
+    local pg_pass_enc
+    pg_pass_enc=$(printf '%s' "${pg_pass}" | sed -e 's/%/%25/g' -e 's/:/%3A/g' -e 's/@/%40/g' -e 's|/|%2F|g' -e 's/?/%3F/g' -e 's/#/%23/g')
+    echo "postgres://${pg_user}:${pg_pass_enc}@127.0.0.1:5432/${pg_db}?sslmode=disable"
+    return 0
+}
+
 install_acme() {
 install_acme() {
     echo -e "${green}Installing acme.sh for SSL certificate management...${plain}"
     echo -e "${green}Installing acme.sh for SSL certificate management...${plain}"
     cd ~ || return 1
     cd ~ || return 1
@@ -741,6 +818,79 @@ config_after_install() {
             local config_username=$(gen_random_string 10)
             local config_username=$(gen_random_string 10)
             local config_password=$(gen_random_string 10)
             local config_password=$(gen_random_string 10)
 
 
+            local db_label="SQLite (/etc/x-ui/x-ui.db)"
+            echo ""
+            echo -e "${green}═══════════════════════════════════════════${plain}"
+            echo -e "${green}     Database Selection                    ${plain}"
+            echo -e "${green}═══════════════════════════════════════════${plain}"
+            echo -e "  1) SQLite     (default — recommended for < 1000 clients)"
+            echo -e "  2) PostgreSQL (recommended for high client counts / many nodes)"
+            read -rp "Choose [1]: " db_choice
+            db_choice="${db_choice:-1}"
+            if [[ "$db_choice" == "2" ]]; then
+                local xui_env_file
+                case "${release}" in
+                    ubuntu | debian | armbian)
+                        xui_env_file="/etc/default/x-ui"
+                        ;;
+                    arch | manjaro | parch | alpine)
+                        xui_env_file="/etc/conf.d/x-ui"
+                        ;;
+                    *)
+                        xui_env_file="/etc/sysconfig/x-ui"
+                        ;;
+                esac
+
+                local xui_dsn=""
+                local pg_mode=""
+                while [[ -z "$xui_dsn" ]]; do
+                    echo ""
+                    echo -e "  1) Install PostgreSQL locally and create a dedicated user/db (recommended)"
+                    echo -e "  2) Use an existing PostgreSQL server (enter DSN)"
+                    read -rp "Choose [1]: " pg_mode
+                    pg_mode="${pg_mode:-1}"
+                    if [[ "$pg_mode" == "2" ]]; then
+                        while [[ -z "$xui_dsn" ]]; do
+                            read -rp "Enter PostgreSQL DSN (postgres://user:pass@host:port/dbname?sslmode=disable): " xui_dsn
+                            xui_dsn="${xui_dsn// /}"
+                        done
+                        db_label="PostgreSQL (external)"
+                    else
+                        echo -e "${yellow}Installing PostgreSQL — this may take a moment...${plain}"
+                        if xui_dsn=$(install_postgres_local); then
+                            db_label="PostgreSQL ([email protected]:5432/xui)"
+                        else
+                            echo ""
+                            echo -e "${red}PostgreSQL installation failed.${plain}"
+                            echo -e "  1) Retry local install"
+                            echo -e "  2) Enter an external DSN instead"
+                            echo -e "  3) Abort install"
+                            echo -e "  4) Fall back to SQLite"
+                            read -rp "Choose [1]: " pg_fail
+                            pg_fail="${pg_fail:-1}"
+                            case "$pg_fail" in
+                                2) pg_mode="2" ;;
+                                3) echo -e "${red}Install aborted.${plain}"; exit 1 ;;
+                                4) db_choice="1"; xui_dsn=""; break ;;
+                                *) xui_dsn="" ;;
+                            esac
+                        fi
+                    fi
+                done
+                if [[ -n "$xui_dsn" ]]; then
+                    install -d -m 755 "$(dirname "$xui_env_file")"
+                    umask 077
+                    cat > "$xui_env_file" << EOF
+XUI_DB_TYPE=postgres
+XUI_DB_DSN=${xui_dsn}
+EOF
+                    chmod 600 "$xui_env_file"
+                    umask 022
+                    export XUI_DB_TYPE=postgres
+                    export XUI_DB_DSN="${xui_dsn}"
+                fi
+            fi
+
             read -rp "Would you like to customize the Panel Port settings? (If not, a random port will be applied) [y/n]: " config_confirm
             read -rp "Would you like to customize the Panel Port settings? (If not, a random port will be applied) [y/n]: " config_confirm
             if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
             if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
                 read -rp "Please set up the panel port: " config_port
                 read -rp "Please set up the panel port: " config_port
@@ -775,6 +925,7 @@ config_after_install() {
             echo -e "${green}Password:    ${config_password}${plain}"
             echo -e "${green}Password:    ${config_password}${plain}"
             echo -e "${green}Port:        ${config_port}${plain}"
             echo -e "${green}Port:        ${config_port}${plain}"
             echo -e "${green}WebBasePath: ${config_webBasePath}${plain}"
             echo -e "${green}WebBasePath: ${config_webBasePath}${plain}"
+            echo -e "${green}Database:    ${db_label}${plain}"
             echo -e "${green}Access URL:  ${SSL_SCHEME}://${SSL_HOST}:${config_port}/${config_webBasePath}${plain}"
             echo -e "${green}Access URL:  ${SSL_SCHEME}://${SSL_HOST}:${config_port}/${config_webBasePath}${plain}"
             echo -e "${green}API Token:   ${config_apiToken}${plain}"
             echo -e "${green}API Token:   ${config_apiToken}${plain}"
             echo -e "${green}═══════════════════════════════════════════${plain}"
             echo -e "${green}═══════════════════════════════════════════${plain}"

+ 27 - 0
update.sh

@@ -105,6 +105,31 @@ gen_random_string() {
         | head -c "$length"
         | head -c "$length"
 }
 }
 
 
+xui_env_file_path() {
+    case "${release}" in
+        ubuntu | debian | armbian)
+            echo "/etc/default/x-ui"
+            ;;
+        arch | manjaro | parch | alpine)
+            echo "/etc/conf.d/x-ui"
+            ;;
+        *)
+            echo "/etc/sysconfig/x-ui"
+            ;;
+    esac
+}
+
+load_xui_env() {
+    local env_file
+    env_file="$(xui_env_file_path)"
+    if [[ -r "$env_file" ]]; then
+        set -a
+        # shellcheck disable=SC1090
+        source "$env_file"
+        set +a
+    fi
+}
+
 install_base() {
 install_base() {
     echo -e "${green}Updating and install dependency packages...${plain}"
     echo -e "${green}Updating and install dependency packages...${plain}"
     case "${release}" in
     case "${release}" in
@@ -775,6 +800,8 @@ config_after_update() {
 update_x-ui() {
 update_x-ui() {
     cd ${xui_folder%/x-ui}/
     cd ${xui_folder%/x-ui}/
 
 
+    load_xui_env
+
     if [ -f "${xui_folder}/x-ui" ]; then
     if [ -f "${xui_folder}/x-ui" ]; then
         current_xui_version=$(${xui_folder}/x-ui -v)
         current_xui_version=$(${xui_folder}/x-ui -v)
         echo -e "${green}Current x-ui version: ${current_xui_version}${plain}"
         echo -e "${green}Current x-ui version: ${current_xui_version}${plain}"

+ 27 - 0
x-ui.sh

@@ -179,6 +179,20 @@ delete_script() {
     exit 1
     exit 1
 }
 }
 
 
+xui_env_file_path() {
+    case "${release}" in
+        ubuntu | debian | armbian)
+            echo "/etc/default/x-ui"
+            ;;
+        arch | manjaro | parch | alpine)
+            echo "/etc/conf.d/x-ui"
+            ;;
+        *)
+            echo "/etc/sysconfig/x-ui"
+            ;;
+    esac
+}
+
 uninstall() {
 uninstall() {
     confirm "Are you sure you want to uninstall the panel? xray will also uninstalled!" "n"
     confirm "Are you sure you want to uninstall the panel? xray will also uninstalled!" "n"
     if [[ $? != 0 ]]; then
     if [[ $? != 0 ]]; then
@@ -202,6 +216,7 @@ uninstall() {
 
 
     rm /etc/x-ui/ -rf
     rm /etc/x-ui/ -rf
     rm ${xui_folder}/ -rf
     rm ${xui_folder}/ -rf
+    rm -f "$(xui_env_file_path)"
 
 
     echo ""
     echo ""
     echo -e "Uninstalled Successfully.\n"
     echo -e "Uninstalled Successfully.\n"
@@ -289,6 +304,18 @@ check_config() {
     fi
     fi
     LOGI "${info}"
     LOGI "${info}"
 
 
+    local db_env_file
+    db_env_file="$(xui_env_file_path)"
+    if [[ -r "$db_env_file" ]] && grep -q '^XUI_DB_TYPE=postgres' "$db_env_file"; then
+        local dsn
+        dsn="$(grep -E '^XUI_DB_DSN=' "$db_env_file" | head -1 | cut -d= -f2-)"
+        local dsn_safe
+        dsn_safe="$(echo "$dsn" | sed -E 's|(://[^:/@]+:)[^@]+@|\1****@|')"
+        echo -e "${green}Database: PostgreSQL — ${dsn_safe}${plain}"
+    else
+        echo -e "${green}Database: SQLite (/etc/x-ui/x-ui.db)${plain}"
+    fi
+
     local existing_webBasePath=$(echo "$info" | grep -Eo 'webBasePath: .+' | awk '{print $2}')
     local existing_webBasePath=$(echo "$info" | grep -Eo 'webBasePath: .+' | awk '{print $2}')
     local existing_port=$(echo "$info" | grep -Eo 'port: .+' | awk '{print $2}')
     local existing_port=$(echo "$info" | grep -Eo 'port: .+' | awk '{print $2}')
     local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
     local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')