| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 |
- package repository
- import (
- "context"
- "database/sql"
- "database/sql/driver"
- "errors"
- "fmt"
- "io"
- "os/exec"
- "sync"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/Wei-Shaw/sub2api/internal/service"
- )
- // PgDumper implements service.DBDumper using pg_dump/psql
- type PgDumper struct {
- cfg *config.DatabaseConfig
- db *sql.DB
- commandContext func(context.Context, string, ...string) *exec.Cmd
- }
- // NewPgDumper creates a new PgDumper
- func NewPgDumper(cfg *config.Config, db *sql.DB) service.DBDumper {
- return &PgDumper{
- cfg: &cfg.Database,
- db: db,
- commandContext: exec.CommandContext,
- }
- }
- // Dump executes pg_dump and returns a streaming reader of the output
- func (d *PgDumper) Dump(ctx context.Context) (io.ReadCloser, error) {
- if d.db == nil {
- return nil, errors.New("acquire backup migration lock: nil sql db")
- }
- lockConn, err := d.db.Conn(ctx)
- if err != nil {
- return nil, fmt.Errorf("acquire backup migration lock connection: %w", err)
- }
- if err := pgAdvisoryLock(ctx, lockConn); err != nil {
- // Lock acquisition can fail after PostgreSQL accepted the request. Discard
- // the session because ownership is ambiguous and advisory locks are session-scoped.
- discardSQLConnection(lockConn)
- return nil, fmt.Errorf("acquire backup migration lock: %w", err)
- }
- releaseLock := func() error {
- return releaseBackupMigrationLock(lockConn)
- }
- args := []string{
- "-h", d.cfg.Host,
- "-p", fmt.Sprintf("%d", d.cfg.Port),
- "-U", d.cfg.User,
- "-d", d.cfg.DBName,
- "--no-owner",
- "--no-acl",
- "--clean",
- "--if-exists",
- }
- commandContext := d.commandContext
- if commandContext == nil {
- commandContext = exec.CommandContext
- }
- cmd := commandContext(ctx, "pg_dump", args...)
- if d.cfg.Password != "" {
- cmd.Env = append(cmd.Environ(), "PGPASSWORD="+d.cfg.Password)
- }
- if d.cfg.SSLMode != "" {
- cmd.Env = append(cmd.Environ(), "PGSSLMODE="+d.cfg.SSLMode)
- }
- stdout, err := cmd.StdoutPipe()
- if err != nil {
- return nil, errors.Join(fmt.Errorf("create stdout pipe: %w", err), releaseLock())
- }
- if err := cmd.Start(); err != nil {
- _ = stdout.Close()
- return nil, errors.Join(fmt.Errorf("start pg_dump: %w", err), releaseLock())
- }
- return &cmdReadCloser{ReadCloser: stdout, cmd: cmd, release: releaseLock}, nil
- }
- func releaseBackupMigrationLock(conn *sql.Conn) error {
- unlockCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- if err := pgAdvisoryUnlock(unlockCtx, conn); err != nil {
- // A failed unlock must not return a possibly lock-owning session to the pool.
- discardSQLConnection(conn)
- return fmt.Errorf("release backup migration lock: %w", err)
- }
- if err := conn.Close(); err != nil {
- return fmt.Errorf("close backup migration lock connection: %w", err)
- }
- return nil
- }
- func discardSQLConnection(conn *sql.Conn) {
- _ = conn.Raw(func(any) error { return driver.ErrBadConn })
- _ = conn.Close()
- }
- // Restore executes psql to restore from a streaming reader
- func (d *PgDumper) Restore(ctx context.Context, data io.Reader) error {
- args := []string{
- "-h", d.cfg.Host,
- "-p", fmt.Sprintf("%d", d.cfg.Port),
- "-U", d.cfg.User,
- "-d", d.cfg.DBName,
- "--single-transaction",
- }
- cmd := exec.CommandContext(ctx, "psql", args...)
- if d.cfg.Password != "" {
- cmd.Env = append(cmd.Environ(), "PGPASSWORD="+d.cfg.Password)
- }
- if d.cfg.SSLMode != "" {
- cmd.Env = append(cmd.Environ(), "PGSSLMODE="+d.cfg.SSLMode)
- }
- cmd.Stdin = data
- output, err := cmd.CombinedOutput()
- if err != nil {
- return fmt.Errorf("%v: %s", err, string(output))
- }
- return nil
- }
- // cmdReadCloser wraps a command stdout pipe and waits for the process on Close
- type cmdReadCloser struct {
- io.ReadCloser
- cmd *exec.Cmd
- release func() error
- closeOnce sync.Once
- closeErr error
- }
- func (c *cmdReadCloser) Close() error {
- c.closeOnce.Do(func() {
- var closeErrs []error
- if err := c.ReadCloser.Close(); err != nil {
- closeErrs = append(closeErrs, fmt.Errorf("close pg_dump stdout: %w", err))
- }
- if err := c.cmd.Wait(); err != nil {
- closeErrs = append(closeErrs, fmt.Errorf("pg_dump exited with error: %w", err))
- }
- if c.release != nil {
- if err := c.release(); err != nil {
- closeErrs = append(closeErrs, err)
- }
- }
- c.closeErr = errors.Join(closeErrs...)
- })
- return c.closeErr
- }
|