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
This commit is contained in:
@@ -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
|
// ensureDatabaseExists checks if a database exists and creates it if not
|
||||||
func (e *Engine) ensureDatabaseExists(ctx context.Context, dbName string) error {
|
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
|
// Skip creation for postgres and template databases - they should already exist
|
||||||
if dbName == "postgres" || dbName == "template0" || dbName == "template1" {
|
if dbName == "postgres" || dbName == "template0" || dbName == "template1" {
|
||||||
e.log.Info("Skipping create for system database (assume exists)", "name", dbName)
|
e.log.Info("Skipping create for system database (assume exists)", "name", dbName)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package restore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -21,6 +22,25 @@ const (
|
|||||||
FormatUnknown ArchiveFormat = "Unknown"
|
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
|
// DetectArchiveFormat detects the format of a backup archive from its filename and content
|
||||||
func DetectArchiveFormat(filename string) ArchiveFormat {
|
func DetectArchiveFormat(filename string) ArchiveFormat {
|
||||||
lower := strings.ToLower(filename)
|
lower := strings.ToLower(filename)
|
||||||
@@ -54,7 +74,14 @@ func DetectArchiveFormat(filename string) ArchiveFormat {
|
|||||||
|
|
||||||
// Check for compressed SQL formats
|
// Check for compressed SQL formats
|
||||||
if strings.HasSuffix(lower, ".sql.gz") {
|
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") {
|
if strings.Contains(lower, "mysql") || strings.Contains(lower, "mariadb") {
|
||||||
return FormatMySQLSQLGz
|
return FormatMySQLSQLGz
|
||||||
}
|
}
|
||||||
@@ -63,7 +90,14 @@ func DetectArchiveFormat(filename string) ArchiveFormat {
|
|||||||
|
|
||||||
// Check for uncompressed SQL formats
|
// Check for uncompressed SQL formats
|
||||||
if strings.HasSuffix(lower, ".sql") {
|
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") {
|
if strings.Contains(lower, "mysql") || strings.Contains(lower, "mariadb") {
|
||||||
return FormatMySQLSQL
|
return FormatMySQLSQL
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,6 +164,10 @@ func (m ArchiveBrowserModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if len(m.archives) == 0 {
|
if len(m.archives) == 0 {
|
||||||
m.message = "No backup archives found"
|
m.message = "No backup archives found"
|
||||||
}
|
}
|
||||||
|
// Auto-forward in auto-confirm mode
|
||||||
|
if m.config.TUIAutoConfirm {
|
||||||
|
return m.parent, tea.Quit
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
|||||||
@@ -199,6 +199,10 @@ func (m BackupExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
} else {
|
} else {
|
||||||
m.status = fmt.Sprintf("❌ Backup failed: %v", m.err)
|
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
|
return m, nil
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ func (m BackupManagerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
// Get free space (simplified - just show message)
|
// Get free space (simplified - just show message)
|
||||||
m.message = fmt.Sprintf("Loaded %d archive(s)", len(m.archives))
|
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
|
return m, nil
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
|||||||
@@ -49,12 +49,36 @@ func NewConfirmationModelWithAction(cfg *config.Config, log logger.Logger, paren
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m ConfirmationModel) Init() tea.Cmd {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// autoConfirmMsg triggers automatic confirmation
|
||||||
|
type autoConfirmMsg struct{}
|
||||||
|
|
||||||
func (m ConfirmationModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m ConfirmationModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
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:
|
case tea.KeyMsg:
|
||||||
|
// Auto-forward ESC/quit in auto-confirm mode
|
||||||
|
if m.config.TUIAutoConfirm {
|
||||||
|
return m.parent, tea.Quit
|
||||||
|
}
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c", "q", "esc", "n":
|
case "ctrl+c", "q", "esc", "n":
|
||||||
return m.parent, nil
|
return m.parent, nil
|
||||||
|
|||||||
@@ -85,18 +85,25 @@ func (m DatabaseSelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
} else {
|
} else {
|
||||||
m.databases = msg.databases
|
m.databases = msg.databases
|
||||||
|
|
||||||
// Auto-select database if specified
|
// Auto-select database if specified, or first database if auto-confirm is enabled
|
||||||
if m.config.TUIAutoDatabase != "" {
|
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 {
|
for i, db := range m.databases {
|
||||||
if db == m.config.TUIAutoDatabase {
|
if db == autoSelectDB {
|
||||||
m.cursor = i
|
m.cursor = i
|
||||||
m.selected = db
|
m.selected = db
|
||||||
m.logger.Info("Auto-selected database", "database", db)
|
m.logger.Info("Auto-selected database", "database", db)
|
||||||
|
|
||||||
// If sample backup, ask for ratio (or auto-use default)
|
// If sample backup, ask for ratio (or auto-use default)
|
||||||
if m.backupType == "sample" {
|
if m.backupType == "sample" {
|
||||||
if m.config.TUIDryRun {
|
if m.config.TUIDryRun || m.config.TUIAutoConfirm {
|
||||||
// In dry-run, use default ratio
|
// In dry-run or auto-confirm, use default ratio
|
||||||
executor := NewBackupExecution(m.config, m.logger, m.parent, m.ctx, m.backupType, m.selected, 10)
|
executor := NewBackupExecution(m.config, m.logger, m.parent, m.ctx, m.backupType, m.selected, 10)
|
||||||
return executor, executor.Init()
|
return executor, executor.Init()
|
||||||
}
|
}
|
||||||
@@ -119,6 +126,10 @@ func (m DatabaseSelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
// Auto-forward ESC/quit in auto-confirm mode
|
||||||
|
if m.config.TUIAutoConfirm {
|
||||||
|
return m.parent, tea.Quit
|
||||||
|
}
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c", "q", "esc":
|
case "ctrl+c", "q", "esc":
|
||||||
return m.parent, nil
|
return m.parent, nil
|
||||||
|
|||||||
@@ -113,6 +113,11 @@ func (m HistoryViewModel) Init() tea.Cmd {
|
|||||||
func (m HistoryViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m HistoryViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
maxVisible := 15 // Show max 15 items at once
|
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) {
|
switch msg := msg.(type) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
|
|||||||
@@ -39,11 +39,30 @@ func NewInputModel(cfg *config.Config, log logger.Logger, parent tea.Model, titl
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m InputModel) Init() tea.Cmd {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// inputAutoConfirmMsg triggers automatic input confirmation
|
||||||
|
type inputAutoConfirmMsg struct{}
|
||||||
|
|
||||||
func (m InputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m InputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
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:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c", "esc":
|
case "ctrl+c", "esc":
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ func (m OperationsViewModel) Init() tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m OperationsViewModel) Update(msg tea.Msg) (tea.Model, 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) {
|
switch msg := msg.(type) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
|
|||||||
@@ -254,6 +254,10 @@ func (m RestoreExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.status = "Failed"
|
m.status = "Failed"
|
||||||
m.phase = "Error"
|
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
|
return m, nil
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
|||||||
@@ -212,6 +212,10 @@ func (m RestorePreviewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.canProceed = msg.canProceed
|
m.canProceed = msg.canProceed
|
||||||
m.existingDBCount = msg.existingDBCount
|
m.existingDBCount = msg.existingDBCount
|
||||||
m.existingDBs = msg.existingDBs
|
m.existingDBs = msg.existingDBs
|
||||||
|
// Auto-forward in auto-confirm mode
|
||||||
|
if m.config.TUIAutoConfirm {
|
||||||
|
return m.parent, tea.Quit
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
|||||||
@@ -390,12 +390,24 @@ func NewSettingsModel(cfg *config.Config, log logger.Logger, parent tea.Model) S
|
|||||||
|
|
||||||
// Init initializes the settings model
|
// Init initializes the settings model
|
||||||
func (m SettingsModel) Init() tea.Cmd {
|
func (m SettingsModel) Init() tea.Cmd {
|
||||||
|
// Auto-forward in auto-confirm mode
|
||||||
|
if m.config.TUIAutoConfirm {
|
||||||
|
return func() tea.Msg {
|
||||||
|
return settingsAutoQuitMsg{}
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// settingsAutoQuitMsg triggers automatic quit in settings
|
||||||
|
type settingsAutoQuitMsg struct{}
|
||||||
|
|
||||||
// Update handles messages
|
// Update handles messages
|
||||||
func (m SettingsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m SettingsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
case settingsAutoQuitMsg:
|
||||||
|
return m.parent, tea.Quit
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
// Handle directory browsing mode
|
// Handle directory browsing mode
|
||||||
if m.browsingDir && m.dirBrowser != nil {
|
if m.browsingDir && m.dirBrowser != nil {
|
||||||
|
|||||||
@@ -37,12 +37,21 @@ func NewStatusView(cfg *config.Config, log logger.Logger, parent tea.Model) Stat
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m StatusViewModel) Init() tea.Cmd {
|
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(
|
return tea.Batch(
|
||||||
fetchStatus(m.config, m.logger),
|
fetchStatus(m.config, m.logger),
|
||||||
tickCmd(),
|
tickCmd(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// statusAutoQuitMsg triggers automatic quit
|
||||||
|
type statusAutoQuitMsg struct{}
|
||||||
|
|
||||||
type tickMsg time.Time
|
type tickMsg time.Time
|
||||||
|
|
||||||
func tickCmd() tea.Cmd {
|
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) {
|
func (m StatusViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
case statusAutoQuitMsg:
|
||||||
|
return m.parent, tea.Quit
|
||||||
|
|
||||||
case tickMsg:
|
case tickMsg:
|
||||||
if m.loading {
|
if m.loading {
|
||||||
return m, tickCmd()
|
return m, tickCmd()
|
||||||
@@ -126,6 +138,10 @@ func (m StatusViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.dbVersion = msg.dbVersion
|
m.dbVersion = msg.dbVersion
|
||||||
}
|
}
|
||||||
m.connected = msg.connected
|
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
|
return m, nil
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
|||||||
Reference in New Issue
Block a user