mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2025-09-07 21:30:39 +02:00
refactor: move SSH backups to ~/.config/sshm/backups/
This commit is contained in:
parent
e1efef4680
commit
67987e6242
23
README.md
23
README.md
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user