vibe coded the shit out of a db migration system
This commit is contained in:
133
internal/backup/backup.go
Normal file
133
internal/backup/backup.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"git.haelnorr.com/h/oslstats/internal/config"
|
||||
"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.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.DB.BackupDir, 0755); 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.DB.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.DB.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.DB.Host,
|
||||
cfg.DB.Port,
|
||||
cfg.DB.User,
|
||||
cfg.DB.DB,
|
||||
filename,
|
||||
)
|
||||
cmd = exec.CommandContext(ctx, "sh", "-c", pgDumpCmd)
|
||||
} else {
|
||||
cmd = exec.CommandContext(ctx, "pg_dump",
|
||||
"-h", cfg.DB.Host,
|
||||
"-p", fmt.Sprint(cfg.DB.Port),
|
||||
"-U", cfg.DB.User,
|
||||
"-d", cfg.DB.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.DB.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.Config, keepCount int) error {
|
||||
// Get all backup files (both .sql and .sql.gz)
|
||||
sqlFiles, err := filepath.Glob(filepath.Join(cfg.DB.BackupDir, "*.sql"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to list .sql backups")
|
||||
}
|
||||
|
||||
gzFiles, err := filepath.Glob(filepath.Join(cfg.DB.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
|
||||
}
|
||||
Reference in New Issue
Block a user