diff --git a/internal/config/config.go b/internal/config/config.go index b339cfc..b82ee2c 100755 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -64,6 +64,9 @@ type Config struct { // Cluster parallelism ClusterParallelism int // Number of concurrent databases during cluster operations (0 = sequential) + // Working directory for large operations (extraction, diagnosis) + WorkDir string // Alternative temp directory for large operations (default: system temp) + // Swap file management (for large backups) SwapFilePath string // Path to temporary swap file SwapFileSizeGB int // Size in GB (0 = disabled) diff --git a/internal/config/persist.go b/internal/config/persist.go index 8cda5e4..9302b61 100755 --- a/internal/config/persist.go +++ b/internal/config/persist.go @@ -22,6 +22,7 @@ type LocalConfig struct { // Backup settings BackupDir string + WorkDir string // Working directory for large operations Compression int Jobs int DumpJobs int @@ -97,6 +98,8 @@ func LoadLocalConfig() (*LocalConfig, error) { switch key { case "backup_dir": cfg.BackupDir = value + case "work_dir": + cfg.WorkDir = value case "compression": if c, err := strconv.Atoi(value); err == nil { cfg.Compression = c @@ -174,6 +177,9 @@ func SaveLocalConfig(cfg *LocalConfig) error { if cfg.BackupDir != "" { sb.WriteString(fmt.Sprintf("backup_dir = %s\n", cfg.BackupDir)) } + if cfg.WorkDir != "" { + sb.WriteString(fmt.Sprintf("work_dir = %s\n", cfg.WorkDir)) + } if cfg.Compression != 0 { sb.WriteString(fmt.Sprintf("compression = %d\n", cfg.Compression)) } @@ -244,6 +250,9 @@ func ApplyLocalConfig(cfg *Config, local *LocalConfig) { if local.BackupDir != "" { cfg.BackupDir = local.BackupDir } + if local.WorkDir != "" { + cfg.WorkDir = local.WorkDir + } if cfg.CompressionLevel == 6 && local.Compression != 0 { cfg.CompressionLevel = local.Compression } @@ -280,6 +289,7 @@ func ConfigFromConfig(cfg *Config) *LocalConfig { Database: cfg.Database, SSLMode: cfg.SSLMode, BackupDir: cfg.BackupDir, + WorkDir: cfg.WorkDir, Compression: cfg.CompressionLevel, Jobs: cfg.Jobs, DumpJobs: cfg.DumpJobs, diff --git a/internal/tui/diagnose_view.go b/internal/tui/diagnose_view.go index f2d2316..a4b7bee 100644 --- a/internal/tui/diagnose_view.go +++ b/internal/tui/diagnose_view.go @@ -88,8 +88,8 @@ func runDiagnosis(cfg *config.Config, log logger.Logger, archive ArchiveInfo) te // For cluster archives, we can do deep analysis if archive.Format.IsClusterBackup() { - // Create temp directory - tempDir, err := createTempDir("dbbackup-diagnose-*") + // Create temp directory (use WorkDir if configured for large archives) + tempDir, err := createTempDirIn(cfg.WorkDir, "dbbackup-diagnose-*") if err != nil { return diagnoseCompleteMsg{err: fmt.Errorf("failed to create temp dir: %w", err)} } @@ -445,6 +445,17 @@ func createTempDir(pattern string) (string, error) { return os.MkdirTemp("", pattern) } +func createTempDirIn(baseDir, pattern string) (string, error) { + if baseDir == "" { + return os.MkdirTemp("", pattern) + } + // Ensure base directory exists + if err := os.MkdirAll(baseDir, 0755); err != nil { + return "", fmt.Errorf("cannot create work directory: %w", err) + } + return os.MkdirTemp(baseDir, pattern) +} + func removeTempDir(path string) error { return os.RemoveAll(path) } diff --git a/internal/tui/restore_exec.go b/internal/tui/restore_exec.go index b969874..105e008 100755 --- a/internal/tui/restore_exec.go +++ b/internal/tui/restore_exec.go @@ -32,6 +32,7 @@ type RestoreExecutionModel struct { cleanClusterFirst bool // Drop all user databases before cluster restore existingDBs []string // List of databases to drop saveDebugLog bool // Save detailed error report on failure + workDir string // Custom work directory for extraction // Progress tracking status string @@ -50,7 +51,7 @@ type RestoreExecutionModel struct { } // NewRestoreExecution creates a new restore execution model -func NewRestoreExecution(cfg *config.Config, log logger.Logger, parent tea.Model, ctx context.Context, archive ArchiveInfo, targetDB string, cleanFirst, createIfMissing bool, restoreType string, cleanClusterFirst bool, existingDBs []string, saveDebugLog bool) RestoreExecutionModel { +func NewRestoreExecution(cfg *config.Config, log logger.Logger, parent tea.Model, ctx context.Context, archive ArchiveInfo, targetDB string, cleanFirst, createIfMissing bool, restoreType string, cleanClusterFirst bool, existingDBs []string, saveDebugLog bool, workDir string) RestoreExecutionModel { return RestoreExecutionModel{ config: cfg, logger: log, @@ -64,6 +65,7 @@ func NewRestoreExecution(cfg *config.Config, log logger.Logger, parent tea.Model cleanClusterFirst: cleanClusterFirst, existingDBs: existingDBs, saveDebugLog: saveDebugLog, + workDir: workDir, status: "Initializing...", phase: "Starting", startTime: time.Now(), diff --git a/internal/tui/restore_preview.go b/internal/tui/restore_preview.go index ef452b0..bb8d3c7 100755 --- a/internal/tui/restore_preview.go +++ b/internal/tui/restore_preview.go @@ -60,6 +60,7 @@ type RestorePreviewModel struct { canProceed bool message string saveDebugLog bool // Save detailed error report on failure + workDir string // Custom work directory for extraction } // NewRestorePreview creates a new restore preview @@ -81,6 +82,7 @@ func NewRestorePreview(cfg *config.Config, log logger.Logger, parent tea.Model, cleanFirst: false, createIfMissing: true, checking: true, + workDir: cfg.WorkDir, // Use configured work directory safetyChecks: []SafetyCheck{ {Name: "Archive integrity", Status: "pending", Critical: true}, {Name: "Dump validity", Status: "pending", Critical: true}, @@ -280,6 +282,18 @@ func (m RestorePreviewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.message = "Debug log: disabled" } + case "w": + // Toggle/set work directory + if m.workDir == "" { + // Set to backup directory as default alternative + m.workDir = m.config.BackupDir + m.message = infoStyle.Render(fmt.Sprintf("📁 Work directory set to: %s", m.workDir)) + } else { + // Clear work directory (use system temp) + m.workDir = "" + m.message = "Work directory: using system temp" + } + case "enter", " ": if m.checking { m.message = "Please wait for safety checks to complete..." @@ -292,7 +306,7 @@ func (m RestorePreviewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // Proceed to restore execution - exec := NewRestoreExecution(m.config, m.logger, m.parent, m.ctx, m.archive, m.targetDB, m.cleanFirst, m.createIfMissing, m.mode, m.cleanClusterFirst, m.existingDBs, m.saveDebugLog) + exec := NewRestoreExecution(m.config, m.logger, m.parent, m.ctx, m.archive, m.targetDB, m.cleanFirst, m.createIfMissing, m.mode, m.cleanClusterFirst, m.existingDBs, m.saveDebugLog, m.workDir) return exec, exec.Init() } } @@ -430,6 +444,24 @@ func (m RestorePreviewModel) View() string { // Advanced Options s.WriteString(archiveHeaderStyle.Render("⚙️ Advanced Options")) s.WriteString("\n") + + // Work directory option + workDirIcon := "✗" + workDirStyle := infoStyle + workDirValue := "(system temp)" + if m.workDir != "" { + workDirIcon = "✓" + workDirStyle = checkPassedStyle + workDirValue = m.workDir + } + s.WriteString(workDirStyle.Render(fmt.Sprintf(" %s Work Dir: %s (press 'w' to toggle)", workDirIcon, workDirValue))) + s.WriteString("\n") + if m.workDir == "" { + s.WriteString(infoStyle.Render(" ⚠️ Large archives need more space than /tmp may have")) + s.WriteString("\n") + } + + // Debug log option debugIcon := "✗" debugStyle := infoStyle if m.saveDebugLog { @@ -457,15 +489,15 @@ func (m RestorePreviewModel) View() string { s.WriteString(successStyle.Render("✅ Ready to restore")) s.WriteString("\n") if m.mode == "restore-single" { - s.WriteString(infoStyle.Render("⌨️ t: Clean-first | c: Create | d: Debug log | Enter: Proceed | Esc: Cancel")) + s.WriteString(infoStyle.Render("⌨️ t: Clean-first | c: Create | w: WorkDir | d: Debug | Enter: Proceed | Esc: Cancel")) } else if m.mode == "restore-cluster" { if m.existingDBCount > 0 { - s.WriteString(infoStyle.Render("⌨️ c: Cleanup | d: Debug log | Enter: Proceed | Esc: Cancel")) + s.WriteString(infoStyle.Render("⌨️ c: Cleanup | w: WorkDir | d: Debug | Enter: Proceed | Esc: Cancel")) } else { - s.WriteString(infoStyle.Render("⌨️ d: Debug log | Enter: Proceed | Esc: Cancel")) + s.WriteString(infoStyle.Render("⌨️ w: WorkDir | d: Debug | Enter: Proceed | Esc: Cancel")) } } else { - s.WriteString(infoStyle.Render("⌨️ d: Debug log | Enter: Proceed | Esc: Cancel")) + s.WriteString(infoStyle.Render("⌨️ w: WorkDir | d: Debug | Enter: Proceed | Esc: Cancel")) } } else { s.WriteString(errorStyle.Render("❌ Cannot proceed - please fix errors above")) diff --git a/internal/tui/settings.go b/internal/tui/settings.go index 1850876..9a827d1 100755 --- a/internal/tui/settings.go +++ b/internal/tui/settings.go @@ -115,6 +115,26 @@ func NewSettingsModel(cfg *config.Config, log logger.Logger, parent tea.Model) S Type: "path", Description: "Directory where backup files will be stored", }, + { + Key: "work_dir", + DisplayName: "Work Directory", + Value: func(c *config.Config) string { + if c.WorkDir == "" { + return "(system temp)" + } + return c.WorkDir + }, + Update: func(c *config.Config, v string) error { + if v == "" || v == "(system temp)" { + c.WorkDir = "" + return nil + } + c.WorkDir = filepath.Clean(v) + return nil + }, + Type: "path", + Description: "Working directory for large operations (extraction, diagnosis). Use when /tmp is too small.", + }, { Key: "compression_level", DisplayName: "Compression Level",