refactor: move SSH backups to ~/.config/sshm/backups/

This commit is contained in:
Gu1llaum-3 2025-09-06 23:36:12 +02:00
parent e1efef4680
commit 67987e6242
3 changed files with 191 additions and 4 deletions

View File

@ -243,6 +243,29 @@ sshm --version
sshm --help
```
### Backup Configuration
SSHM automatically creates backups of your SSH configuration files before making any changes to ensure your configurations are safe.
**Backup Location:**
- **Unix/Linux/macOS**: `~/.config/sshm/backups/` (or `$XDG_CONFIG_HOME/sshm/backups/` if set)
- **Windows**: `%APPDATA%\sshm\backups\` (fallback: `%USERPROFILE%\.config\sshm\backups\`)
**Key Features:**
- Automatic backup before any modification
- One backup per file (overwrites previous backup)
- Stored separately to avoid SSH Include conflicts
- Easy manual recovery if needed
**Quick Recovery:**
```bash
# Unix/Linux/macOS
cp ~/.config/sshm/backups/config.backup ~/.ssh/config
# Windows
copy "%APPDATA%\sshm\backups\config.backup" "%USERPROFILE%\.ssh\config"
```
### Configuration File Options
By default, SSHM uses the standard SSH configuration file at `~/.ssh/config`. You can specify a different configuration file using the `-c` flag:

View File

@ -40,6 +40,36 @@ func GetDefaultSSHConfigPath() (string, error) {
}
}
// GetSSHMBackupDir returns the SSHM backup directory
func GetSSHMBackupDir() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
var configDir string
switch runtime.GOOS {
case "windows":
// Use %APPDATA%/sshm on Windows
appData := os.Getenv("APPDATA")
if appData != "" {
configDir = filepath.Join(appData, "sshm")
} else {
configDir = filepath.Join(homeDir, ".config", "sshm")
}
default:
// Use XDG Base Directory specification
xdgConfigDir := os.Getenv("XDG_CONFIG_HOME")
if xdgConfigDir != "" {
configDir = filepath.Join(xdgConfigDir, "sshm")
} else {
configDir = filepath.Join(homeDir, ".config", "sshm")
}
}
return filepath.Join(configDir, "backups"), nil
}
// GetSSHDirectory returns the .ssh directory path
func GetSSHDirectory() (string, error) {
homeDir, err := os.UserHomeDir()
@ -67,9 +97,23 @@ func ensureSSHDirectory() error {
// configMutex protects SSH config file operations from race conditions
var configMutex sync.Mutex
// backupConfig creates a backup of the SSH config file
// backupConfig creates a backup of the SSH config file in ~/.config/sshm/backups/
func backupConfig(configPath string) error {
backupPath := configPath + ".backup"
// Get backup directory and ensure it exists
backupDir, err := GetSSHMBackupDir()
if err != nil {
return fmt.Errorf("failed to get backup directory: %w", err)
}
if err := os.MkdirAll(backupDir, 0755); err != nil {
return fmt.Errorf("failed to create backup directory: %w", err)
}
// Create simple backup filename (overwrites previous backup)
filename := filepath.Base(configPath)
backupPath := filepath.Join(backupDir, filename+".backup")
// Copy file
src, err := os.Open(configPath)
if err != nil {
return err
@ -82,8 +126,12 @@ func backupConfig(configPath string) error {
}
defer dst.Close()
_, err = io.Copy(dst, src)
return err
if _, err = io.Copy(dst, src); err != nil {
return err
}
// Set appropriate permissions
return os.Chmod(backupPath, 0600)
}
// ParseSSHConfig parses the SSH config file and returns the list of hosts

View File

@ -452,6 +452,122 @@ Include *.conf
}
}
func TestBackupConfigToSSHMDirectory(t *testing.T) {
// Create temporary directory for test files
tempDir := t.TempDir()
// Override the home directory for this test
originalHome := os.Getenv("HOME")
if originalHome == "" {
originalHome = os.Getenv("USERPROFILE") // Windows
}
// Set test home directory
os.Setenv("HOME", tempDir)
defer os.Setenv("HOME", originalHome)
// Create a test SSH config file
sshDir := filepath.Join(tempDir, ".ssh")
err := os.MkdirAll(sshDir, 0700)
if err != nil {
t.Fatalf("Failed to create .ssh directory: %v", err)
}
configPath := filepath.Join(sshDir, "config")
configContent := `Host test-host
HostName test.example.com
User testuser
`
err = os.WriteFile(configPath, []byte(configContent), 0600)
if err != nil {
t.Fatalf("Failed to create config file: %v", err)
}
// Test backup creation
err = backupConfig(configPath)
if err != nil {
t.Fatalf("backupConfig() error = %v", err)
}
// Verify backup directory was created
backupDir, err := GetSSHMBackupDir()
if err != nil {
t.Fatalf("GetSSHMBackupDir() error = %v", err)
}
if _, err := os.Stat(backupDir); os.IsNotExist(err) {
t.Errorf("Backup directory was not created: %s", backupDir)
}
// Verify backup file was created
files, err := os.ReadDir(backupDir)
if err != nil {
t.Fatalf("Failed to read backup directory: %v", err)
}
if len(files) != 1 {
t.Errorf("Expected 1 backup file, got %d", len(files))
}
if len(files) > 0 {
backupFile := files[0]
expectedName := "config.backup"
if backupFile.Name() != expectedName {
t.Errorf("Backup file has unexpected name: got %s, want %s", backupFile.Name(), expectedName)
}
// Verify backup content
backupContent, err := os.ReadFile(filepath.Join(backupDir, backupFile.Name()))
if err != nil {
t.Fatalf("Failed to read backup file: %v", err)
}
if string(backupContent) != configContent {
t.Errorf("Backup content doesn't match original")
}
}
// Test that subsequent backups overwrite the previous one
newConfigContent := `Host test-host-updated
HostName updated.example.com
User updateduser
`
err = os.WriteFile(configPath, []byte(newConfigContent), 0600)
if err != nil {
t.Fatalf("Failed to update config file: %v", err)
}
// Create second backup
err = backupConfig(configPath)
if err != nil {
t.Fatalf("Second backupConfig() error = %v", err)
}
// Verify still only one backup file exists
files, err = os.ReadDir(backupDir)
if err != nil {
t.Fatalf("Failed to read backup directory after second backup: %v", err)
}
if len(files) != 1 {
t.Errorf("Expected still 1 backup file after overwrite, got %d", len(files))
}
// Verify backup content was updated
if len(files) > 0 {
backupContent, err := os.ReadFile(filepath.Join(backupDir, files[0].Name()))
if err != nil {
t.Fatalf("Failed to read updated backup file: %v", err)
}
if string(backupContent) != newConfigContent {
t.Errorf("Updated backup content doesn't match new config content")
}
}
}
func TestFindHostInAllConfigs(t *testing.T) {
// Create temporary directory for test files
tempDir := t.TempDir()