mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-01 16:29:29 +00:00
162 lines
4.2 KiB
Go
162 lines
4.2 KiB
Go
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
|
|
}
|