feat(db): add pgclient command to install or upgrade PostgreSQL client tools

Restoring a panel backup made by a newer pg_dump fails when the host's
pg_restore is older, and the existing pg_ensure_client only installs the
distribution package when the tools are missing - it can never upgrade,
and distribution repositories often cap below the required major.

Add pg_upgrade_client to x-ui.sh, exposed as 'x-ui pgclient [major]' and
as a PostgreSQL menu entry: it checks the installed pg_restore major,
tries the distribution package for the exact requested major first, and
falls back to the official PostgreSQL repository (apt on Debian/Ubuntu,
yum/dnf on Enterprise Linux, with a /usr/pgsql PATH symlink fallback);
Arch, Alpine and openSUSE install their current package. The panel's
dump-version mismatch error now names the ready-to-copy command with
the exact major parsed from the dump header.
This commit is contained in:
MHSanaei
2026-07-06 09:24:18 +02:00
parent de70ecb026
commit f36f481e02
3 changed files with 112 additions and 4 deletions
+1 -1
View File
@@ -1640,7 +1640,7 @@ func pgRestoreReadFailureError(probeOutput, localVersion string) error {
localVersion = "unknown"
}
if major, known := pgArchiveVersionIntroducedIn[m[1]]; known {
return common.NewErrorf("This backup was created by pg_dump from PostgreSQL %d or newer, but the server's pg_restore is version %s and cannot read it; upgrade the postgresql-client package to version %d or newer and retry the import", major, localVersion, major)
return common.NewErrorf("This backup was created by pg_dump from PostgreSQL %d or newer, but the server's pg_restore is version %s and cannot read it; run 'x-ui pgclient %d' on the server (or upgrade the postgresql-client package to version %d or newer), then retry the import", major, localVersion, major, major)
}
return common.NewErrorf("This backup was created by a newer pg_dump than the server's pg_restore (version %s) can read; upgrade the postgresql-client package and retry the import", localVersion)
}
@@ -13,13 +13,13 @@ func TestPgRestoreReadFailureError(t *testing.T) {
name: "dump from postgres 17 on older client",
probeOutput: "pg_restore: error: unsupported version (1.16) in file header",
localVersion: "16.4",
want: "This backup was created by pg_dump from PostgreSQL 17 or newer, but the server's pg_restore is version 16.4 and cannot read it; upgrade the postgresql-client package to version 17 or newer and retry the import",
want: "This backup was created by pg_dump from PostgreSQL 17 or newer, but the server's pg_restore is version 16.4 and cannot read it; run 'x-ui pgclient 17' on the server (or upgrade the postgresql-client package to version 17 or newer), then retry the import",
},
{
name: "dump from postgres 16 on older client",
probeOutput: "pg_restore: error: unsupported version (1.15) in file header",
localVersion: "15.8",
want: "This backup was created by pg_dump from PostgreSQL 16 or newer, but the server's pg_restore is version 15.8 and cannot read it; upgrade the postgresql-client package to version 16 or newer and retry the import",
want: "This backup was created by pg_dump from PostgreSQL 16 or newer, but the server's pg_restore is version 15.8 and cannot read it; run 'x-ui pgclient 16' on the server (or upgrade the postgresql-client package to version 16 or newer), then retry the import",
},
{
name: "archive version newer than any known mapping",
@@ -31,7 +31,7 @@ func TestPgRestoreReadFailureError(t *testing.T) {
name: "local version could not be determined",
probeOutput: "pg_restore: error: unsupported version (1.16) in file header",
localVersion: "",
want: "This backup was created by pg_dump from PostgreSQL 17 or newer, but the server's pg_restore is version unknown and cannot read it; upgrade the postgresql-client package to version 17 or newer and retry the import",
want: "This backup was created by pg_dump from PostgreSQL 17 or newer, but the server's pg_restore is version unknown and cannot read it; run 'x-ui pgclient 17' on the server (or upgrade the postgresql-client package to version 17 or newer), then retry the import",
},
{
name: "unrelated read failure passes through",
+108
View File
@@ -3012,6 +3012,104 @@ pg_ensure_client() {
command -v pg_dump > /dev/null 2>&1 && command -v pg_restore > /dev/null 2>&1
}
# Prints the major version of the installed pg_restore, or nothing when absent.
pg_client_major() {
command -v pg_restore > /dev/null 2>&1 || return 1
pg_restore --version 2> /dev/null | grep -oE '[0-9]+' | head -n 1
}
# Installs or upgrades the PostgreSQL client tools (pg_dump/pg_restore) so their
# major version is at least $1 (e.g. 17); with no argument any installed version
# is accepted. Falls back to the official PostgreSQL package repository when the
# distribution one is too old. Restoring a panel backup made by a newer pg_dump
# needs this: x-ui pgclient <major>
pg_upgrade_client() {
local want="$1" have
if [[ -n "$want" && ! "$want" =~ ^[0-9]+$ ]]; then
LOGE "Invalid PostgreSQL major version '${want}' (expected a number like 17)."
return 1
fi
have=$(pg_client_major)
if [[ -n "$have" ]]; then
if [[ -z "$want" || "$have" -ge "$want" ]]; then
LOGI "PostgreSQL client tools are already installed (version ${have})."
return 0
fi
LOGI "Installed PostgreSQL client tools are version ${have}; version ${want} or newer is required."
fi
if [[ "${running_in_docker}" == "true" ]]; then
LOGI "Note: packages installed inside the container are lost when the container is recreated."
fi
case "${release}" in
ubuntu | debian | armbian)
apt-get update >&2 || return 1
if [[ -z "$want" ]]; then
apt-get install -y -q postgresql-client >&2 || return 1
elif ! apt-get install -y -q "postgresql-client-${want}" >&2; then
LOGI "postgresql-client-${want} is not in the distribution repositories; adding the official PostgreSQL apt repository..."
apt-get install -y -q postgresql-common ca-certificates >&2 || return 1
/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y >&2 || return 1
apt-get install -y -q "postgresql-client-${want}" >&2 || return 1
fi
;;
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol | centos)
local pkg_mgr="dnf"
command -v dnf > /dev/null 2>&1 || pkg_mgr="yum"
if [[ -z "$want" ]]; then
"$pkg_mgr" install -y -q postgresql >&2 || return 1
elif ! "$pkg_mgr" install -y -q "postgresql${want}" >&2; then
local elver
elver=$(rpm -E %rhel 2> /dev/null)
if [[ ! "$elver" =~ ^[0-9]+$ ]]; then
LOGE "Could not determine the Enterprise Linux release; install the PostgreSQL ${want} client tools manually."
return 1
fi
LOGI "postgresql${want} is not in the enabled repositories; adding the official PostgreSQL yum repository..."
"$pkg_mgr" install -y "https://download.postgresql.org/pub/repos/yum/reporpms/EL-${elver}-$(uname -m)/pgdg-redhat-repo-latest.noarch.rpm" >&2 || return 1
if [[ "$pkg_mgr" == "dnf" ]]; then
dnf -qy module disable postgresql >&2 || true
fi
"$pkg_mgr" install -y -q "postgresql${want}" >&2 || return 1
fi
;;
arch | manjaro | parch)
pacman -Sy --noconfirm postgresql >&2 || return 1
;;
opensuse-tumbleweed | opensuse-leap)
if [[ -z "$want" ]] || ! zypper -q install -y "postgresql${want}" >&2; then
zypper -q install -y postgresql >&2 || return 1
fi
;;
alpine)
if [[ -z "$want" ]] || ! apk add --no-cache "postgresql${want}-client" >&2; then
apk add --no-cache postgresql-client >&2 || return 1
fi
;;
*)
LOGE "Unsupported OS '${release}'; install the PostgreSQL client tools manually."
return 1
;;
esac
hash -r 2> /dev/null
have=$(pg_client_major)
if [[ -n "$want" && ( -z "$have" || "$have" -lt "$want" ) && -x "/usr/pgsql-${want}/bin/pg_restore" ]]; then
ln -sf "/usr/pgsql-${want}/bin/pg_dump" /usr/local/bin/pg_dump
ln -sf "/usr/pgsql-${want}/bin/pg_restore" /usr/local/bin/pg_restore
hash -r 2> /dev/null
have=$(pg_client_major)
fi
if [[ -z "$have" ]]; then
LOGE "pg_dump/pg_restore are still unavailable after installation."
return 1
fi
if [[ -n "$want" && "$have" -lt "$want" ]]; then
LOGE "PostgreSQL client tools are version ${have} after installation but ${want} or newer is required; install them manually."
return 1
fi
LOGI "PostgreSQL client tools are ready (version ${have})."
return 0
}
# Writes XUI_DB_TYPE/XUI_DB_DSN into the service env file, preserving other entries.
pg_write_env() {
local dsn="$1" envfile
@@ -3122,6 +3220,7 @@ postgresql_menu() {
echo -e "${green}\t7.${plain} ${green}Enable${plain} Autostart on boot"
echo -e "${green}\t8.${plain} View PostgreSQL Log"
echo -e "${green}\t9.${plain} Convert SQLite ${green}.db <-> .dump${plain}"
echo -e "${green}\t10.${plain} Install/Upgrade client tools (pg_dump/pg_restore)"
echo -e "${green}\t0.${plain} Back to Main Menu"
read -rp "Choose an option: " choice
case "$choice" in
@@ -3164,6 +3263,11 @@ postgresql_menu() {
migrate_db_prompt
postgresql_menu
;;
10)
read -rp "Required PostgreSQL major version (empty = any): " pg_client_ver
pg_upgrade_client "$pg_client_ver"
postgresql_menu
;;
*)
echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
postgresql_menu
@@ -3283,6 +3387,7 @@ show_usage() {
${blue}x-ui update-dev${plain} - Update to Dev channel (latest) │
${blue}x-ui update-all-geofiles${plain} - Update all geo files │
${blue}x-ui migrateDB [file]${plain} - Convert .db <-> .dump (SQLite) │
${blue}x-ui pgclient [ver]${plain} - Upgrade pg_dump/pg_restore tools │
${blue}x-ui legacy${plain} - Legacy version │
${blue}x-ui install${plain} - Install │
${blue}x-ui uninstall${plain} - Uninstall │
@@ -3486,6 +3591,9 @@ if [[ $# > 0 ]]; then
"migrateDB")
migrate_db "$2" "$3"
;;
"pgclient")
pg_upgrade_client "$2"
;;
*) show_usage ;;
esac
else