TUI: Add database progress tracking to cluster backup
- Add ProgressCallback and DatabaseProgressCallback to backup engine - Add SetProgressCallback() and SetDatabaseProgressCallback() methods - Wire database progress callback in backup_exec.go - Show progress bar (Database: [████░░░] X/Y) during cluster backups - Hide spinner when progress bar is displayed - Use shared progress state pattern for thread-safe TUI updates
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
130
internal/tui/backup_exec.go
Executable file → Normal file
130
internal/tui/backup_exec.go
Executable file → Normal file
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user