diff --git a/bin/README.md b/bin/README.md index 6ab1eac..2a275fc 100644 --- a/bin/README.md +++ b/bin/README.md @@ -4,8 +4,8 @@ This directory contains pre-compiled binaries for the DB Backup Tool across mult ## Build Information - **Version**: 3.42.34 -- **Build Time**: 2026-01-14_16:19:00_UTC -- **Git Commit**: 7711a20 +- **Build Time**: 2026-01-15_14:16:33_UTC +- **Git Commit**: eeacbfa ## Recent Updates (v1.1.0) - ✅ Fixed TUI progress display with line-by-line output diff --git a/internal/backup/engine.go b/internal/backup/engine.go index 3130e48..5cd41d9 100755 --- a/internal/backup/engine.go +++ b/internal/backup/engine.go @@ -28,14 +28,22 @@ import ( "dbbackup/internal/swap" ) +// ProgressCallback is called with byte-level progress updates during backup operations +type ProgressCallback func(current, total int64, description string) + +// DatabaseProgressCallback is called with database count progress during cluster backup +type DatabaseProgressCallback func(done, total int, dbName string) + // Engine handles backup operations type Engine struct { - cfg *config.Config - log logger.Logger - db database.Database - progress progress.Indicator - detailedReporter *progress.DetailedReporter - silent bool // Silent mode for TUI + cfg *config.Config + log logger.Logger + db database.Database + progress progress.Indicator + detailedReporter *progress.DetailedReporter + silent bool // Silent mode for TUI + progressCallback ProgressCallback + dbProgressCallback DatabaseProgressCallback } // New creates a new backup engine @@ -86,6 +94,30 @@ func NewSilent(cfg *config.Config, log logger.Logger, db database.Database, prog } } +// SetProgressCallback sets a callback for detailed progress reporting (for TUI mode) +func (e *Engine) SetProgressCallback(cb ProgressCallback) { + e.progressCallback = cb +} + +// SetDatabaseProgressCallback sets a callback for database count progress during cluster backup +func (e *Engine) SetDatabaseProgressCallback(cb DatabaseProgressCallback) { + e.dbProgressCallback = cb +} + +// reportProgress reports progress to the callback if set +func (e *Engine) reportProgress(current, total int64, description string) { + if e.progressCallback != nil { + e.progressCallback(current, total, description) + } +} + +// reportDatabaseProgress reports database count progress to the callback if set +func (e *Engine) reportDatabaseProgress(done, total int, dbName string) { + if e.dbProgressCallback != nil { + e.dbProgressCallback(done, total, dbName) + } +} + // loggerAdapter adapts our logger to the progress.Logger interface type loggerAdapter struct { logger logger.Logger @@ -465,6 +497,8 @@ func (e *Engine) BackupCluster(ctx context.Context) error { estimator.UpdateProgress(idx) e.printf(" [%d/%d] Backing up database: %s\n", idx+1, len(databases), name) quietProgress.Update(fmt.Sprintf("Backing up database %d/%d: %s", idx+1, len(databases), name)) + // Report database progress to TUI callback + e.reportDatabaseProgress(idx+1, len(databases), name) mu.Unlock() // Check database size and warn if very large diff --git a/internal/tui/backup_exec.go b/internal/tui/backup_exec.go old mode 100755 new mode 100644 index 6bf82fb..baab868 --- a/internal/tui/backup_exec.go +++ b/internal/tui/backup_exec.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "sync" "time" tea "github.com/charmbracelet/bubbletea" @@ -33,6 +34,56 @@ type BackupExecutionModel struct { startTime time.Time details []string spinnerFrame int + + // Database count progress (for cluster backup) + dbTotal int + dbDone int + dbName string // Current database being backed up +} + +// sharedBackupProgressState holds progress state that can be safely accessed from callbacks +type sharedBackupProgressState struct { + mu sync.Mutex + dbTotal int + dbDone int + dbName string + hasUpdate bool +} + +// Package-level shared progress state for backup operations +var ( + currentBackupProgressMu sync.Mutex + currentBackupProgressState *sharedBackupProgressState +) + +func setCurrentBackupProgress(state *sharedBackupProgressState) { + currentBackupProgressMu.Lock() + defer currentBackupProgressMu.Unlock() + currentBackupProgressState = state +} + +func clearCurrentBackupProgress() { + currentBackupProgressMu.Lock() + defer currentBackupProgressMu.Unlock() + currentBackupProgressState = nil +} + +func getCurrentBackupProgress() (dbTotal, dbDone int, dbName string, hasUpdate bool) { + currentBackupProgressMu.Lock() + defer currentBackupProgressMu.Unlock() + + if currentBackupProgressState == nil { + return 0, 0, "", false + } + + currentBackupProgressState.mu.Lock() + defer currentBackupProgressState.mu.Unlock() + + hasUpdate = currentBackupProgressState.hasUpdate + currentBackupProgressState.hasUpdate = false + + return currentBackupProgressState.dbTotal, currentBackupProgressState.dbDone, + currentBackupProgressState.dbName, hasUpdate } func NewBackupExecution(cfg *config.Config, log logger.Logger, parent tea.Model, ctx context.Context, backupType, dbName string, ratio int) BackupExecutionModel { @@ -55,7 +106,6 @@ func NewBackupExecution(cfg *config.Config, log logger.Logger, parent tea.Model, } func (m BackupExecutionModel) Init() tea.Cmd { - // TUI handles all display through View() - no progress callbacks needed return tea.Batch( executeBackupWithTUIProgress(m.ctx, m.config, m.logger, m.backupType, m.databaseName, m.ratio), backupTickCmd(), @@ -91,6 +141,11 @@ func executeBackupWithTUIProgress(parentCtx context.Context, cfg *config.Config, start := time.Now() + // Setup shared progress state for TUI polling + progressState := &sharedBackupProgressState{} + setCurrentBackupProgress(progressState) + defer clearCurrentBackupProgress() + dbClient, err := database.New(cfg, log) if err != nil { return backupCompleteMsg{ @@ -110,6 +165,16 @@ func executeBackupWithTUIProgress(parentCtx context.Context, cfg *config.Config, // Pass nil as indicator - TUI itself handles all display, no stdout printing engine := backup.NewSilent(cfg, log, dbClient, nil) + // Set database progress callback for cluster backups + engine.SetDatabaseProgressCallback(func(done, total int, currentDB string) { + progressState.mu.Lock() + progressState.dbDone = done + progressState.dbTotal = total + progressState.dbName = currentDB + progressState.hasUpdate = true + progressState.mu.Unlock() + }) + var backupErr error switch backupType { case "single": @@ -157,10 +222,21 @@ func (m BackupExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Increment spinner frame for smooth animation m.spinnerFrame = (m.spinnerFrame + 1) % len(spinnerFrames) - // Update status based on elapsed time to show progress + // Poll for database progress updates from callbacks + dbTotal, dbDone, dbName, hasUpdate := getCurrentBackupProgress() + if hasUpdate { + m.dbTotal = dbTotal + m.dbDone = dbDone + m.dbName = dbName + } + + // Update status based on progress and elapsed time elapsedSec := int(time.Since(m.startTime).Seconds()) - if elapsedSec < 2 { + if m.dbTotal > 0 && m.dbDone > 0 { + // We have real progress from cluster backup + m.status = fmt.Sprintf("Backing up database: %s", m.dbName) + } else if elapsedSec < 2 { m.status = "Initializing backup..." } else if elapsedSec < 5 { if m.backupType == "cluster" { @@ -234,6 +310,34 @@ func (m BackupExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } +// renderDatabaseProgressBar renders a progress bar for database count progress +func renderBackupDatabaseProgressBar(done, total int, dbName string, width int) string { + if total == 0 { + return "" + } + + // Calculate progress percentage + percent := float64(done) / float64(total) + if percent > 1.0 { + percent = 1.0 + } + + // Calculate filled width + barWidth := width - 20 // Leave room for label and percentage + if barWidth < 10 { + barWidth = 10 + } + filled := int(float64(barWidth) * percent) + if filled > barWidth { + filled = barWidth + } + + // Build progress bar + bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled) + + return fmt.Sprintf(" Database: [%s] %d/%d", bar, done, total) +} + func (m BackupExecutionModel) View() string { var s strings.Builder s.Grow(512) // Pre-allocate estimated capacity for better performance @@ -255,12 +359,24 @@ func (m BackupExecutionModel) View() string { s.WriteString(fmt.Sprintf(" %-10s %s\n", "Duration:", time.Since(m.startTime).Round(time.Second))) s.WriteString("\n") - // Status with spinner + // Status display if !m.done { - if m.cancelling { - s.WriteString(fmt.Sprintf(" %s %s\n", spinnerFrames[m.spinnerFrame], m.status)) + // Show database progress bar if we have progress data (cluster backup) + if m.dbTotal > 0 && m.dbDone > 0 { + // Show progress bar instead of spinner when we have real progress + progressBar := renderBackupDatabaseProgressBar(m.dbDone, m.dbTotal, m.dbName, 50) + s.WriteString(progressBar + "\n") + s.WriteString(fmt.Sprintf(" %s\n", m.status)) } else { - s.WriteString(fmt.Sprintf(" %s %s\n", spinnerFrames[m.spinnerFrame], m.status)) + // Show spinner during initial phases + if m.cancelling { + s.WriteString(fmt.Sprintf(" %s %s\n", spinnerFrames[m.spinnerFrame], m.status)) + } else { + s.WriteString(fmt.Sprintf(" %s %s\n", spinnerFrames[m.spinnerFrame], m.status)) + } + } + + if !m.cancelling { s.WriteString("\n [KEY] Press Ctrl+C or ESC to cancel\n") } } else {