- Created internal/progress/estimator.go with ETAEstimator component - Tracks elapsed time and estimates remaining time based on progress - Enhanced Spinner and LineByLine indicators to display ETA info - Integrated into BackupCluster and RestoreCluster functions - Display format: 'Operation | X/Y (Z%) | Elapsed: Xm | ETA: ~Ym remaining' - Preserves spinner animation while showing progress/time estimates - Quick Win approach: no historical data storage, just current operation tracking
321 lines
8.7 KiB
Go
321 lines
8.7 KiB
Go
package tui
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"dbbackup/internal/backup"
|
|
"dbbackup/internal/config"
|
|
"dbbackup/internal/database"
|
|
"dbbackup/internal/logger"
|
|
"dbbackup/internal/progress"
|
|
)
|
|
|
|
// TUIProgressReporter is a progress reporter that integrates with the TUI
|
|
type TUIProgressReporter struct {
|
|
mu sync.RWMutex
|
|
operations map[string]*progress.OperationStatus
|
|
callbacks []func([]progress.OperationStatus)
|
|
defaultOperationID string
|
|
}
|
|
|
|
// NewTUIProgressReporter creates a new TUI-compatible progress reporter
|
|
func NewTUIProgressReporter() *TUIProgressReporter {
|
|
return &TUIProgressReporter{
|
|
operations: make(map[string]*progress.OperationStatus),
|
|
callbacks: make([]func([]progress.OperationStatus), 0),
|
|
}
|
|
}
|
|
|
|
// AddCallback adds a callback function to be called when operations update
|
|
func (t *TUIProgressReporter) AddCallback(callback func([]progress.OperationStatus)) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
t.callbacks = append(t.callbacks, callback)
|
|
}
|
|
|
|
// notifyCallbacks calls all registered callbacks with current operations
|
|
func (t *TUIProgressReporter) notifyCallbacks() {
|
|
operations := make([]progress.OperationStatus, 0, len(t.operations))
|
|
for _, op := range t.operations {
|
|
operations = append(operations, *op)
|
|
}
|
|
|
|
for _, callback := range t.callbacks {
|
|
go callback(operations)
|
|
}
|
|
}
|
|
|
|
func (t *TUIProgressReporter) ensureDefaultOperationLocked(message string) *progress.OperationStatus {
|
|
if t.defaultOperationID == "" {
|
|
t.defaultOperationID = fmt.Sprintf("tui-progress-%d", time.Now().UnixNano())
|
|
}
|
|
|
|
op, exists := t.operations[t.defaultOperationID]
|
|
if !exists {
|
|
op = &progress.OperationStatus{
|
|
ID: t.defaultOperationID,
|
|
Name: "Backup Progress",
|
|
Type: "indicator",
|
|
Status: "running",
|
|
StartTime: time.Now(),
|
|
Message: message,
|
|
Progress: 0,
|
|
Details: make(map[string]string),
|
|
Steps: make([]progress.StepStatus, 0),
|
|
}
|
|
t.operations[t.defaultOperationID] = op
|
|
}
|
|
|
|
if message != "" {
|
|
op.Message = message
|
|
}
|
|
return op
|
|
}
|
|
|
|
func (t *TUIProgressReporter) Start(message string) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
op := t.ensureDefaultOperationLocked(message)
|
|
now := time.Now()
|
|
op.Status = "running"
|
|
op.StartTime = now
|
|
op.EndTime = nil
|
|
op.Progress = 0
|
|
op.Message = message
|
|
t.notifyCallbacks()
|
|
}
|
|
|
|
func (t *TUIProgressReporter) Update(message string) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
op := t.ensureDefaultOperationLocked(message)
|
|
if op.Progress < 95 {
|
|
op.Progress += 5
|
|
}
|
|
op.Message = message
|
|
t.notifyCallbacks()
|
|
}
|
|
|
|
func (t *TUIProgressReporter) Complete(message string) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
if t.defaultOperationID == "" {
|
|
return
|
|
}
|
|
if op, exists := t.operations[t.defaultOperationID]; exists {
|
|
now := time.Now()
|
|
op.Status = "completed"
|
|
op.Message = message
|
|
op.Progress = 100
|
|
op.EndTime = &now
|
|
op.Duration = now.Sub(op.StartTime)
|
|
t.notifyCallbacks()
|
|
}
|
|
}
|
|
|
|
func (t *TUIProgressReporter) Fail(message string) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
if t.defaultOperationID == "" {
|
|
return
|
|
}
|
|
if op, exists := t.operations[t.defaultOperationID]; exists {
|
|
now := time.Now()
|
|
op.Status = "failed"
|
|
op.Message = message
|
|
op.EndTime = &now
|
|
op.Duration = now.Sub(op.StartTime)
|
|
t.notifyCallbacks()
|
|
}
|
|
}
|
|
|
|
func (t *TUIProgressReporter) Stop() {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
if t.defaultOperationID == "" {
|
|
return
|
|
}
|
|
if op, exists := t.operations[t.defaultOperationID]; exists {
|
|
if op.Status == "running" {
|
|
now := time.Now()
|
|
op.Status = "stopped"
|
|
op.EndTime = &now
|
|
op.Duration = now.Sub(op.StartTime)
|
|
}
|
|
t.notifyCallbacks()
|
|
}
|
|
}
|
|
|
|
// StartOperation starts tracking a new operation
|
|
func (t *TUIProgressReporter) StartOperation(id, name, opType string) *TUIOperationTracker {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
operation := &progress.OperationStatus{
|
|
ID: id,
|
|
Name: name,
|
|
Type: opType,
|
|
Status: "running",
|
|
StartTime: time.Now(),
|
|
Progress: 0,
|
|
Message: fmt.Sprintf("Starting %s: %s", opType, name),
|
|
Details: make(map[string]string),
|
|
Steps: make([]progress.StepStatus, 0),
|
|
}
|
|
|
|
t.operations[id] = operation
|
|
t.notifyCallbacks()
|
|
|
|
return &TUIOperationTracker{
|
|
reporter: t,
|
|
operationID: id,
|
|
}
|
|
}
|
|
|
|
// TUIOperationTracker tracks progress for TUI display
|
|
type TUIOperationTracker struct {
|
|
reporter *TUIProgressReporter
|
|
operationID string
|
|
}
|
|
|
|
// UpdateProgress updates the operation progress
|
|
func (t *TUIOperationTracker) UpdateProgress(progress int, message string) {
|
|
t.reporter.mu.Lock()
|
|
defer t.reporter.mu.Unlock()
|
|
|
|
if op, exists := t.reporter.operations[t.operationID]; exists {
|
|
op.Progress = progress
|
|
op.Message = message
|
|
t.reporter.notifyCallbacks()
|
|
}
|
|
}
|
|
|
|
// Complete marks the operation as completed
|
|
func (t *TUIOperationTracker) Complete(message string) {
|
|
t.reporter.mu.Lock()
|
|
defer t.reporter.mu.Unlock()
|
|
|
|
if op, exists := t.reporter.operations[t.operationID]; exists {
|
|
now := time.Now()
|
|
op.Status = "completed"
|
|
op.Progress = 100
|
|
op.Message = message
|
|
op.EndTime = &now
|
|
op.Duration = now.Sub(op.StartTime)
|
|
t.reporter.notifyCallbacks()
|
|
}
|
|
}
|
|
|
|
// Fail marks the operation as failed
|
|
func (t *TUIOperationTracker) Fail(message string) {
|
|
t.reporter.mu.Lock()
|
|
defer t.reporter.mu.Unlock()
|
|
|
|
if op, exists := t.reporter.operations[t.operationID]; exists {
|
|
now := time.Now()
|
|
op.Status = "failed"
|
|
op.Message = message
|
|
op.EndTime = &now
|
|
op.Duration = now.Sub(op.StartTime)
|
|
t.reporter.notifyCallbacks()
|
|
}
|
|
}
|
|
|
|
// GetOperations returns all current operations
|
|
func (t *TUIProgressReporter) GetOperations() []progress.OperationStatus {
|
|
t.mu.RLock()
|
|
defer t.mu.RUnlock()
|
|
|
|
operations := make([]progress.OperationStatus, 0, len(t.operations))
|
|
for _, op := range t.operations {
|
|
operations = append(operations, *op)
|
|
}
|
|
return operations
|
|
}
|
|
|
|
// SilentLogger implements logger.Logger but doesn't output anything
|
|
type SilentLogger struct{}
|
|
|
|
func (s *SilentLogger) Info(msg string, args ...any) {}
|
|
func (s *SilentLogger) Warn(msg string, args ...any) {}
|
|
func (s *SilentLogger) Error(msg string, args ...any) {}
|
|
func (s *SilentLogger) Debug(msg string, args ...any) {}
|
|
func (s *SilentLogger) Time(msg string, args ...any) {}
|
|
func (s *SilentLogger) StartOperation(name string) logger.OperationLogger {
|
|
return &SilentOperation{}
|
|
}
|
|
|
|
// SilentOperation implements logger.OperationLogger but doesn't output anything
|
|
type SilentOperation struct{}
|
|
|
|
func (s *SilentOperation) Update(message string, args ...any) {}
|
|
func (s *SilentOperation) Complete(message string, args ...any) {}
|
|
func (s *SilentOperation) Fail(message string, args ...any) {}
|
|
|
|
// SilentProgressIndicator implements progress.Indicator but doesn't output anything
|
|
type SilentProgressIndicator struct{}
|
|
|
|
func (s *SilentProgressIndicator) Start(message string) {}
|
|
func (s *SilentProgressIndicator) Update(message string) {}
|
|
func (s *SilentProgressIndicator) Complete(message string) {}
|
|
func (s *SilentProgressIndicator) Fail(message string) {}
|
|
func (s *SilentProgressIndicator) Stop() {}
|
|
func (s *SilentProgressIndicator) SetEstimator(estimator *progress.ETAEstimator) {}
|
|
|
|
// RunBackupInTUI runs a backup operation with TUI-compatible progress reporting
|
|
func RunBackupInTUI(ctx context.Context, cfg *config.Config, log logger.Logger,
|
|
backupType string, databaseName string, reporter *TUIProgressReporter) error {
|
|
|
|
// Create database connection
|
|
db, err := database.New(cfg, &SilentLogger{}) // Use silent logger
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create database connection: %w", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
err = db.Connect(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to connect to database: %w", err)
|
|
}
|
|
|
|
// Create backup engine with silent progress indicator and logger
|
|
silentProgress := &SilentProgressIndicator{}
|
|
engine := backup.NewSilent(cfg, &SilentLogger{}, db, silentProgress)
|
|
|
|
// Start operation tracking
|
|
operationID := fmt.Sprintf("%s_%d", backupType, time.Now().Unix())
|
|
tracker := reporter.StartOperation(operationID, databaseName, backupType)
|
|
|
|
// Run the appropriate backup type
|
|
switch backupType {
|
|
case "single":
|
|
tracker.UpdateProgress(10, "Preparing single database backup...")
|
|
err = engine.BackupSingle(ctx, databaseName)
|
|
case "cluster":
|
|
tracker.UpdateProgress(10, "Preparing cluster backup...")
|
|
err = engine.BackupCluster(ctx)
|
|
case "sample":
|
|
tracker.UpdateProgress(10, "Preparing sample backup...")
|
|
err = engine.BackupSample(ctx, databaseName)
|
|
default:
|
|
err = fmt.Errorf("unknown backup type: %s", backupType)
|
|
}
|
|
|
|
// Update final status
|
|
if err != nil {
|
|
tracker.Fail(fmt.Sprintf("Backup failed: %v", err))
|
|
return err
|
|
} else {
|
|
tracker.Complete(fmt.Sprintf("%s backup completed successfully", backupType))
|
|
return nil
|
|
}
|
|
}
|