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
|
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
|
### 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:
|
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
|
// GetSSHDirectory returns the .ssh directory path
|
||||||
func GetSSHDirectory() (string, error) {
|
func GetSSHDirectory() (string, error) {
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
@ -67,9 +97,23 @@ func ensureSSHDirectory() error {
|
|||||||
// configMutex protects SSH config file operations from race conditions
|
// configMutex protects SSH config file operations from race conditions
|
||||||
var configMutex sync.Mutex
|
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 {
|
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)
|
src, err := os.Open(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -82,8 +126,12 @@ func backupConfig(configPath string) error {
|
|||||||
}
|
}
|
||||||
defer dst.Close()
|
defer dst.Close()
|
||||||
|
|
||||||
_, err = io.Copy(dst, src)
|
if _, err = io.Copy(dst, src); err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set appropriate permissions
|
||||||
|
return os.Chmod(backupPath, 0600)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseSSHConfig parses the SSH config file and returns the list of hosts
|
// 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) {
|
func TestFindHostInAllConfigs(t *testing.T) {
|
||||||
// Create temporary directory for test files
|
// Create temporary directory for test files
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user