mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-12 17:04:21 +00:00
52f48e5ea6
* fix(backup): detect pg server major for version-aware pg_dump hints pg_dump aborts when its major version is older than the server's, so backup preflight now runs SHOW server_version_num against the live PG server and uses the detected major to drive: - the missing-pg_dump hint (names the exact postgresqlNN-client) - a new compat check that flags an installed-but-too-old pg_dump as not-ready, instead of letting backup fail partway through the dump Adds ParsePgDumpMajor helper for Debian/Homebrew/EDB output shapes, covered by table-driven unit tests. Normalizes nil ctx once at RunPreflight boundary so downstream exec.CommandContext calls are safe. Stubs detectPGServerMajor + checkPgDumpServerCompat for the sqliteonly build. UI: removes the duplicate static hint from the preflight alert box so the dynamic, actionable warning is the single source of truth. Drops the obsolete pgDumpHint i18n key from en/vi/zh locale files. Refs #829 * fix(docker): bump runtime base alpine 3.22 → 3.23 Alpine 3.23 main ships postgresql16-client, postgresql17-client, and postgresql18-client simultaneously. Alpine 3.22 only shipped up to postgresql17-client, so the backup preflight's dynamic hint to install postgresql18-client previously failed with "no such package" on PG 18 deployments. No client package is pre-bundled: the on-demand install via the Packages page now resolves for any supported PG major. Refs #829
105 lines
2.3 KiB
Go
105 lines
2.3 KiB
Go
//go:build !sqliteonly
|
|
|
|
package backup
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// DumpDatabase runs pg_dump and streams plain-SQL output to w.
|
|
// Uses a temporary .pgpass file (0600) to pass the password securely.
|
|
// The child process receives only PGPASSFILE, PATH, HOME, LC_ALL=C.
|
|
func DumpDatabase(ctx context.Context, dsn string, w io.Writer) error {
|
|
creds, err := ParseDSN(dsn)
|
|
if err != nil {
|
|
return fmt.Errorf("parse DSN: %w", err)
|
|
}
|
|
|
|
pgDump, err := exec.LookPath("pg_dump")
|
|
if err != nil {
|
|
return fmt.Errorf("pg_dump not found on PATH: %w", err)
|
|
}
|
|
|
|
tempDir, pgpassPath, err := WritePgpass(creds)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
args := []string{
|
|
"--host", creds.Host,
|
|
"--port", creds.Port,
|
|
"--username", creds.User,
|
|
"--dbname", creds.DBName,
|
|
"--format=plain",
|
|
"--clean",
|
|
"--if-exists",
|
|
"--no-owner",
|
|
"--no-privileges",
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, pgDump, args...)
|
|
cmd.Env = CleanEnv(pgpassPath)
|
|
cmd.Stdout = w
|
|
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
errMsg := strings.TrimSpace(stderr.String())
|
|
if errMsg == "" {
|
|
errMsg = err.Error()
|
|
}
|
|
return fmt.Errorf("pg_dump failed: %s", errMsg)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PgDumpVersion returns the version string from pg_dump --version.
|
|
func PgDumpVersion(ctx context.Context) (string, error) {
|
|
pgDump, err := exec.LookPath("pg_dump")
|
|
if err != nil {
|
|
return "", fmt.Errorf("pg_dump not found: %w", err)
|
|
}
|
|
|
|
out, err := exec.CommandContext(ctx, pgDump, "--version").Output()
|
|
if err != nil {
|
|
return "", fmt.Errorf("pg_dump --version: %w", err)
|
|
}
|
|
return strings.TrimSpace(string(out)), nil
|
|
}
|
|
|
|
// ParsePgDumpMajor extracts the PostgreSQL major version number from a
|
|
// pg_dump --version string. Returns 0 if parsing fails.
|
|
// Example inputs:
|
|
//
|
|
// "pg_dump (PostgreSQL) 17.9 (Debian 17.9-1.pgdg12+1)" -> 17
|
|
// "pg_dump (PostgreSQL) 18.3" -> 18
|
|
func ParsePgDumpMajor(version string) int {
|
|
const marker = "(PostgreSQL) "
|
|
idx := strings.Index(version, marker)
|
|
if idx < 0 {
|
|
return 0
|
|
}
|
|
rest := version[idx+len(marker):]
|
|
end := 0
|
|
for end < len(rest) && rest[end] >= '0' && rest[end] <= '9' {
|
|
end++
|
|
}
|
|
if end == 0 {
|
|
return 0
|
|
}
|
|
major, err := strconv.Atoi(rest[:end])
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return major
|
|
}
|