From 12c1ab476ca76a743c41f59617f5df5a52b4caae Mon Sep 17 00:00:00 2001 From: Gu1llaum-3 Date: Thu, 11 Sep 2025 17:10:39 +0200 Subject: [PATCH] feat: centralize history storage in config directory Automatically migrates existing ~/.ssh/sshm_history.json to platform-appropriate config location --- internal/config/ssh.go | 13 +++++- internal/history/history.go | 55 +++++++++++++++++++++++++- internal/history/history_test.go | 68 ++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 4 deletions(-) diff --git a/internal/config/ssh.go b/internal/config/ssh.go index 3c1a16e..2932f5c 100644 --- a/internal/config/ssh.go +++ b/internal/config/ssh.go @@ -40,8 +40,8 @@ func GetDefaultSSHConfigPath() (string, error) { } } -// GetSSHMBackupDir returns the SSHM backup directory -func GetSSHMBackupDir() (string, error) { +// GetSSHMConfigDir returns the SSHM config directory +func GetSSHMConfigDir() (string, error) { homeDir, err := os.UserHomeDir() if err != nil { return "", err @@ -67,6 +67,15 @@ func GetSSHMBackupDir() (string, error) { } } + return configDir, nil +} + +// GetSSHMBackupDir returns the SSHM backup directory +func GetSSHMBackupDir() (string, error) { + configDir, err := GetSSHMConfigDir() + if err != nil { + return "", err + } return filepath.Join(configDir, "backups"), nil } diff --git a/internal/history/history.go b/internal/history/history.go index 411313f..85b83ef 100644 --- a/internal/history/history.go +++ b/internal/history/history.go @@ -30,12 +30,23 @@ type HistoryManager struct { // NewHistoryManager creates a new history manager func NewHistoryManager() (*HistoryManager, error) { - homeDir, err := os.UserHomeDir() + configDir, err := config.GetSSHMConfigDir() if err != nil { return nil, err } - historyPath := filepath.Join(homeDir, ".ssh", "sshm_history.json") + // Ensure config dir exists + if err := os.MkdirAll(configDir, 0755); err != nil { + return nil, err + } + + historyPath := filepath.Join(configDir, "sshm_history.json") + + // Migration: check if old history file exists and migrate it + if err := migrateOldHistoryFile(historyPath); err != nil { + // Don't fail if migration fails, just log it + // In a production environment, you might want to log this properly + } hm := &HistoryManager{ historyPath: historyPath, @@ -54,6 +65,46 @@ func NewHistoryManager() (*HistoryManager, error) { return hm, nil } +// migrateOldHistoryFile migrates the old history file from ~/.ssh to ~/.config/sshm +// TODO: Remove this migration logic in v2.0.0 (introduced in v1.6.0) +func migrateOldHistoryFile(newHistoryPath string) error { + // Check if new file already exists, skip migration + if _, err := os.Stat(newHistoryPath); err == nil { + return nil // New file exists, no migration needed + } + + // Get old history file path - use same logic as SSH config location + sshDir, err := config.GetSSHDirectory() + if err != nil { + return err + } + oldHistoryPath := filepath.Join(sshDir, "sshm_history.json") + + // Check if old file exists + if _, err := os.Stat(oldHistoryPath); os.IsNotExist(err) { + return nil // Old file doesn't exist, nothing to migrate + } + + // Read old file + data, err := os.ReadFile(oldHistoryPath) + if err != nil { + return err + } + + // Write to new location + if err := os.WriteFile(newHistoryPath, data, 0644); err != nil { + return err + } + + // Remove old file only if write was successful + if err := os.Remove(oldHistoryPath); err != nil { + // Don't fail if we can't remove the old file + // The migration was successful even if cleanup failed + } + + return nil +} + // loadHistory loads the connection history from the JSON file func (hm *HistoryManager) loadHistory() error { data, err := os.ReadFile(hm.historyPath) diff --git a/internal/history/history_test.go b/internal/history/history_test.go index 1c6b370..5fdfdf5 100644 --- a/internal/history/history_test.go +++ b/internal/history/history_test.go @@ -1,6 +1,7 @@ package history import ( + "os" "path/filepath" "testing" "time" @@ -94,3 +95,70 @@ func TestHistoryManager_GetConnectionCount(t *testing.T) { t.Errorf("Expected connection count 3, got %d", count) } } + +func TestMigrateOldHistoryFile(t *testing.T) { + // This test verifies that migration doesn't fail when called + // The actual migration logic will be tested in integration tests + + tempDir := t.TempDir() + newHistoryPath := filepath.Join(tempDir, "sshm_history.json") + + // Test that migration works when no old file exists (common case) + if err := migrateOldHistoryFile(newHistoryPath); err != nil { + t.Errorf("migrateOldHistoryFile() with no old file error = %v", err) + } + + // Test that migration skips when new file already exists + if err := os.WriteFile(newHistoryPath, []byte(`{"connections":{}}`), 0644); err != nil { + t.Fatalf("Failed to write new history file: %v", err) + } + + if err := migrateOldHistoryFile(newHistoryPath); err != nil { + t.Errorf("migrateOldHistoryFile() with existing new file error = %v", err) + } + + // File should be unchanged + data, err := os.ReadFile(newHistoryPath) + if err != nil { + t.Errorf("Failed to read new file: %v", err) + } + if string(data) != `{"connections":{}}` { + t.Error("New file was modified when it shouldn't have been") + } +} + +func TestMigrateOldHistoryFile_NoOldFile(t *testing.T) { + // Test migration when no old file exists + tempDir := t.TempDir() + newHistoryPath := filepath.Join(tempDir, "sshm_history.json") + + // Should not return error when old file doesn't exist + if err := migrateOldHistoryFile(newHistoryPath); err != nil { + t.Errorf("migrateOldHistoryFile() with no old file error = %v", err) + } +} + +func TestMigrateOldHistoryFile_NewFileExists(t *testing.T) { + // Test migration when new file already exists (should skip migration) + tempDir := t.TempDir() + newHistoryPath := filepath.Join(tempDir, "sshm_history.json") + + // Create new file first + if err := os.WriteFile(newHistoryPath, []byte(`{"connections":{}}`), 0644); err != nil { + t.Fatalf("Failed to write new history file: %v", err) + } + + // Migration should skip when new file exists + if err := migrateOldHistoryFile(newHistoryPath); err != nil { + t.Errorf("migrateOldHistoryFile() with existing new file error = %v", err) + } + + // New file should be unchanged + data, err := os.ReadFile(newHistoryPath) + if err != nil { + t.Errorf("Failed to read new file: %v", err) + } + if string(data) != `{"connections":{}}` { + t.Error("New file was modified when it shouldn't have been") + } +}