From 98c1c593cf130b8b90ae922d6b8f55bbdb264180 Mon Sep 17 00:00:00 2001 From: Adem Baccara <71262172+Adembc@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:21:04 +0100 Subject: [PATCH 1/2] feat: backup the original file before manage it by lazyssh --- README.md | 8 ++++++ .../adapters/data/ssh_config_file/backup.go | 26 +++++++++++++++++++ .../data/ssh_config_file/config_io.go | 5 ++++ 3 files changed, 39 insertions(+) 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..3e6b71a 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, "config.original.backup") + + 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) } From b7371d4337d2f478aa00b3cc5571b33c027d3c83 Mon Sep 17 00:00:00 2001 From: Adem Baccara <71262172+Adembc@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:23:40 +0100 Subject: [PATCH 2/2] make the original file name in const --- internal/adapters/data/ssh_config_file/backup.go | 2 +- internal/adapters/data/ssh_config_file/crud.go | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/adapters/data/ssh_config_file/backup.go b/internal/adapters/data/ssh_config_file/backup.go index 3e6b71a..2f5f156 100644 --- a/internal/adapters/data/ssh_config_file/backup.go +++ b/internal/adapters/data/ssh_config_file/backup.go @@ -136,7 +136,7 @@ func (r *Repository) createOriginalBackupIfNeeded() error { } configDir := filepath.Dir(r.configPath) - originalBackupPath := filepath.Join(configDir, "config.original.backup") + originalBackupPath := filepath.Join(configDir, OriginalBackupName) if _, err := r.fileSystem.Stat(originalBackupPath); err == nil { return nil 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.