Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e363e1937f | |||
| df1ab2f55b | |||
| 0e050b2def | |||
| 62d58c77af |
@@ -4,8 +4,8 @@ This directory contains pre-compiled binaries for the DB Backup Tool across mult
|
|||||||
|
|
||||||
## Build Information
|
## Build Information
|
||||||
- **Version**: 3.42.50
|
- **Version**: 3.42.50
|
||||||
- **Build Time**: 2026-01-17_06:48:35_UTC
|
- **Build Time**: 2026-01-17_15:26:14_UTC
|
||||||
- **Git Commit**: dd1db84
|
- **Git Commit**: df1ab2f
|
||||||
|
|
||||||
## Recent Updates (v1.1.0)
|
## Recent Updates (v1.1.0)
|
||||||
- ✅ Fixed TUI progress display with line-by-line output
|
- ✅ Fixed TUI progress display with line-by-line output
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ func init() {
|
|||||||
restoreClusterCmd.Flags().BoolVar(&restoreForce, "force", false, "Skip safety checks and confirmations")
|
restoreClusterCmd.Flags().BoolVar(&restoreForce, "force", false, "Skip safety checks and confirmations")
|
||||||
restoreClusterCmd.Flags().BoolVar(&restoreCleanCluster, "clean-cluster", false, "Drop all existing user databases before restore (disaster recovery)")
|
restoreClusterCmd.Flags().BoolVar(&restoreCleanCluster, "clean-cluster", false, "Drop all existing user databases before restore (disaster recovery)")
|
||||||
restoreClusterCmd.Flags().IntVar(&restoreJobs, "jobs", 0, "Number of parallel decompression jobs (0 = auto)")
|
restoreClusterCmd.Flags().IntVar(&restoreJobs, "jobs", 0, "Number of parallel decompression jobs (0 = auto)")
|
||||||
restoreClusterCmd.Flags().IntVar(&restoreParallelDBs, "parallel-dbs", 0, "Number of databases to restore in parallel (0 = use config default, 1 = sequential)")
|
restoreClusterCmd.Flags().IntVar(&restoreParallelDBs, "parallel-dbs", 0, "Number of databases to restore in parallel (0 = use config default, 1 = sequential, -1 = auto-detect based on CPU/RAM)")
|
||||||
restoreClusterCmd.Flags().StringVar(&restoreWorkdir, "workdir", "", "Working directory for extraction (use when system disk is small, e.g. /mnt/storage/restore_tmp)")
|
restoreClusterCmd.Flags().StringVar(&restoreWorkdir, "workdir", "", "Working directory for extraction (use when system disk is small, e.g. /mnt/storage/restore_tmp)")
|
||||||
restoreClusterCmd.Flags().BoolVar(&restoreVerbose, "verbose", false, "Show detailed restore progress")
|
restoreClusterCmd.Flags().BoolVar(&restoreVerbose, "verbose", false, "Show detailed restore progress")
|
||||||
restoreClusterCmd.Flags().BoolVar(&restoreNoProgress, "no-progress", false, "Disable progress indicators")
|
restoreClusterCmd.Flags().BoolVar(&restoreNoProgress, "no-progress", false, "Disable progress indicators")
|
||||||
@@ -786,7 +786,12 @@ func runRestoreCluster(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Override cluster parallelism if --parallel-dbs is specified
|
// Override cluster parallelism if --parallel-dbs is specified
|
||||||
if restoreParallelDBs > 0 {
|
if restoreParallelDBs == -1 {
|
||||||
|
// Auto-detect optimal parallelism based on system resources
|
||||||
|
autoParallel := restore.CalculateOptimalParallel()
|
||||||
|
cfg.ClusterParallelism = autoParallel
|
||||||
|
log.Info("Auto-detected optimal parallelism for database restores", "parallel_dbs", autoParallel, "mode", "auto")
|
||||||
|
} else if restoreParallelDBs > 0 {
|
||||||
cfg.ClusterParallelism = restoreParallelDBs
|
cfg.ClusterParallelism = restoreParallelDBs
|
||||||
log.Info("Using custom parallelism for database restores", "parallel_dbs", restoreParallelDBs)
|
log.Info("Using custom parallelism for database restores", "parallel_dbs", restoreParallelDBs)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,57 @@ import (
|
|||||||
"github.com/shirou/gopsutil/v3/mem"
|
"github.com/shirou/gopsutil/v3/mem"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CalculateOptimalParallel returns the recommended number of parallel workers
|
||||||
|
// based on available system resources (CPU cores and RAM).
|
||||||
|
// This is a standalone function that can be called from anywhere.
|
||||||
|
// Returns 0 if resources cannot be detected.
|
||||||
|
func CalculateOptimalParallel() int {
|
||||||
|
cpuCores := runtime.NumCPU()
|
||||||
|
|
||||||
|
vmem, err := mem.VirtualMemory()
|
||||||
|
if err != nil {
|
||||||
|
// Fallback: use half of CPU cores if memory detection fails
|
||||||
|
if cpuCores > 1 {
|
||||||
|
return cpuCores / 2
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
memAvailableGB := float64(vmem.Available) / (1024 * 1024 * 1024)
|
||||||
|
|
||||||
|
// Each pg_restore worker needs approximately 2-4GB of RAM
|
||||||
|
// Use conservative 3GB per worker to avoid OOM
|
||||||
|
const memPerWorkerGB = 3.0
|
||||||
|
|
||||||
|
// Calculate limits
|
||||||
|
maxByMem := int(memAvailableGB / memPerWorkerGB)
|
||||||
|
maxByCPU := cpuCores
|
||||||
|
|
||||||
|
// Use the minimum of memory and CPU limits
|
||||||
|
recommended := maxByMem
|
||||||
|
if maxByCPU < recommended {
|
||||||
|
recommended = maxByCPU
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sensible bounds
|
||||||
|
if recommended < 1 {
|
||||||
|
recommended = 1
|
||||||
|
}
|
||||||
|
if recommended > 16 {
|
||||||
|
recommended = 16 // Cap at 16 to avoid diminishing returns
|
||||||
|
}
|
||||||
|
|
||||||
|
// If memory pressure is high (>80%), reduce parallelism
|
||||||
|
if vmem.UsedPercent > 80 && recommended > 1 {
|
||||||
|
recommended = recommended / 2
|
||||||
|
if recommended < 1 {
|
||||||
|
recommended = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return recommended
|
||||||
|
}
|
||||||
|
|
||||||
// PreflightResult contains all preflight check results
|
// PreflightResult contains all preflight check results
|
||||||
type PreflightResult struct {
|
type PreflightResult struct {
|
||||||
// Linux system checks
|
// Linux system checks
|
||||||
@@ -35,15 +86,17 @@ type PreflightResult struct {
|
|||||||
|
|
||||||
// LinuxChecks contains Linux kernel/system checks
|
// LinuxChecks contains Linux kernel/system checks
|
||||||
type LinuxChecks struct {
|
type LinuxChecks struct {
|
||||||
ShmMax int64 // /proc/sys/kernel/shmmax
|
ShmMax int64 // /proc/sys/kernel/shmmax
|
||||||
ShmAll int64 // /proc/sys/kernel/shmall
|
ShmAll int64 // /proc/sys/kernel/shmall
|
||||||
MemTotal uint64 // Total RAM in bytes
|
MemTotal uint64 // Total RAM in bytes
|
||||||
MemAvailable uint64 // Available RAM in bytes
|
MemAvailable uint64 // Available RAM in bytes
|
||||||
MemUsedPercent float64 // Memory usage percentage
|
MemUsedPercent float64 // Memory usage percentage
|
||||||
ShmMaxOK bool // Is shmmax sufficient?
|
CPUCores int // Number of CPU cores
|
||||||
ShmAllOK bool // Is shmall sufficient?
|
RecommendedParallel int // Auto-calculated optimal parallel count
|
||||||
MemAvailableOK bool // Is available RAM sufficient?
|
ShmMaxOK bool // Is shmmax sufficient?
|
||||||
IsLinux bool // Are we running on Linux?
|
ShmAllOK bool // Is shmall sufficient?
|
||||||
|
MemAvailableOK bool // Is available RAM sufficient?
|
||||||
|
IsLinux bool // Are we running on Linux?
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostgreSQLChecks contains PostgreSQL configuration checks
|
// PostgreSQLChecks contains PostgreSQL configuration checks
|
||||||
@@ -100,6 +153,7 @@ func (e *Engine) RunPreflightChecks(ctx context.Context, dumpsDir string, entrie
|
|||||||
// checkSystemResources uses gopsutil for cross-platform system checks
|
// checkSystemResources uses gopsutil for cross-platform system checks
|
||||||
func (e *Engine) checkSystemResources(result *PreflightResult) {
|
func (e *Engine) checkSystemResources(result *PreflightResult) {
|
||||||
result.Linux.IsLinux = runtime.GOOS == "linux"
|
result.Linux.IsLinux = runtime.GOOS == "linux"
|
||||||
|
result.Linux.CPUCores = runtime.NumCPU()
|
||||||
|
|
||||||
// Get memory info (works on Linux, macOS, Windows, BSD)
|
// Get memory info (works on Linux, macOS, Windows, BSD)
|
||||||
if vmem, err := mem.VirtualMemory(); err == nil {
|
if vmem, err := mem.VirtualMemory(); err == nil {
|
||||||
@@ -118,6 +172,9 @@ func (e *Engine) checkSystemResources(result *PreflightResult) {
|
|||||||
e.log.Warn("Could not detect system memory", "error", err)
|
e.log.Warn("Could not detect system memory", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate recommended parallel based on resources
|
||||||
|
result.Linux.RecommendedParallel = e.calculateRecommendedParallel(result)
|
||||||
|
|
||||||
// Linux-specific kernel checks (shmmax, shmall)
|
// Linux-specific kernel checks (shmmax, shmall)
|
||||||
if result.Linux.IsLinux {
|
if result.Linux.IsLinux {
|
||||||
e.checkLinuxKernel(result)
|
e.checkLinuxKernel(result)
|
||||||
@@ -434,6 +491,56 @@ func (e *Engine) calculateRecommendations(result *PreflightResult) {
|
|||||||
"recommended_locks", lockBoost)
|
"recommended_locks", lockBoost)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calculateRecommendedParallel determines optimal parallelism based on system resources
|
||||||
|
// Returns the recommended number of parallel workers for pg_restore
|
||||||
|
func (e *Engine) calculateRecommendedParallel(result *PreflightResult) int {
|
||||||
|
cpuCores := result.Linux.CPUCores
|
||||||
|
if cpuCores == 0 {
|
||||||
|
cpuCores = runtime.NumCPU()
|
||||||
|
}
|
||||||
|
|
||||||
|
memAvailableGB := float64(result.Linux.MemAvailable) / (1024 * 1024 * 1024)
|
||||||
|
|
||||||
|
// Each pg_restore worker needs approximately 2-4GB of RAM
|
||||||
|
// Use conservative 3GB per worker to avoid OOM
|
||||||
|
const memPerWorkerGB = 3.0
|
||||||
|
|
||||||
|
// Calculate limits
|
||||||
|
maxByMem := int(memAvailableGB / memPerWorkerGB)
|
||||||
|
maxByCPU := cpuCores
|
||||||
|
|
||||||
|
// Use the minimum of memory and CPU limits
|
||||||
|
recommended := maxByMem
|
||||||
|
if maxByCPU < recommended {
|
||||||
|
recommended = maxByCPU
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sensible bounds
|
||||||
|
if recommended < 1 {
|
||||||
|
recommended = 1
|
||||||
|
}
|
||||||
|
if recommended > 16 {
|
||||||
|
recommended = 16 // Cap at 16 to avoid diminishing returns
|
||||||
|
}
|
||||||
|
|
||||||
|
// If memory pressure is high (>80%), reduce parallelism
|
||||||
|
if result.Linux.MemUsedPercent > 80 && recommended > 1 {
|
||||||
|
recommended = recommended / 2
|
||||||
|
if recommended < 1 {
|
||||||
|
recommended = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.log.Info("Calculated recommended parallel",
|
||||||
|
"cpu_cores", cpuCores,
|
||||||
|
"mem_available_gb", fmt.Sprintf("%.1f", memAvailableGB),
|
||||||
|
"max_by_mem", maxByMem,
|
||||||
|
"max_by_cpu", maxByCPU,
|
||||||
|
"recommended", recommended)
|
||||||
|
|
||||||
|
return recommended
|
||||||
|
}
|
||||||
|
|
||||||
// printPreflightSummary prints a nice summary of all checks
|
// printPreflightSummary prints a nice summary of all checks
|
||||||
func (e *Engine) printPreflightSummary(result *PreflightResult) {
|
func (e *Engine) printPreflightSummary(result *PreflightResult) {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
@@ -446,6 +553,8 @@ func (e *Engine) printPreflightSummary(result *PreflightResult) {
|
|||||||
printCheck("Total RAM", humanize.Bytes(result.Linux.MemTotal), true)
|
printCheck("Total RAM", humanize.Bytes(result.Linux.MemTotal), true)
|
||||||
printCheck("Available RAM", humanize.Bytes(result.Linux.MemAvailable), result.Linux.MemAvailableOK || result.Linux.MemAvailable == 0)
|
printCheck("Available RAM", humanize.Bytes(result.Linux.MemAvailable), result.Linux.MemAvailableOK || result.Linux.MemAvailable == 0)
|
||||||
printCheck("Memory Usage", fmt.Sprintf("%.1f%%", result.Linux.MemUsedPercent), result.Linux.MemUsedPercent < 85)
|
printCheck("Memory Usage", fmt.Sprintf("%.1f%%", result.Linux.MemUsedPercent), result.Linux.MemUsedPercent < 85)
|
||||||
|
printCheck("CPU Cores", fmt.Sprintf("%d", result.Linux.CPUCores), true)
|
||||||
|
printCheck("Recommended Parallel", fmt.Sprintf("%d (auto-calculated)", result.Linux.RecommendedParallel), true)
|
||||||
|
|
||||||
// Linux-specific kernel checks
|
// Linux-specific kernel checks
|
||||||
if result.Linux.IsLinux && result.Linux.ShmMax > 0 {
|
if result.Linux.IsLinux && result.Linux.ShmMax > 0 {
|
||||||
|
|||||||
@@ -334,10 +334,12 @@ func (s *Safety) checkPostgresDatabaseExists(ctx context.Context, dbName string)
|
|||||||
"-tAc", fmt.Sprintf("SELECT 1 FROM pg_database WHERE datname='%s'", dbName),
|
"-tAc", fmt.Sprintf("SELECT 1 FROM pg_database WHERE datname='%s'", dbName),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only add -h flag if host is not localhost (to use Unix socket for peer auth)
|
// Always add -h flag for explicit host connection (required for password auth)
|
||||||
if s.cfg.Host != "localhost" && s.cfg.Host != "127.0.0.1" && s.cfg.Host != "" {
|
host := s.cfg.Host
|
||||||
args = append([]string{"-h", s.cfg.Host}, args...)
|
if host == "" {
|
||||||
|
host = "localhost"
|
||||||
}
|
}
|
||||||
|
args = append([]string{"-h", host}, args...)
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "psql", args...)
|
cmd := exec.CommandContext(ctx, "psql", args...)
|
||||||
|
|
||||||
@@ -346,9 +348,9 @@ func (s *Safety) checkPostgresDatabaseExists(ctx context.Context, dbName string)
|
|||||||
cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", s.cfg.Password))
|
cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", s.cfg.Password))
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := cmd.Output()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("failed to check database existence: %w", err)
|
return false, fmt.Errorf("failed to check database existence: %w (output: %s)", err, strings.TrimSpace(string(output)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.TrimSpace(string(output)) == "1", nil
|
return strings.TrimSpace(string(output)) == "1", nil
|
||||||
@@ -405,10 +407,13 @@ func (s *Safety) listPostgresUserDatabases(ctx context.Context) ([]string, error
|
|||||||
"-c", query,
|
"-c", query,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only add -h flag if host is not localhost (to use Unix socket for peer auth)
|
// Always add -h flag for explicit host connection (required for password auth)
|
||||||
if s.cfg.Host != "localhost" && s.cfg.Host != "127.0.0.1" && s.cfg.Host != "" {
|
// Empty or unset host defaults to localhost
|
||||||
args = append([]string{"-h", s.cfg.Host}, args...)
|
host := s.cfg.Host
|
||||||
|
if host == "" {
|
||||||
|
host = "localhost"
|
||||||
}
|
}
|
||||||
|
args = append([]string{"-h", host}, args...)
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "psql", args...)
|
cmd := exec.CommandContext(ctx, "psql", args...)
|
||||||
|
|
||||||
@@ -417,9 +422,10 @@ func (s *Safety) listPostgresUserDatabases(ctx context.Context) ([]string, error
|
|||||||
cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", s.cfg.Password))
|
cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", s.cfg.Password))
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := cmd.Output()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to list databases: %w", err)
|
// Include psql output in error for debugging
|
||||||
|
return nil, fmt.Errorf("failed to list databases: %w (output: %s)", err, strings.TrimSpace(string(output)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse output
|
// Parse output
|
||||||
|
|||||||
@@ -454,65 +454,61 @@ func (m BackupExecutionModel) View() string {
|
|||||||
} else {
|
} else {
|
||||||
// Show completion summary with detailed stats
|
// Show completion summary with detailed stats
|
||||||
if m.err != nil {
|
if m.err != nil {
|
||||||
|
s.WriteString(errorStyle.Render("╔══════════════════════════════════════════════════════════════╗"))
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
s.WriteString(errorStyle.Render(" ╔══════════════════════════════════════════════════════════╗"))
|
s.WriteString(errorStyle.Render("║ [FAIL] BACKUP FAILED ║"))
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
s.WriteString(errorStyle.Render(" ║ [FAIL] BACKUP FAILED ║"))
|
s.WriteString(errorStyle.Render("╚══════════════════════════════════════════════════════════════╝"))
|
||||||
s.WriteString("\n")
|
|
||||||
s.WriteString(errorStyle.Render(" ╚══════════════════════════════════════════════════════════╝"))
|
|
||||||
s.WriteString("\n\n")
|
s.WriteString("\n\n")
|
||||||
s.WriteString(errorStyle.Render(fmt.Sprintf(" Error: %v", m.err)))
|
s.WriteString(errorStyle.Render(fmt.Sprintf(" Error: %v", m.err)))
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
} else {
|
} else {
|
||||||
|
s.WriteString(successStyle.Render("╔══════════════════════════════════════════════════════════════╗"))
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
s.WriteString(successStyle.Render(" ╔══════════════════════════════════════════════════════════╗"))
|
s.WriteString(successStyle.Render("║ [OK] BACKUP COMPLETED SUCCESSFULLY ║"))
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
s.WriteString(successStyle.Render(" ║ [OK] BACKUP COMPLETED SUCCESSFULLY ║"))
|
s.WriteString(successStyle.Render("╚══════════════════════════════════════════════════════════════╝"))
|
||||||
s.WriteString("\n")
|
|
||||||
s.WriteString(successStyle.Render(" ╚══════════════════════════════════════════════════════════╝"))
|
|
||||||
s.WriteString("\n\n")
|
s.WriteString("\n\n")
|
||||||
|
|
||||||
// Summary section
|
// Summary section
|
||||||
s.WriteString(infoStyle.Render(" ─── Summary ─────────────────────────────────────────────"))
|
s.WriteString(infoStyle.Render(" ─── Summary ───────────────────────────────────────────────"))
|
||||||
s.WriteString("\n\n")
|
s.WriteString("\n\n")
|
||||||
|
|
||||||
// Backup type specific info
|
// Backup type specific info
|
||||||
switch m.backupType {
|
switch m.backupType {
|
||||||
case "cluster":
|
case "cluster":
|
||||||
s.WriteString(" Type: Cluster Backup\n")
|
s.WriteString(" Type: Cluster Backup\n")
|
||||||
if m.dbTotal > 0 {
|
if m.dbTotal > 0 {
|
||||||
s.WriteString(fmt.Sprintf(" Databases: %d backed up\n", m.dbTotal))
|
s.WriteString(fmt.Sprintf(" Databases: %d backed up\n", m.dbTotal))
|
||||||
}
|
}
|
||||||
case "single":
|
case "single":
|
||||||
s.WriteString(" Type: Single Database Backup\n")
|
s.WriteString(" Type: Single Database Backup\n")
|
||||||
s.WriteString(fmt.Sprintf(" Database: %s\n", m.databaseName))
|
s.WriteString(fmt.Sprintf(" Database: %s\n", m.databaseName))
|
||||||
case "sample":
|
case "sample":
|
||||||
s.WriteString(" Type: Sample Backup\n")
|
s.WriteString(" Type: Sample Backup\n")
|
||||||
s.WriteString(fmt.Sprintf(" Database: %s\n", m.databaseName))
|
s.WriteString(fmt.Sprintf(" Database: %s\n", m.databaseName))
|
||||||
s.WriteString(fmt.Sprintf(" Sample Ratio: %d\n", m.ratio))
|
s.WriteString(fmt.Sprintf(" Sample Ratio: %d\n", m.ratio))
|
||||||
}
|
}
|
||||||
|
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
|
}
|
||||||
// Timing section
|
|
||||||
s.WriteString(infoStyle.Render(" ─── Timing ──────────────────────────────────────────────"))
|
// Timing section (always shown, consistent with restore)
|
||||||
s.WriteString("\n\n")
|
s.WriteString(infoStyle.Render(" ─── Timing ────────────────────────────────────────────────"))
|
||||||
|
s.WriteString("\n\n")
|
||||||
elapsed := time.Since(m.startTime)
|
|
||||||
s.WriteString(fmt.Sprintf(" Total Time: %s\n", formatBackupDuration(elapsed)))
|
elapsed := time.Since(m.startTime)
|
||||||
|
s.WriteString(fmt.Sprintf(" Total Time: %s\n", formatBackupDuration(elapsed)))
|
||||||
if m.backupType == "cluster" && m.dbTotal > 0 {
|
|
||||||
avgPerDB := elapsed / time.Duration(m.dbTotal)
|
if m.backupType == "cluster" && m.dbTotal > 0 && m.err == nil {
|
||||||
s.WriteString(fmt.Sprintf(" Avg per DB: %s\n", formatBackupDuration(avgPerDB)))
|
avgPerDB := elapsed / time.Duration(m.dbTotal)
|
||||||
}
|
s.WriteString(fmt.Sprintf(" Avg per DB: %s\n", formatBackupDuration(avgPerDB)))
|
||||||
|
|
||||||
s.WriteString("\n")
|
|
||||||
s.WriteString(infoStyle.Render(" ─────────────────────────────────────────────────────────"))
|
|
||||||
s.WriteString("\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
s.WriteString(" [KEY] Press Enter or ESC to return to menu\n")
|
s.WriteString(infoStyle.Render(" ───────────────────────────────────────────────────────────"))
|
||||||
|
s.WriteString("\n\n")
|
||||||
|
s.WriteString(infoStyle.Render(" [KEYS] Press Enter to continue"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.String()
|
return s.String()
|
||||||
|
|||||||
@@ -299,9 +299,13 @@ func (m *MenuModel) View() string {
|
|||||||
|
|
||||||
var s string
|
var s string
|
||||||
|
|
||||||
|
// Product branding header
|
||||||
|
brandLine := fmt.Sprintf("dbbackup v%s • Enterprise Database Backup & Recovery", m.config.Version)
|
||||||
|
s += "\n" + infoStyle.Render(brandLine) + "\n"
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
header := titleStyle.Render("Database Backup Tool - Interactive Menu")
|
header := titleStyle.Render("Interactive Menu")
|
||||||
s += fmt.Sprintf("\n%s\n\n", header)
|
s += fmt.Sprintf("%s\n\n", header)
|
||||||
|
|
||||||
if len(m.dbTypes) > 0 {
|
if len(m.dbTypes) > 0 {
|
||||||
options := make([]string, len(m.dbTypes))
|
options := make([]string, len(m.dbTypes))
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ type RestorePreviewModel struct {
|
|||||||
cleanClusterFirst bool // For cluster restore: drop all user databases first
|
cleanClusterFirst bool // For cluster restore: drop all user databases first
|
||||||
existingDBCount int // Number of existing user databases
|
existingDBCount int // Number of existing user databases
|
||||||
existingDBs []string // List of existing user databases
|
existingDBs []string // List of existing user databases
|
||||||
|
existingDBError string // Error message if database listing failed
|
||||||
safetyChecks []SafetyCheck
|
safetyChecks []SafetyCheck
|
||||||
checking bool
|
checking bool
|
||||||
canProceed bool
|
canProceed bool
|
||||||
@@ -102,6 +103,7 @@ type safetyCheckCompleteMsg struct {
|
|||||||
canProceed bool
|
canProceed bool
|
||||||
existingDBCount int
|
existingDBCount int
|
||||||
existingDBs []string
|
existingDBs []string
|
||||||
|
existingDBError string
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSafetyChecks(cfg *config.Config, log logger.Logger, archive ArchiveInfo, targetDB string) tea.Cmd {
|
func runSafetyChecks(cfg *config.Config, log logger.Logger, archive ArchiveInfo, targetDB string) tea.Cmd {
|
||||||
@@ -221,10 +223,12 @@ func runSafetyChecks(cfg *config.Config, log logger.Logger, archive ArchiveInfo,
|
|||||||
check = SafetyCheck{Name: "Existing databases", Status: "checking", Critical: false}
|
check = SafetyCheck{Name: "Existing databases", Status: "checking", Critical: false}
|
||||||
|
|
||||||
// Get list of existing user databases (exclude templates and system DBs)
|
// Get list of existing user databases (exclude templates and system DBs)
|
||||||
|
var existingDBError string
|
||||||
dbList, err := safety.ListUserDatabases(ctx)
|
dbList, err := safety.ListUserDatabases(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
check.Status = "warning"
|
check.Status = "warning"
|
||||||
check.Message = fmt.Sprintf("Cannot list databases: %v", err)
|
check.Message = fmt.Sprintf("Cannot list databases: %v", err)
|
||||||
|
existingDBError = err.Error()
|
||||||
} else {
|
} else {
|
||||||
existingDBCount = len(dbList)
|
existingDBCount = len(dbList)
|
||||||
existingDBs = dbList
|
existingDBs = dbList
|
||||||
@@ -238,6 +242,14 @@ func runSafetyChecks(cfg *config.Config, log logger.Logger, archive ArchiveInfo,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
checks = append(checks, check)
|
checks = append(checks, check)
|
||||||
|
|
||||||
|
return safetyCheckCompleteMsg{
|
||||||
|
checks: checks,
|
||||||
|
canProceed: canProceed,
|
||||||
|
existingDBCount: existingDBCount,
|
||||||
|
existingDBs: existingDBs,
|
||||||
|
existingDBError: existingDBError,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return safetyCheckCompleteMsg{
|
return safetyCheckCompleteMsg{
|
||||||
@@ -257,6 +269,7 @@ func (m RestorePreviewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.canProceed = msg.canProceed
|
m.canProceed = msg.canProceed
|
||||||
m.existingDBCount = msg.existingDBCount
|
m.existingDBCount = msg.existingDBCount
|
||||||
m.existingDBs = msg.existingDBs
|
m.existingDBs = msg.existingDBs
|
||||||
|
m.existingDBError = msg.existingDBError
|
||||||
// Auto-forward in auto-confirm mode
|
// Auto-forward in auto-confirm mode
|
||||||
if m.config.TUIAutoConfirm {
|
if m.config.TUIAutoConfirm {
|
||||||
return m.parent, tea.Quit
|
return m.parent, tea.Quit
|
||||||
@@ -275,12 +288,17 @@ func (m RestorePreviewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
case "c":
|
case "c":
|
||||||
if m.mode == "restore-cluster" {
|
if m.mode == "restore-cluster" {
|
||||||
// Toggle cluster cleanup
|
// Prevent toggle if we couldn't detect existing databases
|
||||||
m.cleanClusterFirst = !m.cleanClusterFirst
|
if m.existingDBError != "" {
|
||||||
if m.cleanClusterFirst {
|
m.message = checkWarningStyle.Render("[WARN] Cannot enable cleanup - database detection failed")
|
||||||
m.message = checkWarningStyle.Render(fmt.Sprintf("[WARN] Will drop %d existing database(s) before restore", m.existingDBCount))
|
|
||||||
} else {
|
} else {
|
||||||
m.message = fmt.Sprintf("Clean cluster first: disabled")
|
// Toggle cluster cleanup
|
||||||
|
m.cleanClusterFirst = !m.cleanClusterFirst
|
||||||
|
if m.cleanClusterFirst {
|
||||||
|
m.message = checkWarningStyle.Render(fmt.Sprintf("[WARN] Will drop %d existing database(s) before restore", m.existingDBCount))
|
||||||
|
} else {
|
||||||
|
m.message = fmt.Sprintf("Clean cluster first: disabled")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Toggle create if missing
|
// Toggle create if missing
|
||||||
@@ -382,7 +400,11 @@ func (m RestorePreviewModel) View() string {
|
|||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
s.WriteString(fmt.Sprintf(" Host: %s:%d\n", m.config.Host, m.config.Port))
|
s.WriteString(fmt.Sprintf(" Host: %s:%d\n", m.config.Host, m.config.Port))
|
||||||
|
|
||||||
if m.existingDBCount > 0 {
|
if m.existingDBError != "" {
|
||||||
|
// Show error when database listing failed
|
||||||
|
s.WriteString(checkWarningStyle.Render(fmt.Sprintf(" Existing Databases: Unable to detect (%s)\n", m.existingDBError)))
|
||||||
|
s.WriteString(infoStyle.Render(" (Cleanup option disabled - cannot verify database status)\n"))
|
||||||
|
} else if m.existingDBCount > 0 {
|
||||||
s.WriteString(fmt.Sprintf(" Existing Databases: %d found\n", m.existingDBCount))
|
s.WriteString(fmt.Sprintf(" Existing Databases: %d found\n", m.existingDBCount))
|
||||||
|
|
||||||
// Show first few database names
|
// Show first few database names
|
||||||
|
|||||||
Reference in New Issue
Block a user