feat(x-ui.sh): add PostgreSQL management menu

Add a self-contained 'PostgreSQL Management' submenu (main-menu option 27) so the panel can be set up and migrated without re-running the remote install script:

- Install PostgreSQL locally (server + client tools + dedicated xui user/db), ported from install.sh so x-ui.sh stays standalone

- Migrate SQLite to PostgreSQL via 'x-ui migrate-db', then write XUI_DB_TYPE/XUI_DB_DSN to the service env file and restart the panel; client tools are ensured first so in-panel backup/restore works for local and external databases

- Service control: status (clusters + port 5432), start, stop, restart, enable autostart, view log, with auto-detected cluster version
This commit is contained in:
MHSanaei
2026-06-01 23:00:35 +02:00
parent 5b9ed34009
commit 47d9b49666

410
x-ui.sh
View File

@@ -2282,6 +2282,407 @@ SSH_port_forwarding() {
esac
}
# PostgreSQL service management (for panels configured with XUI_DB_TYPE=postgres).
postgresql_installed() {
command -v pg_lsclusters > /dev/null 2>&1 || command -v psql > /dev/null 2>&1 || command -v postgres > /dev/null 2>&1
}
# Prints "VER CLUSTER" of the first configured cluster on Debian-style installs (e.g. "16 main").
pg_cluster_info() {
if command -v pg_lsclusters > /dev/null 2>&1; then
pg_lsclusters 2> /dev/null | awk '$1 ~ /^[0-9]+$/ {print $1, $2; exit}'
fi
}
# Resolves the systemd unit used to manage the PostgreSQL server.
pg_systemd_unit() {
local info ver cluster
info="$(pg_cluster_info)"
if [[ -n "$info" ]]; then
ver="${info%% *}"
cluster="${info##* }"
echo "postgresql@${ver}-${cluster}"
else
echo "postgresql"
fi
}
postgresql_status() {
if ! postgresql_installed; then
LOGE "PostgreSQL does not appear to be installed on this system."
return 1
fi
if command -v pg_lsclusters > /dev/null 2>&1; then
pg_lsclusters
else
systemctl status "$(pg_systemd_unit)" --no-pager
fi
echo ""
if command -v ss > /dev/null 2>&1; then
local listening
listening=$(ss -ltnp 2> /dev/null | grep ':5432')
if [[ -n "$listening" ]]; then
echo -e "${green}PostgreSQL is listening on port 5432:${plain}"
echo "$listening"
else
echo -e "${red}Nothing is listening on port 5432 - the database is not running.${plain}"
fi
fi
}
postgresql_start() {
pg_require_installed || return 1
if [[ $release == "alpine" ]]; then
rc-service postgresql start
else
systemctl start "$(pg_systemd_unit)"
fi
sleep 1
postgresql_status
}
postgresql_stop() {
pg_require_installed || return 1
if [[ $release == "alpine" ]]; then
rc-service postgresql stop
else
systemctl stop "$(pg_systemd_unit)"
fi
LOGI "PostgreSQL stop signal sent."
}
postgresql_restart() {
pg_require_installed || return 1
if [[ $release == "alpine" ]]; then
rc-service postgresql restart
else
systemctl restart "$(pg_systemd_unit)"
fi
sleep 1
postgresql_status
}
postgresql_enable() {
pg_require_installed || return 1
if [[ $release == "alpine" ]]; then
rc-update add postgresql default
else
systemctl enable "$(pg_systemd_unit)"
fi
if [[ $? == 0 ]]; then
LOGI "PostgreSQL set to start automatically on boot."
else
LOGE "Failed to enable PostgreSQL autostart."
fi
}
postgresql_log() {
pg_require_installed || return 1
local info ver cluster logfile
info="$(pg_cluster_info)"
if [[ -n "$info" ]]; then
ver="${info%% *}"
cluster="${info##* }"
logfile="/var/log/postgresql/postgresql-${ver}-${cluster}.log"
fi
if [[ -n "$logfile" && -f "$logfile" ]]; then
tail -n 40 "$logfile"
elif command -v journalctl > /dev/null 2>&1; then
journalctl -u "$(pg_systemd_unit)" -n 40 --no-pager
else
LOGE "No PostgreSQL log found."
fi
}
pg_require_installed() {
if ! postgresql_installed; then
LOGE "PostgreSQL is not installed. Use option 1 (Install PostgreSQL) in this menu first."
return 1
fi
}
# Installs a local PostgreSQL server and creates a dedicated xui user/database.
# Progress goes to stderr; on success the connection DSN is printed to stdout so
# callers can capture it. Mirrors install_postgres_local() from install.sh, so the
# panel can be set up without re-running the remote install script.
pg_install_local() {
local pg_user pg_pass pg_db pg_host pg_port
pg_pass=$(gen_random_string 24)
pg_db="xui"
pg_host="127.0.0.1"
pg_port="5432"
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
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
local existing_owner=""
existing_owner=$(sudo -u postgres psql -tAc \
"SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_database WHERE datname='${pg_db}'" 2> /dev/null \
| tr -d '[:space:]')
if [[ -n "${existing_owner}" && "${existing_owner}" != "postgres" ]]; then
pg_user="${existing_owner}"
else
pg_user=$(gen_random_string 8)
fi
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}@${pg_host}:${pg_port}/${pg_db}?sslmode=disable"
return 0
}
# Installs the PostgreSQL client tools (pg_dump/pg_restore) used by in-panel backup.
pg_ensure_client() {
if command -v pg_dump > /dev/null 2>&1 && command -v pg_restore > /dev/null 2>&1; then
return 0
fi
echo -e "${yellow}Installing PostgreSQL client tools (pg_dump/pg_restore)...${plain}" >&2
case "${release}" in
ubuntu | debian | armbian)
apt-get update >&2 && apt-get install -y -q postgresql-client >&2 || return 1
;;
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
dnf install -y -q postgresql >&2 || return 1
;;
centos)
if [[ "${VERSION_ID}" =~ ^7 ]]; then
yum install -y postgresql >&2 || return 1
else
dnf install -y -q postgresql >&2 || return 1
fi
;;
arch | manjaro | parch)
pacman -Sy --noconfirm postgresql >&2 || return 1
;;
opensuse-tumbleweed | opensuse-leap)
zypper -q install -y postgresql >&2 || return 1
;;
alpine)
apk add --no-cache postgresql-client >&2 || return 1
;;
*)
return 1
;;
esac
command -v pg_dump > /dev/null 2>&1 && command -v pg_restore > /dev/null 2>&1
}
# Writes XUI_DB_TYPE/XUI_DB_DSN into the service env file, preserving other entries.
pg_write_env() {
local dsn="$1" envfile
envfile="$(xui_env_file_path)"
install -d -m 755 "$(dirname "$envfile")"
touch "$envfile"
sed -i '/^XUI_DB_TYPE=/d; /^XUI_DB_DSN=/d' "$envfile"
{
echo "XUI_DB_TYPE=postgres"
echo "XUI_DB_DSN=${dsn}"
} >> "$envfile"
chmod 600 "$envfile"
}
pg_install_server_action() {
if postgresql_installed; then
LOGI "PostgreSQL already appears to be installed on this system."
confirm "Run setup anyway (ensures the xui database/user exist)?" "n" || return 0
fi
LOGI "Installing PostgreSQL server and creating a dedicated user/database..."
local dsn
dsn=$(pg_install_local)
if [[ $? -ne 0 || -z "$dsn" ]]; then
LOGE "PostgreSQL installation failed."
return 1
fi
PG_LAST_DSN="$dsn"
pg_ensure_client || LOGE "Could not install pg_dump/pg_restore (panel DB backup may be unavailable)."
echo ""
LOGI "PostgreSQL is installed and ready."
echo -e "${green}Connection DSN:${plain} ${dsn}"
echo -e "${yellow}Use option 2 to migrate your SQLite data and switch the panel to PostgreSQL.${plain}"
}
# Copies the current SQLite data into PostgreSQL, then switches the panel over.
migrate_to_postgres() {
if [[ ! -x "${xui_folder}/x-ui" ]]; then
LOGE "x-ui is not installed."
return 1
fi
echo ""
echo -e "${yellow}This copies your current SQLite data into a PostgreSQL database,${plain}"
echo -e "${yellow}then switches the panel to PostgreSQL and restarts it.${plain}"
echo -e "${yellow}The destination PostgreSQL database must be empty.${plain}"
confirm "Continue?" "n" || return 0
local dsn="" pg_mode
if [[ -n "$PG_LAST_DSN" ]]; then
echo -e "A PostgreSQL database was created in this session:"
echo -e " ${green}${PG_LAST_DSN}${plain}"
confirm "Migrate into this database?" "y" && dsn="$PG_LAST_DSN"
fi
if [[ -z "$dsn" ]]; then
echo ""
echo -e "${green}\t1.${plain} Install PostgreSQL locally and create a dedicated user/db (recommended)"
echo -e "${green}\t2.${plain} 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 "$dsn" ]]; do
read -rp "Enter PostgreSQL DSN (postgres://user:pass@host:port/dbname?sslmode=disable): " dsn
dsn="${dsn// /}"
done
else
LOGI "Installing PostgreSQL locally (this may take a moment)..."
dsn=$(pg_install_local)
if [[ $? -ne 0 || -z "$dsn" ]]; then
LOGE "PostgreSQL installation failed. Aborting migration."
return 1
fi
PG_LAST_DSN="$dsn"
fi
fi
pg_ensure_client || LOGE "Could not install pg_dump/pg_restore (in-panel DB backup/restore may be unavailable)."
LOGI "Stopping panel to take a consistent snapshot..."
stop 0 > /dev/null 2>&1
echo ""
LOGI "Migrating data into PostgreSQL..."
if ! ${xui_folder}/x-ui migrate-db --dsn "$dsn"; then
LOGE "Migration failed. The panel was NOT switched to PostgreSQL."
start 0 > /dev/null 2>&1
return 1
fi
pg_write_env "$dsn"
LOGI "Wrote database settings to $(xui_env_file_path) (XUI_DB_TYPE=postgres)."
LOGI "Restarting panel on PostgreSQL..."
restart 0
sleep 1
if check_status; then
LOGI "Migration complete. The panel is now running on PostgreSQL."
else
LOGE "Panel did not come up. Check logs (option 16). Your SQLite data is left intact."
fi
}
postgresql_menu() {
echo -e "${green}\t1.${plain} ${green}Install${plain} PostgreSQL (server + client + xui db)"
echo -e "${green}\t2.${plain} Migrate SQLite ${green}->${plain} PostgreSQL"
echo -e "${green}\t3.${plain} Status (clusters & port 5432)"
echo -e "${green}\t4.${plain} ${green}Start${plain} PostgreSQL"
echo -e "${green}\t5.${plain} ${red}Stop${plain} PostgreSQL"
echo -e "${green}\t6.${plain} Restart PostgreSQL"
echo -e "${green}\t7.${plain} ${green}Enable${plain} Autostart on boot"
echo -e "${green}\t8.${plain} View PostgreSQL Log"
echo -e "${green}\t0.${plain} Back to Main Menu"
read -rp "Choose an option: " choice
case "$choice" in
0)
show_menu
;;
1)
pg_install_server_action
postgresql_menu
;;
2)
migrate_to_postgres
postgresql_menu
;;
3)
postgresql_status
postgresql_menu
;;
4)
postgresql_start
postgresql_menu
;;
5)
postgresql_stop
postgresql_menu
;;
6)
postgresql_restart
postgresql_menu
;;
7)
postgresql_enable
postgresql_menu
;;
8)
postgresql_log
postgresql_menu
;;
*)
echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
postgresql_menu
;;
esac
}
show_usage() {
echo -e "┌────────────────────────────────────────────────────────────────┐
${blue}x-ui control menu usages (subcommands):${plain}
@@ -2342,10 +2743,12 @@ show_menu() {
${green}24.${plain} Enable BBR │
${green}25.${plain} Update Geo Files │
${green}26.${plain} Speedtest by Ookla │
│────────────────────────────────────────────────│
${green}27.${plain} PostgreSQL Management │
╚────────────────────────────────────────────────╝
"
show_status
echo && read -rp "Please enter your selection [0-26]: " num
echo && read -rp "Please enter your selection [0-27]: " num
case "${num}" in
0)
@@ -2429,8 +2832,11 @@ show_menu() {
26)
run_speedtest
;;
27)
postgresql_menu
;;
*)
LOGE "Please enter the correct number [0-26]"
LOGE "Please enter the correct number [0-27]"
;;
esac
}