From d710578c48bb1d39af2339cf83efa4136d567b24 Mon Sep 17 00:00:00 2001 From: "A. Renz" Date: Fri, 12 Dec 2025 12:38:20 +0100 Subject: [PATCH] Fix MySQL support and TUI auto-confirm mode - Fix format detection to read database_type from .meta.json metadata file - Add ensureMySQLDatabaseExists() for MySQL/MariaDB database creation - Route database creation to correct implementation based on db type - Add TUI auto-forward in auto-confirm mode (no input required for debugging) - All TUI components now exit automatically when --auto-confirm is set - Fix status view to skip loading in auto-confirm mode --- internal/restore/engine.go | 34 +++++++++++++++++++++++++++++ internal/restore/formats.go | 38 +++++++++++++++++++++++++++++++-- internal/tui/archive_browser.go | 4 ++++ internal/tui/backup_exec.go | 4 ++++ internal/tui/backup_manager.go | 4 ++++ internal/tui/confirmation.go | 24 +++++++++++++++++++++ internal/tui/dbselector.go | 21 +++++++++++++----- internal/tui/history.go | 5 +++++ internal/tui/input.go | 19 +++++++++++++++++ internal/tui/operations.go | 5 +++++ internal/tui/restore_exec.go | 4 ++++ internal/tui/restore_preview.go | 4 ++++ internal/tui/settings.go | 12 +++++++++++ internal/tui/status.go | 16 ++++++++++++++ 14 files changed, 187 insertions(+), 7 deletions(-) diff --git a/internal/restore/engine.go b/internal/restore/engine.go index 3da4a3f..584d9a5 100755 --- a/internal/restore/engine.go +++ b/internal/restore/engine.go @@ -971,6 +971,40 @@ func (e *Engine) dropDatabaseIfExists(ctx context.Context, dbName string) error // ensureDatabaseExists checks if a database exists and creates it if not func (e *Engine) ensureDatabaseExists(ctx context.Context, dbName string) error { + // Route to appropriate implementation based on database type + if e.cfg.DatabaseType == "mysql" || e.cfg.DatabaseType == "mariadb" { + return e.ensureMySQLDatabaseExists(ctx, dbName) + } + return e.ensurePostgresDatabaseExists(ctx, dbName) +} + +// ensureMySQLDatabaseExists checks if a MySQL database exists and creates it if not +func (e *Engine) ensureMySQLDatabaseExists(ctx context.Context, dbName string) error { + // Build mysql command + args := []string{ + "-h", e.cfg.Host, + "-P", fmt.Sprintf("%d", e.cfg.Port), + "-u", e.cfg.User, + "-e", fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`", dbName), + } + + if e.cfg.Password != "" { + args = append(args, fmt.Sprintf("-p%s", e.cfg.Password)) + } + + cmd := exec.CommandContext(ctx, "mysql", args...) + output, err := cmd.CombinedOutput() + if err != nil { + e.log.Warn("MySQL database creation failed", "name", dbName, "error", err, "output", string(output)) + return fmt.Errorf("failed to create database '%s': %w (output: %s)", dbName, err, strings.TrimSpace(string(output))) + } + + e.log.Info("Successfully ensured MySQL database exists", "name", dbName) + return nil +} + +// ensurePostgresDatabaseExists checks if a PostgreSQL database exists and creates it if not +func (e *Engine) ensurePostgresDatabaseExists(ctx context.Context, dbName string) error { // Skip creation for postgres and template databases - they should already exist if dbName == "postgres" || dbName == "template0" || dbName == "template1" { e.log.Info("Skipping create for system database (assume exists)", "name", dbName) diff --git a/internal/restore/formats.go b/internal/restore/formats.go index 77efd94..2c7fdab 100755 --- a/internal/restore/formats.go +++ b/internal/restore/formats.go @@ -2,6 +2,7 @@ package restore import ( "compress/gzip" + "encoding/json" "io" "os" "strings" @@ -21,6 +22,25 @@ const ( FormatUnknown ArchiveFormat = "Unknown" ) +// backupMetadata represents the structure of .meta.json files +type backupMetadata struct { + DatabaseType string `json:"database_type"` +} + +// readMetadataDBType reads the database_type from the .meta.json file if it exists +func readMetadataDBType(archivePath string) string { + metaPath := archivePath + ".meta.json" + data, err := os.ReadFile(metaPath) + if err != nil { + return "" + } + var meta backupMetadata + if err := json.Unmarshal(data, &meta); err != nil { + return "" + } + return strings.ToLower(meta.DatabaseType) +} + // DetectArchiveFormat detects the format of a backup archive from its filename and content func DetectArchiveFormat(filename string) ArchiveFormat { lower := strings.ToLower(filename) @@ -54,7 +74,14 @@ func DetectArchiveFormat(filename string) ArchiveFormat { // Check for compressed SQL formats if strings.HasSuffix(lower, ".sql.gz") { - // Determine if MySQL or PostgreSQL based on naming convention + // First, try to determine from metadata file + if dbType := readMetadataDBType(filename); dbType != "" { + if dbType == "mysql" || dbType == "mariadb" { + return FormatMySQLSQLGz + } + return FormatPostgreSQLSQLGz + } + // Fallback: determine if MySQL or PostgreSQL based on naming convention if strings.Contains(lower, "mysql") || strings.Contains(lower, "mariadb") { return FormatMySQLSQLGz } @@ -63,7 +90,14 @@ func DetectArchiveFormat(filename string) ArchiveFormat { // Check for uncompressed SQL formats if strings.HasSuffix(lower, ".sql") { - // Determine if MySQL or PostgreSQL based on naming convention + // First, try to determine from metadata file + if dbType := readMetadataDBType(filename); dbType != "" { + if dbType == "mysql" || dbType == "mariadb" { + return FormatMySQLSQLGz + } + return FormatPostgreSQLSQL + } + // Fallback: determine if MySQL or PostgreSQL based on naming convention if strings.Contains(lower, "mysql") || strings.Contains(lower, "mariadb") { return FormatMySQLSQL } diff --git a/internal/tui/archive_browser.go b/internal/tui/archive_browser.go index 105caba..f9bdf37 100755 --- a/internal/tui/archive_browser.go +++ b/internal/tui/archive_browser.go @@ -164,6 +164,10 @@ func (m ArchiveBrowserModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if len(m.archives) == 0 { m.message = "No backup archives found" } + // Auto-forward in auto-confirm mode + if m.config.TUIAutoConfirm { + return m.parent, tea.Quit + } return m, nil case tea.KeyMsg: diff --git a/internal/tui/backup_exec.go b/internal/tui/backup_exec.go index f364e15..eefdfcb 100755 --- a/internal/tui/backup_exec.go +++ b/internal/tui/backup_exec.go @@ -199,6 +199,10 @@ func (m BackupExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } else { m.status = fmt.Sprintf("❌ Backup failed: %v", m.err) } + // Auto-forward in debug/auto-confirm mode + if m.config.TUIAutoConfirm { + return m.parent, tea.Quit + } return m, nil case tea.KeyMsg: diff --git a/internal/tui/backup_manager.go b/internal/tui/backup_manager.go index 3085159..ccddd27 100755 --- a/internal/tui/backup_manager.go +++ b/internal/tui/backup_manager.go @@ -61,6 +61,10 @@ func (m BackupManagerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Get free space (simplified - just show message) m.message = fmt.Sprintf("Loaded %d archive(s)", len(m.archives)) + // Auto-forward in auto-confirm mode + if m.config.TUIAutoConfirm { + return m.parent, tea.Quit + } return m, nil case tea.KeyMsg: diff --git a/internal/tui/confirmation.go b/internal/tui/confirmation.go index 6179108..4dad945 100755 --- a/internal/tui/confirmation.go +++ b/internal/tui/confirmation.go @@ -49,12 +49,36 @@ func NewConfirmationModelWithAction(cfg *config.Config, log logger.Logger, paren } func (m ConfirmationModel) Init() tea.Cmd { + // Auto-confirm in debug/auto-confirm mode + if m.config.TUIAutoConfirm { + if m.onConfirm != nil { + return func() tea.Msg { + return autoConfirmMsg{} + } + } + } return nil } +// autoConfirmMsg triggers automatic confirmation +type autoConfirmMsg struct{} + func (m ConfirmationModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { + case autoConfirmMsg: + // Auto-confirm triggered + m.confirmed = true + if m.onConfirm != nil { + return m.onConfirm() + } + executor := NewBackupExecution(m.config, m.logger, m.parent, m.ctx, "cluster", "", 0) + return executor, executor.Init() + case tea.KeyMsg: + // Auto-forward ESC/quit in auto-confirm mode + if m.config.TUIAutoConfirm { + return m.parent, tea.Quit + } switch msg.String() { case "ctrl+c", "q", "esc", "n": return m.parent, nil diff --git a/internal/tui/dbselector.go b/internal/tui/dbselector.go index 93dab58..0bb84c4 100755 --- a/internal/tui/dbselector.go +++ b/internal/tui/dbselector.go @@ -85,18 +85,25 @@ func (m DatabaseSelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } else { m.databases = msg.databases - // Auto-select database if specified - if m.config.TUIAutoDatabase != "" { + // Auto-select database if specified, or first database if auto-confirm is enabled + autoSelectDB := m.config.TUIAutoDatabase + if autoSelectDB == "" && m.config.TUIAutoConfirm && len(m.databases) > 0 { + // Auto-confirm mode: select first database automatically + autoSelectDB = m.databases[0] + m.logger.Info("Auto-confirm mode: selecting first database", "database", autoSelectDB) + } + + if autoSelectDB != "" { for i, db := range m.databases { - if db == m.config.TUIAutoDatabase { + if db == autoSelectDB { m.cursor = i m.selected = db m.logger.Info("Auto-selected database", "database", db) // If sample backup, ask for ratio (or auto-use default) if m.backupType == "sample" { - if m.config.TUIDryRun { - // In dry-run, use default ratio + if m.config.TUIDryRun || m.config.TUIAutoConfirm { + // In dry-run or auto-confirm, use default ratio executor := NewBackupExecution(m.config, m.logger, m.parent, m.ctx, m.backupType, m.selected, 10) return executor, executor.Init() } @@ -119,6 +126,10 @@ func (m DatabaseSelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case tea.KeyMsg: + // Auto-forward ESC/quit in auto-confirm mode + if m.config.TUIAutoConfirm { + return m.parent, tea.Quit + } switch msg.String() { case "ctrl+c", "q", "esc": return m.parent, nil diff --git a/internal/tui/history.go b/internal/tui/history.go index d084c5b..84d0443 100755 --- a/internal/tui/history.go +++ b/internal/tui/history.go @@ -113,6 +113,11 @@ func (m HistoryViewModel) Init() tea.Cmd { func (m HistoryViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { maxVisible := 15 // Show max 15 items at once + // Auto-forward in auto-confirm mode + if m.config.TUIAutoConfirm { + return m.parent, tea.Quit + } + switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { diff --git a/internal/tui/input.go b/internal/tui/input.go index 950d378..65083ef 100755 --- a/internal/tui/input.go +++ b/internal/tui/input.go @@ -39,11 +39,30 @@ func NewInputModel(cfg *config.Config, log logger.Logger, parent tea.Model, titl } func (m InputModel) Init() tea.Cmd { + // Auto-confirm: use default value and proceed + if m.config.TUIAutoConfirm && m.value != "" { + return func() tea.Msg { + return inputAutoConfirmMsg{} + } + } return nil } +// inputAutoConfirmMsg triggers automatic input confirmation +type inputAutoConfirmMsg struct{} + func (m InputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { + case inputAutoConfirmMsg: + // Use default value and proceed + if selector, ok := m.parent.(DatabaseSelectorModel); ok { + ratio, _ := strconv.Atoi(m.value) + executor := NewBackupExecution(selector.config, selector.logger, selector.parent, selector.ctx, + selector.backupType, selector.selected, ratio) + return executor, executor.Init() + } + return m.parent, tea.Quit + case tea.KeyMsg: switch msg.String() { case "ctrl+c", "esc": diff --git a/internal/tui/operations.go b/internal/tui/operations.go index 05ef175..6c9eadd 100755 --- a/internal/tui/operations.go +++ b/internal/tui/operations.go @@ -30,6 +30,11 @@ func (m OperationsViewModel) Init() tea.Cmd { } func (m OperationsViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + // Auto-forward in auto-confirm mode + if m.config.TUIAutoConfirm { + return m.parent, tea.Quit + } + switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { diff --git a/internal/tui/restore_exec.go b/internal/tui/restore_exec.go index dfc87c7..889c76a 100755 --- a/internal/tui/restore_exec.go +++ b/internal/tui/restore_exec.go @@ -254,6 +254,10 @@ func (m RestoreExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.status = "Failed" m.phase = "Error" } + // Auto-forward in auto-confirm mode when done + if m.config.TUIAutoConfirm && m.done { + return m.parent, tea.Quit + } return m, nil case tea.KeyMsg: diff --git a/internal/tui/restore_preview.go b/internal/tui/restore_preview.go index f90b903..9737096 100755 --- a/internal/tui/restore_preview.go +++ b/internal/tui/restore_preview.go @@ -212,6 +212,10 @@ func (m RestorePreviewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.canProceed = msg.canProceed m.existingDBCount = msg.existingDBCount m.existingDBs = msg.existingDBs + // Auto-forward in auto-confirm mode + if m.config.TUIAutoConfirm { + return m.parent, tea.Quit + } return m, nil case tea.KeyMsg: diff --git a/internal/tui/settings.go b/internal/tui/settings.go index 97158eb..1850876 100755 --- a/internal/tui/settings.go +++ b/internal/tui/settings.go @@ -390,12 +390,24 @@ func NewSettingsModel(cfg *config.Config, log logger.Logger, parent tea.Model) S // Init initializes the settings model func (m SettingsModel) Init() tea.Cmd { + // Auto-forward in auto-confirm mode + if m.config.TUIAutoConfirm { + return func() tea.Msg { + return settingsAutoQuitMsg{} + } + } return nil } +// settingsAutoQuitMsg triggers automatic quit in settings +type settingsAutoQuitMsg struct{} + // Update handles messages func (m SettingsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { + case settingsAutoQuitMsg: + return m.parent, tea.Quit + case tea.KeyMsg: // Handle directory browsing mode if m.browsingDir && m.dirBrowser != nil { diff --git a/internal/tui/status.go b/internal/tui/status.go index 95eaf62..a618863 100755 --- a/internal/tui/status.go +++ b/internal/tui/status.go @@ -37,12 +37,21 @@ func NewStatusView(cfg *config.Config, log logger.Logger, parent tea.Model) Stat } func (m StatusViewModel) Init() tea.Cmd { + // Auto-forward in auto-confirm mode - skip status check + if m.config.TUIAutoConfirm { + return func() tea.Msg { + return statusAutoQuitMsg{} + } + } return tea.Batch( fetchStatus(m.config, m.logger), tickCmd(), ) } +// statusAutoQuitMsg triggers automatic quit +type statusAutoQuitMsg struct{} + type tickMsg time.Time func tickCmd() tea.Cmd { @@ -109,6 +118,9 @@ func fetchStatus(cfg *config.Config, log logger.Logger) tea.Cmd { func (m StatusViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { + case statusAutoQuitMsg: + return m.parent, tea.Quit + case tickMsg: if m.loading { return m, tickCmd() @@ -126,6 +138,10 @@ func (m StatusViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.dbVersion = msg.dbVersion } m.connected = msg.connected + // Auto-forward in auto-confirm mode after status loads + if m.config.TUIAutoConfirm { + return m.parent, tea.Quit + } return m, nil case tea.KeyMsg: