133 lines
3.6 KiB
Go
133 lines
3.6 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// CreateBackup creates a compressed PostgreSQL dump before migrations
|
|
// Returns backup filename and error
|
|
// If pg_dump is not available, returns nil error with warning
|
|
func CreateBackup(ctx context.Context, cfg *Config, operation string) (string, error) {
|
|
// Check if pg_dump is available
|
|
if _, err := exec.LookPath("pg_dump"); err != nil {
|
|
fmt.Println("[WARN] pg_dump not found - skipping backup")
|
|
fmt.Println("[WARN] Install PostgreSQL client tools for automatic backups:")
|
|
fmt.Println("[WARN] Ubuntu/Debian: sudo apt-get install postgresql-client")
|
|
fmt.Println("[WARN] macOS: brew install postgresql")
|
|
fmt.Println("[WARN] Arch: sudo pacman -S postgresql-libs")
|
|
return "", nil // Don't fail, just warn
|
|
}
|
|
|
|
// Ensure backup directory exists
|
|
if err := os.MkdirAll(cfg.BackupDir, 0o755); err != nil {
|
|
return "", errors.Wrap(err, "failed to create backup directory")
|
|
}
|
|
|
|
// Generate filename: YYYYMMDD_HHmmss_pre_{operation}.sql.gz
|
|
timestamp := time.Now().Format("20060102_150405")
|
|
filename := filepath.Join(cfg.BackupDir,
|
|
fmt.Sprintf("%s_pre_%s.sql.gz", timestamp, operation))
|
|
|
|
// Check if gzip is available
|
|
useGzip := true
|
|
if _, err := exec.LookPath("gzip"); err != nil {
|
|
fmt.Println("[WARN] gzip not found - using uncompressed backup")
|
|
useGzip = false
|
|
filename = filepath.Join(cfg.BackupDir,
|
|
fmt.Sprintf("%s_pre_%s.sql", timestamp, operation))
|
|
}
|
|
|
|
// Build pg_dump command
|
|
var cmd *exec.Cmd
|
|
if useGzip {
|
|
// Use shell to pipe pg_dump through gzip
|
|
pgDumpCmd := fmt.Sprintf(
|
|
"pg_dump -h %s -p %d -U %s -d %s --no-owner --no-acl --clean --if-exists | gzip > %s",
|
|
cfg.Host,
|
|
cfg.Port,
|
|
cfg.User,
|
|
cfg.DB,
|
|
filename,
|
|
)
|
|
cmd = exec.CommandContext(ctx, "sh", "-c", pgDumpCmd)
|
|
} else {
|
|
cmd = exec.CommandContext(ctx, "pg_dump",
|
|
"-h", cfg.Host,
|
|
"-p", fmt.Sprint(cfg.Port),
|
|
"-U", cfg.User,
|
|
"-d", cfg.DB,
|
|
"-f", filename,
|
|
"--no-owner",
|
|
"--no-acl",
|
|
"--clean",
|
|
"--if-exists",
|
|
)
|
|
}
|
|
|
|
// Set password via environment variable
|
|
cmd.Env = append(os.Environ(),
|
|
fmt.Sprintf("PGPASSWORD=%s", cfg.Password))
|
|
|
|
// Run backup
|
|
if err := cmd.Run(); err != nil {
|
|
return "", errors.Wrap(err, "pg_dump failed")
|
|
}
|
|
|
|
// Get file size for logging
|
|
info, err := os.Stat(filename)
|
|
if err != nil {
|
|
return filename, errors.Wrap(err, "stat backup file")
|
|
}
|
|
|
|
sizeMB := float64(info.Size()) / 1024 / 1024
|
|
fmt.Printf("[INFO] Backup created: %s (%.2f MB)\n", filename, sizeMB)
|
|
|
|
return filename, nil
|
|
}
|
|
|
|
// CleanOldBackups keeps only the N most recent backups
|
|
func CleanOldBackups(cfg *Config, keepCount int) error {
|
|
// Get all backup files (both .sql and .sql.gz)
|
|
sqlFiles, err := filepath.Glob(filepath.Join(cfg.BackupDir, "*.sql"))
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to list .sql backups")
|
|
}
|
|
|
|
gzFiles, err := filepath.Glob(filepath.Join(cfg.BackupDir, "*.sql.gz"))
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to list .sql.gz backups")
|
|
}
|
|
|
|
files := append(sqlFiles, gzFiles...)
|
|
|
|
if len(files) <= keepCount {
|
|
return nil // Nothing to clean
|
|
}
|
|
|
|
// Sort files by modification time (newest first)
|
|
sort.Slice(files, func(i, j int) bool {
|
|
iInfo, _ := os.Stat(files[i])
|
|
jInfo, _ := os.Stat(files[j])
|
|
return iInfo.ModTime().After(jInfo.ModTime())
|
|
})
|
|
|
|
// Delete old backups
|
|
for i := keepCount; i < len(files); i++ {
|
|
if err := os.Remove(files[i]); err != nil {
|
|
fmt.Printf("[WARN] Failed to remove old backup %s: %v\n", files[i], err)
|
|
} else {
|
|
fmt.Printf("[INFO] Removed old backup: %s\n", filepath.Base(files[i]))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|