mirror of
https://github.com/eduardolat/pgbackweb.git
synced 2026-01-24 21:48:30 -06:00
271 lines
7.2 KiB
Go
271 lines
7.2 KiB
Go
package postgres
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
|
|
"github.com/eduardolat/pgbackweb/internal/util/strutil"
|
|
"github.com/orsinium-labs/enum"
|
|
)
|
|
|
|
/*
|
|
Important:
|
|
Versions supported by PG Back Web must be supported in PostgreSQL Version Policy
|
|
https://www.postgresql.org/support/versioning/
|
|
|
|
Backing up a database from an old unsupported version should not be allowed.
|
|
*/
|
|
|
|
type version struct {
|
|
Version string
|
|
PGDump string
|
|
PSQL string
|
|
}
|
|
|
|
type PGVersion enum.Member[version]
|
|
|
|
var (
|
|
PG13 = PGVersion{version{
|
|
Version: "13",
|
|
PGDump: "/usr/lib/postgresql/13/bin/pg_dump",
|
|
PSQL: "/usr/lib/postgresql/13/bin/psql",
|
|
}}
|
|
PG14 = PGVersion{version{
|
|
Version: "14",
|
|
PGDump: "/usr/lib/postgresql/14/bin/pg_dump",
|
|
PSQL: "/usr/lib/postgresql/14/bin/psql",
|
|
}}
|
|
PG15 = PGVersion{version{
|
|
Version: "15",
|
|
PGDump: "/usr/lib/postgresql/15/bin/pg_dump",
|
|
PSQL: "/usr/lib/postgresql/15/bin/psql",
|
|
}}
|
|
PG16 = PGVersion{version{
|
|
Version: "16",
|
|
PGDump: "/usr/lib/postgresql/16/bin/pg_dump",
|
|
PSQL: "/usr/lib/postgresql/16/bin/psql",
|
|
}}
|
|
PG17 = PGVersion{version{
|
|
Version: "17",
|
|
PGDump: "/usr/lib/postgresql/17/bin/pg_dump",
|
|
PSQL: "/usr/lib/postgresql/17/bin/psql",
|
|
}}
|
|
|
|
PGVersions = []PGVersion{PG13, PG14, PG15, PG16, PG17}
|
|
)
|
|
|
|
type Client struct{}
|
|
|
|
func New() *Client {
|
|
return &Client{}
|
|
}
|
|
|
|
// ParseVersion returns the PGVersion enum member for the given PostgreSQL
|
|
// version as a string.
|
|
func (Client) ParseVersion(version string) (PGVersion, error) {
|
|
switch version {
|
|
case "13":
|
|
return PG13, nil
|
|
case "14":
|
|
return PG14, nil
|
|
case "15":
|
|
return PG15, nil
|
|
case "16":
|
|
return PG16, nil
|
|
case "17":
|
|
return PG17, nil
|
|
default:
|
|
return PGVersion{}, fmt.Errorf("pg version not allowed: %s", version)
|
|
}
|
|
}
|
|
|
|
// Test tests the connection to the PostgreSQL database
|
|
func (Client) Test(version PGVersion, connString string) error {
|
|
cmd := exec.Command(version.Value.PSQL, connString, "-c", "SELECT 1;")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"error running psql test v%s: %s",
|
|
version.Value.Version, output,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DumpParams contains the parameters for the pg_dump command
|
|
type DumpParams struct {
|
|
// DataOnly (--data-only): Dump only the data, not the schema (data definitions).
|
|
// Table data, large objects, and sequence values are dumped.
|
|
DataOnly bool
|
|
|
|
// SchemaOnly (--schema-only): Dump only the object definitions (schema), not data.
|
|
SchemaOnly bool
|
|
|
|
// Clean (--clean): Output commands to DROP all the dumped database objects
|
|
// prior to outputting the commands for creating them. This option is useful
|
|
// when the restore is to overwrite an existing database. If any of the
|
|
// objects do not exist in the destination database, ignorable error messages
|
|
// will be reported during restore, unless --if-exists is also specified.
|
|
Clean bool
|
|
|
|
// IfExists (--if-exists): Use DROP ... IF EXISTS commands to drop objects in
|
|
// --clean mode. This suppresses “does not exist” errors that might otherwise
|
|
// be reported. This option is not valid unless --clean is also specified.
|
|
IfExists bool
|
|
|
|
// Create (--create): Begin the output with a command to create the database
|
|
// itself and reconnect to the created database. (With a script of this form,
|
|
// it doesn't matter which database in the destination installation you
|
|
// connect to before running the script.) If --clean is also specified, the
|
|
// script drops and recreates the target database before reconnecting to it.
|
|
Create bool
|
|
|
|
// NoComments (--no-comments): Do not dump comments.
|
|
NoComments bool
|
|
}
|
|
|
|
// Dump runs the pg_dump command with the given parameters. It returns the SQL
|
|
// dump as an io.Reader.
|
|
func (Client) Dump(
|
|
version PGVersion, connString string, params ...DumpParams,
|
|
) io.Reader {
|
|
pickedParams := DumpParams{}
|
|
if len(params) > 0 {
|
|
pickedParams = params[0]
|
|
}
|
|
|
|
args := []string{connString}
|
|
if pickedParams.DataOnly {
|
|
args = append(args, "--data-only")
|
|
}
|
|
if pickedParams.SchemaOnly {
|
|
args = append(args, "--schema-only")
|
|
}
|
|
if pickedParams.Clean {
|
|
args = append(args, "--clean")
|
|
}
|
|
if pickedParams.IfExists {
|
|
args = append(args, "--if-exists")
|
|
}
|
|
if pickedParams.Create {
|
|
args = append(args, "--create")
|
|
}
|
|
if pickedParams.NoComments {
|
|
args = append(args, "--no-comments")
|
|
}
|
|
|
|
errorBuffer := &bytes.Buffer{}
|
|
reader, writer := io.Pipe()
|
|
cmd := exec.Command(version.Value.PGDump, args...)
|
|
cmd.Stdout = writer
|
|
cmd.Stderr = errorBuffer
|
|
|
|
go func() {
|
|
defer writer.Close()
|
|
if err := cmd.Run(); err != nil {
|
|
writer.CloseWithError(fmt.Errorf(
|
|
"error running pg_dump v%s: %s",
|
|
version.Value.Version, errorBuffer.String(),
|
|
))
|
|
}
|
|
}()
|
|
|
|
return reader
|
|
}
|
|
|
|
// DumpZip runs the pg_dump command with the given parameters and returns the
|
|
// ZIP-compressed SQL dump as an io.Reader.
|
|
func (c *Client) DumpZip(
|
|
version PGVersion, connString string, params ...DumpParams,
|
|
) io.Reader {
|
|
dumpReader := c.Dump(version, connString, params...)
|
|
reader, writer := io.Pipe()
|
|
|
|
go func() {
|
|
defer writer.Close()
|
|
|
|
zipWriter := zip.NewWriter(writer)
|
|
defer zipWriter.Close()
|
|
|
|
fileWriter, err := zipWriter.Create("dump.sql")
|
|
if err != nil {
|
|
writer.CloseWithError(fmt.Errorf("error creating zip file: %w", err))
|
|
return
|
|
}
|
|
|
|
if _, err := io.Copy(fileWriter, dumpReader); err != nil {
|
|
writer.CloseWithError(fmt.Errorf("error writing to zip file: %w", err))
|
|
return
|
|
}
|
|
}()
|
|
|
|
return reader
|
|
}
|
|
|
|
// RestoreZip downloads or copies the ZIP from the given url or path, unzips it,
|
|
// and runs the psql command to restore the database.
|
|
//
|
|
// The ZIP file must contain a dump.sql file with the SQL dump to restore.
|
|
//
|
|
// - version: PostgreSQL version to use for the restore
|
|
// - connString: connection string to the database
|
|
// - isLocal: whether the ZIP file is local or a URL
|
|
// - zipURLOrPath: URL or path to the ZIP file
|
|
func (Client) RestoreZip(
|
|
version PGVersion, connString string, isLocal bool, zipURLOrPath string,
|
|
) error {
|
|
workDir, err := os.MkdirTemp("", "pbw-restore-*")
|
|
if err != nil {
|
|
return fmt.Errorf("error creating temp dir: %w", err)
|
|
}
|
|
defer os.RemoveAll(workDir)
|
|
zipPath := strutil.CreatePath(true, workDir, "dump.zip")
|
|
dumpPath := strutil.CreatePath(true, workDir, "dump.sql")
|
|
|
|
if isLocal {
|
|
cmd := exec.Command("cp", zipURLOrPath, zipPath)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("error copying ZIP file to temp dir: %s", output)
|
|
}
|
|
}
|
|
|
|
if !isLocal {
|
|
cmd := exec.Command("wget", "--no-verbose", "-O", zipPath, zipURLOrPath)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("error downloading ZIP file: %s", output)
|
|
}
|
|
}
|
|
|
|
if _, err := os.Stat(zipPath); os.IsNotExist(err) {
|
|
return fmt.Errorf("zip file not found: %s", zipPath)
|
|
}
|
|
|
|
cmd := exec.Command("unzip", "-o", zipPath, "dump.sql", "-d", workDir)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("error unzipping ZIP file: %s", output)
|
|
}
|
|
|
|
if _, err := os.Stat(dumpPath); os.IsNotExist(err) {
|
|
return fmt.Errorf("dump.sql file not found in ZIP file: %s", zipPath)
|
|
}
|
|
|
|
cmd = exec.Command(version.Value.PSQL, connString, "-f", dumpPath)
|
|
output, err = cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"error running psql v%s command: %s",
|
|
version.Value.Version, output,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|