diff --git a/README.md b/README.md index 3517768..3394aab 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,14 @@ It is simply a UI/TUI wrapper around your existing `~/.ssh/config` file. - File permissions on your SSH config are preserved to ensure security. +## 🛡️ Config Safety: Non‑destructive writes and backups + +- Non‑destructive edits: lazyssh only writes the minimal required changes to your ~/.ssh/config. It uses a parser that preserves existing comments, spacing, order, and any settings it didn’t touch. Your handcrafted comments and formatting remain intact. +- Atomic writes: updates are written to a temporary file and then atomically renamed over the original, minimizing the risk of partial writes. +- Backups: + - One‑time original backup: before lazyssh makes its first change, it creates a single snapshot named config.original.backup beside your SSH config. If this file is present, it will never be recreated or overwritten. + - Rolling backups: on every subsequent save, lazyssh also creates a timestamped backup named like: ~/.ssh/config--lazyssh.backup. The app keeps at most 10 of these backups, automatically removing the oldest ones. + ## 📷 Screenshots
diff --git a/internal/adapters/data/ssh_config_file/backup.go b/internal/adapters/data/ssh_config_file/backup.go index 20357e4..2f5f156 100644 --- a/internal/adapters/data/ssh_config_file/backup.go +++ b/internal/adapters/data/ssh_config_file/backup.go @@ -125,3 +125,29 @@ func (r *Repository) findBackupFiles(dir string) ([]os.FileInfo, error) { return backupFiles, nil } + +// createOriginalBackupIfNeeded creates a one-time original backup of the current SSH config. +func (r *Repository) createOriginalBackupIfNeeded() error { + // If no SSH config file, nothing to do. + if _, err := r.fileSystem.Stat(r.configPath); os.IsNotExist(err) { + return nil + } else if err != nil { + return fmt.Errorf("failed to check if config file exists: %w", err) + } + + configDir := filepath.Dir(r.configPath) + originalBackupPath := filepath.Join(configDir, OriginalBackupName) + + if _, err := r.fileSystem.Stat(originalBackupPath); err == nil { + return nil + } else if !r.fileSystem.IsNotExist(err) { + return fmt.Errorf("failed to check if original backup exists: %w", err) + } + + if err := r.copyFile(r.configPath, originalBackupPath); err != nil { + return fmt.Errorf("failed to create original backup: %w", err) + } + + r.logger.Infof("Created original backup: %s", originalBackupPath) + return nil +} diff --git a/internal/adapters/data/ssh_config_file/config_io.go b/internal/adapters/data/ssh_config_file/config_io.go index b4a820b..b5a5da1 100644 --- a/internal/adapters/data/ssh_config_file/config_io.go +++ b/internal/adapters/data/ssh_config_file/config_io.go @@ -66,6 +66,11 @@ func (r *Repository) saveConfig(cfg *ssh_config.Config) error { return fmt.Errorf("failed to write config to temporary file: %w", err) } + // Ensure a one-time original backup exists before any modifications managed by lazyssh. + if err := r.createOriginalBackupIfNeeded(); err != nil { + return fmt.Errorf("failed to create original backup: %w", err) + } + if err := r.createBackup(); err != nil { return fmt.Errorf("failed to create backup: %w", err) } diff --git a/internal/adapters/data/ssh_config_file/crud.go b/internal/adapters/data/ssh_config_file/crud.go index 7cea18c..f22c560 100644 --- a/internal/adapters/data/ssh_config_file/crud.go +++ b/internal/adapters/data/ssh_config_file/crud.go @@ -23,10 +23,11 @@ import ( ) const ( - MaxBackups = 10 - TempSuffix = ".tmp" - BackupSuffix = "lazyssh.backup" - SSHConfigPerms = 0o600 + MaxBackups = 10 + TempSuffix = ".tmp" + BackupSuffix = "lazyssh.backup" + SSHConfigPerms = 0o600 + OriginalBackupName = "config.original.backup" ) // filterServers filters servers based on the query string.