mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-07 05:14:27 +00:00
fix(db): probe dump readability before PostgreSQL import
pg_restore cannot read archives newer than itself, so importing a dump made by pg_dump from PostgreSQL 17+ into a panel with an older postgresql-client failed with a raw 'unsupported version (1.16) in file header' - and only after Xray had already been stopped for the restore. Probe the uploaded file with pg_restore --list first, which reads only the archive TOC without touching the database, so an unreadable dump is rejected before Xray is interrupted. When the failure is a dump-format version mismatch, translate it into a message naming the PostgreSQL version that produced the dump and the client version to install.
This commit is contained in:
@@ -1608,6 +1608,55 @@ func (s *ServerService) exportPostgresDB() ([]byte, error) {
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
|
||||
var (
|
||||
pgUnsupportedDumpVersionPattern = regexp.MustCompile(`unsupported version \((\d+\.\d+)\) in file header`)
|
||||
pgToolVersionPattern = regexp.MustCompile(`\d+(?:\.\d+)+`)
|
||||
)
|
||||
|
||||
var pgArchiveVersionIntroducedIn = map[string]int{
|
||||
"1.15": 16,
|
||||
"1.16": 17,
|
||||
}
|
||||
|
||||
// checkPgRestoreCanRead probes the dump with pg_restore --list (reads only the
|
||||
// TOC, no database connection) so an unreadable file fails before Xray is stopped.
|
||||
func checkPgRestoreCanRead(bin, dumpPath string) error {
|
||||
cmd := exec.CommandContext(context.Background(), bin, "--list", dumpPath)
|
||||
cmd.Stdout = io.Discard
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if cmd.Run() == nil {
|
||||
return nil
|
||||
}
|
||||
return pgRestoreReadFailureError(strings.TrimSpace(stderr.String()), pgRestoreVersion(bin))
|
||||
}
|
||||
|
||||
func pgRestoreReadFailureError(probeOutput, localVersion string) error {
|
||||
m := pgUnsupportedDumpVersionPattern.FindStringSubmatch(probeOutput)
|
||||
if m == nil {
|
||||
return common.NewErrorf("pg_restore cannot read this dump file: %s", probeOutput)
|
||||
}
|
||||
if localVersion == "" {
|
||||
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 a newer pg_dump than the server's pg_restore (version %s) can read; upgrade the postgresql-client package and retry the import", localVersion)
|
||||
}
|
||||
|
||||
func pgRestoreVersion(bin string) string {
|
||||
out, err := exec.CommandContext(context.Background(), bin, "--version").Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return parsePgToolVersion(string(out))
|
||||
}
|
||||
|
||||
func parsePgToolVersion(versionOutput string) string {
|
||||
return pgToolVersionPattern.FindString(versionOutput)
|
||||
}
|
||||
|
||||
func (s *ServerService) importPostgresDB(file multipart.File) error {
|
||||
header := make([]byte, 5)
|
||||
if _, err := file.ReadAt(header, 0); err != nil {
|
||||
@@ -1643,6 +1692,10 @@ func (s *ServerService) importPostgresDB(file multipart.File) error {
|
||||
return common.NewErrorf("Error closing temporary dump file: %v", err)
|
||||
}
|
||||
|
||||
if err := checkPgRestoreCanRead(bin, tempPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
xrayStopped := true
|
||||
defer func() {
|
||||
if xrayStopped {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package service
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPgRestoreReadFailureError(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
probeOutput string
|
||||
localVersion string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
name: "archive version newer than any known mapping",
|
||||
probeOutput: "pg_restore: error: unsupported version (1.17) in file header",
|
||||
localVersion: "17.2",
|
||||
want: "This backup was created by a newer pg_dump than the server's pg_restore (version 17.2) can read; upgrade the postgresql-client package and retry the import",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
name: "unrelated read failure passes through",
|
||||
probeOutput: "pg_restore: error: could not read from input file: end of file",
|
||||
localVersion: "16.4",
|
||||
want: "pg_restore cannot read this dump file: pg_restore: error: could not read from input file: end of file",
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := pgRestoreReadFailureError(tc.probeOutput, tc.localVersion)
|
||||
if err == nil {
|
||||
t.Fatal("pgRestoreReadFailureError returned nil, want error")
|
||||
}
|
||||
if err.Error() != tc.want {
|
||||
t.Errorf("pgRestoreReadFailureError(%q, %q) = %q, want %q", tc.probeOutput, tc.localVersion, err.Error(), tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePgToolVersion(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"plain", "pg_restore (PostgreSQL) 17.2\n", "17.2"},
|
||||
{"debian packaging suffix", "pg_restore (PostgreSQL) 16.10 (Debian 16.10-1.pgdg120+1)\n", "16.10"},
|
||||
{"three component version", "pg_restore (PostgreSQL) 9.6.24\n", "9.6.24"},
|
||||
{"no version present", "pg_restore malfunction\n", ""},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := parsePgToolVersion(tc.in)
|
||||
if got != tc.want {
|
||||
t.Errorf("parsePgToolVersion(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user