Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<timestamp>-lazyssh.backup. The app keeps at most 10 of these backups, automatically removing the oldest ones.

## 📷 Screenshots

<div align="center">
Expand Down
26 changes: 26 additions & 0 deletions internal/adapters/data/ssh_config_file/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
5 changes: 5 additions & 0 deletions internal/adapters/data/ssh_config_file/config_io.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
9 changes: 5 additions & 4 deletions internal/adapters/data/ssh_config_file/crud.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down