Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 09a917766f |
@@ -4,8 +4,8 @@ This directory contains pre-compiled binaries for the DB Backup Tool across mult
|
|||||||
|
|
||||||
## Build Information
|
## Build Information
|
||||||
- **Version**: 3.42.34
|
- **Version**: 3.42.34
|
||||||
- **Build Time**: 2026-01-14_16:19:00_UTC
|
- **Build Time**: 2026-01-15_14:16:33_UTC
|
||||||
- **Git Commit**: 7711a20
|
- **Git Commit**: eeacbfa
|
||||||
|
|
||||||
## 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
|
||||||
|
|||||||
@@ -28,14 +28,22 @@ import (
|
|||||||
"dbbackup/internal/swap"
|
"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
|
// Engine handles backup operations
|
||||||
type Engine struct {
|
type Engine struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
log logger.Logger
|
log logger.Logger
|
||||||
db database.Database
|
db database.Database
|
||||||
progress progress.Indicator
|
progress progress.Indicator
|
||||||
detailedReporter *progress.DetailedReporter
|
detailedReporter *progress.DetailedReporter
|
||||||
silent bool // Silent mode for TUI
|
silent bool // Silent mode for TUI
|
||||||
|
progressCallback ProgressCallback
|
||||||
|
dbProgressCallback DatabaseProgressCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new backup engine
|
// 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
|
// loggerAdapter adapts our logger to the progress.Logger interface
|
||||||
type loggerAdapter struct {
|
type loggerAdapter struct {
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
@@ -465,6 +497,8 @@ func (e *Engine) BackupCluster(ctx context.Context) error {
|
|||||||
estimator.UpdateProgress(idx)
|
estimator.UpdateProgress(idx)
|
||||||
e.printf(" [%d/%d] Backing up database: %s\n", idx+1, len(databases), name)
|
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))
|
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()
|
mu.Unlock()
|
||||||
|
|
||||||
// Check database size and warn if very large
|
// 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"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
@@ -33,6 +34,56 @@ type BackupExecutionModel struct {
|
|||||||
startTime time.Time
|
startTime time.Time
|
||||||
details []string
|
details []string
|
||||||
spinnerFrame int
|
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 {
|
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 {
|
func (m BackupExecutionModel) Init() tea.Cmd {
|
||||||
// TUI handles all display through View() - no progress callbacks needed
|
|
||||||
return tea.Batch(
|
return tea.Batch(
|
||||||
executeBackupWithTUIProgress(m.ctx, m.config, m.logger, m.backupType, m.databaseName, m.ratio),
|
executeBackupWithTUIProgress(m.ctx, m.config, m.logger, m.backupType, m.databaseName, m.ratio),
|
||||||
backupTickCmd(),
|
backupTickCmd(),
|
||||||
@@ -91,6 +141,11 @@ func executeBackupWithTUIProgress(parentCtx context.Context, cfg *config.Config,
|
|||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
// Setup shared progress state for TUI polling
|
||||||
|
progressState := &sharedBackupProgressState{}
|
||||||
|
setCurrentBackupProgress(progressState)
|
||||||
|
defer clearCurrentBackupProgress()
|
||||||
|
|
||||||
dbClient, err := database.New(cfg, log)
|
dbClient, err := database.New(cfg, log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return backupCompleteMsg{
|
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
|
// Pass nil as indicator - TUI itself handles all display, no stdout printing
|
||||||
engine := backup.NewSilent(cfg, log, dbClient, nil)
|
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
|
var backupErr error
|
||||||
switch backupType {
|
switch backupType {
|
||||||
case "single":
|
case "single":
|
||||||
@@ -157,10 +222,21 @@ func (m BackupExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
// Increment spinner frame for smooth animation
|
// Increment spinner frame for smooth animation
|
||||||
m.spinnerFrame = (m.spinnerFrame + 1) % len(spinnerFrames)
|
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())
|
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..."
|
m.status = "Initializing backup..."
|
||||||
} else if elapsedSec < 5 {
|
} else if elapsedSec < 5 {
|
||||||
if m.backupType == "cluster" {
|
if m.backupType == "cluster" {
|
||||||
@@ -234,6 +310,34 @@ func (m BackupExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
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 {
|
func (m BackupExecutionModel) View() string {
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
s.Grow(512) // Pre-allocate estimated capacity for better performance
|
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(fmt.Sprintf(" %-10s %s\n", "Duration:", time.Since(m.startTime).Round(time.Second)))
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
|
|
||||||
// Status with spinner
|
// Status display
|
||||||
if !m.done {
|
if !m.done {
|
||||||
if m.cancelling {
|
// Show database progress bar if we have progress data (cluster backup)
|
||||||
s.WriteString(fmt.Sprintf(" %s %s\n", spinnerFrames[m.spinnerFrame], m.status))
|
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 {
|
} 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")
|
s.WriteString("\n [KEY] Press Ctrl+C or ESC to cancel\n")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user