diff --git a/internal/web/service/server.go b/internal/web/service/server.go index d6d4ac3ee..733b188c8 100644 --- a/internal/web/service/server.go +++ b/internal/web/service/server.go @@ -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) } diff --git a/internal/web/service/server_pg_restore_test.go b/internal/web/service/server_pg_restore_test.go index ccd28e9c4..29add7d58 100644 --- a/internal/web/service/server_pg_restore_test.go +++ b/internal/web/service/server_pg_restore_test.go @@ -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", diff --git a/x-ui.sh b/x-ui.sh index ac45f8f51..94e74198f 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -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 +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