211 lines
5.2 KiB
Go
211 lines
5.2 KiB
Go
package tui
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
tea "github.com/charmbracelet/bubbletea"
|
||
|
||
"dbbackup/internal/backup"
|
||
"dbbackup/internal/config"
|
||
"dbbackup/internal/database"
|
||
"dbbackup/internal/logger"
|
||
"dbbackup/internal/progress"
|
||
)
|
||
|
||
// BackupExecutionModel handles backup execution with progress
|
||
type BackupExecutionModel struct {
|
||
config *config.Config
|
||
logger logger.Logger
|
||
parent tea.Model
|
||
backupType string
|
||
databaseName string
|
||
ratio int
|
||
status string
|
||
progress int
|
||
done bool
|
||
err error
|
||
result string
|
||
startTime time.Time
|
||
details []string
|
||
}
|
||
|
||
func NewBackupExecution(cfg *config.Config, log logger.Logger, parent tea.Model, backupType, dbName string, ratio int) BackupExecutionModel {
|
||
return BackupExecutionModel{
|
||
config: cfg,
|
||
logger: log,
|
||
parent: parent,
|
||
backupType: backupType,
|
||
databaseName: dbName,
|
||
ratio: ratio,
|
||
status: "Initializing...",
|
||
startTime: time.Now(),
|
||
details: []string{},
|
||
}
|
||
}
|
||
|
||
func (m BackupExecutionModel) Init() tea.Cmd {
|
||
reporter := NewTUIProgressReporter()
|
||
reporter.AddCallback(func(ops []progress.OperationStatus) {
|
||
if len(ops) == 0 {
|
||
return
|
||
}
|
||
|
||
latest := ops[len(ops)-1]
|
||
tea.Println(backupProgressMsg{
|
||
status: latest.Message,
|
||
progress: latest.Progress,
|
||
detail: latest.Status,
|
||
})
|
||
})
|
||
|
||
return executeBackupWithTUIProgress(m.config, m.logger, m.backupType, m.databaseName, m.ratio, reporter)
|
||
}
|
||
|
||
type backupProgressMsg struct {
|
||
status string
|
||
progress int
|
||
detail string
|
||
}
|
||
|
||
type backupCompleteMsg struct {
|
||
result string
|
||
err error
|
||
}
|
||
|
||
func executeBackupWithTUIProgress(cfg *config.Config, log logger.Logger, backupType, dbName string, ratio int, reporter *TUIProgressReporter) tea.Cmd {
|
||
return func() tea.Msg {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||
defer cancel()
|
||
|
||
start := time.Now()
|
||
|
||
dbClient, err := database.New(cfg, log)
|
||
if err != nil {
|
||
return backupCompleteMsg{
|
||
result: "",
|
||
err: fmt.Errorf("failed to create database client: %w", err),
|
||
}
|
||
}
|
||
defer dbClient.Close()
|
||
|
||
if err := dbClient.Connect(ctx); err != nil {
|
||
return backupCompleteMsg{
|
||
result: "",
|
||
err: fmt.Errorf("database connection failed: %w", err),
|
||
}
|
||
}
|
||
|
||
engine := backup.NewSilent(cfg, log, dbClient, reporter)
|
||
|
||
var backupErr error
|
||
switch backupType {
|
||
case "single":
|
||
backupErr = engine.BackupSingle(ctx, dbName)
|
||
case "sample":
|
||
cfg.SampleStrategy = "ratio"
|
||
cfg.SampleValue = ratio
|
||
backupErr = engine.BackupSample(ctx, dbName)
|
||
case "cluster":
|
||
backupErr = engine.BackupCluster(ctx)
|
||
default:
|
||
return backupCompleteMsg{err: fmt.Errorf("unknown backup type: %s", backupType)}
|
||
}
|
||
|
||
if backupErr != nil {
|
||
return backupCompleteMsg{
|
||
result: "",
|
||
err: fmt.Errorf("backup failed: %w", backupErr),
|
||
}
|
||
}
|
||
|
||
elapsed := time.Since(start).Round(time.Second)
|
||
|
||
var result string
|
||
switch backupType {
|
||
case "single":
|
||
result = fmt.Sprintf("✓ Single database backup of '%s' completed successfully in %v", dbName, elapsed)
|
||
case "sample":
|
||
result = fmt.Sprintf("✓ Sample backup of '%s' (ratio: %d) completed successfully in %v", dbName, ratio, elapsed)
|
||
case "cluster":
|
||
result = fmt.Sprintf("✓ Cluster backup completed successfully in %v", elapsed)
|
||
}
|
||
|
||
return backupCompleteMsg{
|
||
result: result,
|
||
err: nil,
|
||
}
|
||
}
|
||
}
|
||
|
||
func (m BackupExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
switch msg := msg.(type) {
|
||
case backupProgressMsg:
|
||
m.status = msg.status
|
||
m.progress = msg.progress
|
||
return m, nil
|
||
|
||
case backupCompleteMsg:
|
||
m.done = true
|
||
m.err = msg.err
|
||
m.result = msg.result
|
||
if m.err == nil {
|
||
m.status = "✅ Backup completed successfully!"
|
||
} else {
|
||
m.status = fmt.Sprintf("❌ Backup failed: %v", m.err)
|
||
}
|
||
return m, nil
|
||
|
||
case tea.KeyMsg:
|
||
if m.done {
|
||
switch msg.String() {
|
||
case "enter", "esc", "q":
|
||
return m.parent, nil
|
||
}
|
||
}
|
||
}
|
||
|
||
return m, nil
|
||
}
|
||
|
||
func (m BackupExecutionModel) View() string {
|
||
var s strings.Builder
|
||
|
||
header := titleStyle.Render("🔄 Backup Execution")
|
||
s.WriteString(fmt.Sprintf("\n%s\n\n", header))
|
||
|
||
s.WriteString(fmt.Sprintf("Type: %s\n", m.backupType))
|
||
if m.databaseName != "" {
|
||
s.WriteString(fmt.Sprintf("Database: %s\n", m.databaseName))
|
||
}
|
||
if m.ratio > 0 {
|
||
s.WriteString(fmt.Sprintf("Sample Ratio: %d\n", m.ratio))
|
||
}
|
||
s.WriteString(fmt.Sprintf("Duration: %s\n\n", time.Since(m.startTime).Round(time.Second)))
|
||
|
||
s.WriteString(fmt.Sprintf("Status: %s\n", m.status))
|
||
|
||
if !m.done {
|
||
spinner := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
||
frame := int(time.Since(m.startTime).Milliseconds()/100) % len(spinner)
|
||
s.WriteString(fmt.Sprintf("\n%s Processing...\n", spinner[frame]))
|
||
} else {
|
||
s.WriteString("\n")
|
||
if m.err != nil {
|
||
s.WriteString(fmt.Sprintf("Error: %v\n\n", m.err))
|
||
}
|
||
lines := strings.Split(m.result, "\n")
|
||
for _, line := range lines {
|
||
if strings.Contains(line, "✅") || strings.Contains(line, "completed") ||
|
||
strings.Contains(line, "Size:") || strings.Contains(line, "backup_") {
|
||
s.WriteString(line + "\n")
|
||
}
|
||
}
|
||
s.WriteString("\n⌨️ Press Enter or ESC to return to menu\n")
|
||
}
|
||
|
||
return s.String()
|
||
}
|