diff --git a/system/backup/restore.go b/system/backup/restore.go new file mode 100644 index 0000000..b70a5c2 --- /dev/null +++ b/system/backup/restore.go @@ -0,0 +1,274 @@ +package backup + +import ( + "archive/tar" + "compress/gzip" + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/mdgspace/sysreplicate/system/output" +) + +type RestoreManager struct { + backupData *UnifiedBackupData +} + +func NewRestoreManager() *RestoreManager { + return &RestoreManager{} +} + +// restoring the complete system from a unified backup +func (rm *RestoreManager) RestoreFromBackup(tarballPath string) error { + fmt.Printf("Starting system restore from: %s\n", tarballPath) + + // extract and parse backup data + err := rm.extractBackupData(tarballPath) + if err != nil { + return fmt.Errorf("failed to extract backup data: %w", err) + } + + // backup information and furthur instructions + ///s + fmt.Printf("Backup created on: %s\n", rm.backupData.Timestamp.Format("2006-01-09 15:04:05")) + fmt.Printf("Original system: %s@%s (%s)\n", + rm.backupData.SystemInfo.Username, + rm.backupData.SystemInfo.Hostname, + rm.backupData.Distro) + + // 1. Restore SSH/GPG keys + fmt.Println("Restoring SSH/GPG keys...") + err = rm.restoreKeys() + if err != nil { + fmt.Printf("Warning: Key restoration failed: %v\n", err) + } + + // 2. Restore dotfiles + fmt.Println("Restoring dotfiles...") + err = rm.restoreDotfiles(tarballPath) + if err != nil { + fmt.Printf("Warning: Dotfile restoration failed: %v\n", err) + } + + // 3. Generate package installation script + fmt.Println("Generating package installation script...") + err = rm.generateInstallScript() + if err != nil { + fmt.Printf("Warning: Package script generation failed: %v\n", err) + } + + fmt.Println("System restore completed successfully!") + fmt.Println("Note: Run the generated install script to restore packages:") + fmt.Println(" chmod +x dist/restored_packages_install.sh") + fmt.Println(" ./dist/restored_packages_install.sh") + + return nil +} + +// extractBackupData extracts and parses the main backup JSON from tarball +func (rm *RestoreManager) extractBackupData(tarballPath string) error { + file, err := os.Open(tarballPath) + if err != nil { + return fmt.Errorf("failed to open tarball: %w", err) + } + defer file.Close() + + gzipReader, err := gzip.NewReader(file) + if err != nil { + return fmt.Errorf("failed to create gzip reader: %w", err) + } + defer gzipReader.Close() + + tarReader := tar.NewReader(gzipReader) + + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("failed to read tar entry: %w", err) + } + + if header.Name == "unified_backup.json" { + data, err := io.ReadAll(tarReader) + if err != nil { + return fmt.Errorf("failed to read backup data: %w", err) + } + + rm.backupData = &UnifiedBackupData{} + err = json.Unmarshal(data, rm.backupData) + if err != nil { + return fmt.Errorf("failed to parse backup data: %w", err) + } + + return nil + } + } + + return fmt.Errorf("backup data not found in tarball") +} + +// decryptiug and restoring SSH/GPG keys to their original locations +func (rm *RestoreManager) restoreKeys() error { + config := &EncryptionConfig{ + Key: rm.backupData.EncryptionKey, + } + + restoredCount := 0 + for keyID, encKey := range rm.backupData.EncryptedKeys { + fmt.Printf("Restoring key: %s -> %s\n", keyID, encKey.OriginalPath) + + // Decrypt the key data + decryptedData, err := rm.decryptData(encKey.EncryptedData, config) + if err != nil { + fmt.Printf("Warning: Failed to decrypt key %s: %v\n", keyID, err) + continue + } + + //// Ensure directory exists + dir := filepath.Dir(encKey.OriginalPath) + if err := os.MkdirAll(dir, 0755); err != nil { + fmt.Printf("Warning: Failed to create directory %s: %v\n", dir, err) + continue + } + + // Write decrypted data to original location + err = os.WriteFile(encKey.OriginalPath, decryptedData, os.FileMode(encKey.Permissions)) + if err != nil { + fmt.Printf("Warning: Failed to write key to %s: %v\n", encKey.OriginalPath, err) + continue + } + + restoredCount++ + } + + fmt.Printf("Successfully restored %d keys\n", restoredCount) + return nil +} + +// extract from tarbell +func (rm *RestoreManager) restoreDotfiles(tarballPath string) error { + file, err := os.Open(tarballPath) + if err != nil { + return fmt.Errorf("failed to open tarball: %w", err) + } + defer file.Close() + + gzipReader, err := gzip.NewReader(file) + if err != nil { + return fmt.Errorf("failed to create gzip reader: %w", err) + } + defer gzipReader.Close() + + tarReader := tar.NewReader(gzipReader) + + homeDir, _ := os.UserHomeDir() + restoredCount := 0 + + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("failed to read tar entry: %w", err) + } + + // Process dotfiles + if strings.HasPrefix(header.Name, "dotfiles/") { + relativePath := strings.TrimPrefix(header.Name, "dotfiles/") + targetPath := filepath.Join(homeDir, relativePath) + + fmt.Printf("Restoring dotfile: %s -> %s\n", header.Name, targetPath) + + // Ensure directory exists + dir := filepath.Dir(targetPath) + if err := os.MkdirAll(dir, 0755); err != nil { + fmt.Printf("Warning: Failed to create directory %s: %v\n", dir, err) + continue + } + + // creates thje target file and copies content to it + targetFile, err := os.Create(targetPath) + if err != nil { + fmt.Printf("Warning: Failed to create file %s: %v\n", targetPath, err) + continue + } + _, err = io.Copy(targetFile, tarReader) + targetFile.Close() + + if err != nil { + fmt.Printf("Warning: Failed to copy dotfile content: %v\n", err) + continue + } + + // Set permissions + err = os.Chmod(targetPath, header.FileInfo().Mode()) + if err != nil { + fmt.Printf("Warning: Failed to set permissions for %s: %v\n", targetPath, err) + } + + restoredCount++ + } + } + + fmt.Printf("Successfully restored %d dotfiles\n", restoredCount) + return nil +} + +// generateInstallScript creates a script to reinstall packages +func (rm *RestoreManager) generateInstallScript() error { + scriptPath := "dist/restored_packages_install.sh" + + // dir check + if err := os.MkdirAll(filepath.Dir(scriptPath), 0755); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + return output.GenerateInstallScript(rm.backupData.BaseDistro, rm.backupData.Packages, scriptPath) +} + +// decryptData decrypts base64 encoded data using AES-GCM +func (rm *RestoreManager) decryptData(encryptedBase64 string, config *EncryptionConfig) ([]byte, error) { + // Decode base64 + ciphertext, err := base64.StdEncoding.DecodeString(encryptedBase64) + if err != nil { + return nil, fmt.Errorf("failed to decode base64: %w", err) + } + + // creating cipher + block, err := aes.NewCipher(config.Key) + if err != nil { + return nil, fmt.Errorf("failed to create cipher: %w", err) + } + + // creating GCM + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("failed to create GCM: %w", err) + } + + // Extract nonce and encrypted data + nonceSize := gcm.NonceSize() + if len(ciphertext) < nonceSize { + return nil, fmt.Errorf("ciphertext too short") + } + + nonce := ciphertext[:nonceSize] + encryptedData := ciphertext[nonceSize:] + + // Decrypt + plaintext, err := gcm.Open(nil, nonce, encryptedData, nil) + if err != nil { + return nil, fmt.Errorf("failed to decrypt: %w", err) + } + + return plaintext, nil +} diff --git a/system/backup/unified_backup.go b/system/backup/unified_backup.go new file mode 100644 index 0000000..150448b --- /dev/null +++ b/system/backup/unified_backup.go @@ -0,0 +1,255 @@ +package backup + +import ( + "archive/tar" + "compress/gzip" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/mdgspace/sysreplicate/system/output" + "github.com/mdgspace/sysreplicate/system/utils" +) + +// all backup information in one structure +type UnifiedBackupData struct { + Timestamp time.Time `json:"timestamp"` + SystemInfo output.SystemInfo `json:"system_info"` + EncryptedKeys map[string]output.EncryptedKey `json:"encrypted_keys"` + Dotfiles []output.Dotfile `json:"dotfiles"` + Packages map[string][]string `json:"packages"` + EncryptionKey []byte `json:"encryption_key"` + Distro string `json:"distro"` + BaseDistro string `json:"base_distro"` +} + +// complete system backup +type UnifiedBackupManager struct { + config *EncryptionConfig +} + +func NewUnifiedBackupManager() *UnifiedBackupManager { + return &UnifiedBackupManager{} +} + +// complete system backup including keys, dotfiles, and packages +func (ubm *UnifiedBackupManager) CreateUnifiedBackup(customPaths []string) error { + fmt.Println("Starting unified system backup...") + + // Generate encryption key + key, err := GenerateKey() + if err != nil { + return fmt.Errorf("failed to generate encryption key: %w", err) + } + + ubm.config = &EncryptionConfig{ + Key: key, + } + + // Get system information + hostname, _ := os.Hostname() + username := os.Getenv("USER") + if username == "" { + username = os.Getenv("USERNAME") + } + + // Detect distro and get packages + distro, baseDistro := utils.DetectDistro() + packages := utils.FetchPackages(baseDistro) + + // Create unified backup data + backupData := &UnifiedBackupData{ + Timestamp: time.Now(), + SystemInfo: output.SystemInfo{ + Hostname: hostname, + Username: username, + OS: "linux", + }, + EncryptedKeys: make(map[string]output.EncryptedKey), + EncryptionKey: key, + Packages: packages, + Distro: distro, + BaseDistro: baseDistro, + } + + // 1. Backup SSH/GPG keys + fmt.Println("Backing up SSH/GPG keys...") + err = ubm.backupKeys(customPaths, backupData) + if err != nil { + fmt.Printf("Warning: Key backup failed: %v\n", err) + } + + // 2. Backup dotfiles + fmt.Println("Backing up dotfiles...") + err = ubm.backupDotfiles(backupData) + if err != nil { + fmt.Printf("Warning: Dotfile backup failed: %v\n", err) + } + + // 3. Create unified tarball + fmt.Println("Creating unified backup tarball...") + tarballPath := fmt.Sprintf("dist/unified-backup-%s.tar.gz", + time.Now().Format("2006-01-02-15-04-05")) + + err = ubm.createUnifiedTarball(backupData, tarballPath) + if err != nil { + return fmt.Errorf("failed to create unified tarball: %w", err) + } + + fmt.Printf("Unified backup completed successfully: %s\n", tarballPath) + fmt.Printf("Backed up %d key files, %d dotfiles, and %d package categories\n", + len(backupData.EncryptedKeys), len(backupData.Dotfiles), len(backupData.Packages)) + + return nil +} + +// SSH/GPG key backup +func (ubm *UnifiedBackupManager) backupKeys(customPaths []string, backupData *UnifiedBackupData) error { + // Search standard locations + standardLocations, err := searchStandardLocations() + if err != nil { + return fmt.Errorf("failed to search standard locations: %w", err) + } + + // process the custom paths user might have given while backup + bm := &BackupManager{} + customLocations := bm.processCustomPaths(customPaths) + + // Combine all locations + allLocations := append(standardLocations, customLocations...) + + // encrypt and store keys + for _, location := range allLocations { + for _, filePath := range location.Files { + fileInfo, err := os.Stat(filePath) + if err != nil { + continue + } + + encryptedData, err := EncryptFile(filePath, ubm.config) + if err != nil { + fmt.Printf("Warning: Failed to encrypt %s: %v\n", filePath, err) + continue + } + + keyID := filepath.Base(filePath) + "_" + strings.ReplaceAll(filePath, "/", "_") + backupData.EncryptedKeys[keyID] = output.EncryptedKey{ + OriginalPath: filePath, + KeyType: location.Type, + EncryptedData: encryptedData, + Permissions: uint32(fileInfo.Mode()), + } + } + } + + return nil +} + +// dotfile backup logic +func (ubm *UnifiedBackupManager) backupDotfiles(backupData *UnifiedBackupData) error { + files, err := ScanDotfiles() + if err != nil { + return fmt.Errorf("error scanning dotfiles: %w", err) + } + + // Convert to output format + outputFiles := make([]output.Dotfile, len(files)) + for i, file := range files { + outputFiles[i] = output.Dotfile{ + Path: file.Path, + RealPath: file.RealPath, + IsDir: file.IsDir, + IsBinary: file.IsBinary, + Mode: file.Mode, + Content: file.Content, + } + } + + backupData.Dotfiles = outputFiles + return nil +} + +// creating one single tarball containing all backup data +func (ubm *UnifiedBackupManager) createUnifiedTarball(backupData *UnifiedBackupData, tarballPath string) error { + // Ensure directory exists + if err := os.MkdirAll(filepath.Dir(tarballPath), 0755); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + file, err := os.Create(tarballPath) + if err != nil { + return fmt.Errorf("failed to create tarball: %w", err) + } + defer file.Close() + + gzipWriter := gzip.NewWriter(file) + defer gzipWriter.Close() + + tarWriter := tar.NewWriter(gzipWriter) + defer tarWriter.Close() + + //// Add main backup metadata + jsonData, err := json.MarshalIndent(backupData, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal backup data: %w", err) + } + + header := &tar.Header{ + Name: "unified_backup.json", + Mode: 0644, + Size: int64(len(jsonData)), + } + + if err := tarWriter.WriteHeader(header); err != nil { + return fmt.Errorf("failed to write header: %w", err) + } + + if _, err := tarWriter.Write(jsonData); err != nil { + return fmt.Errorf("failed to write backup data: %w", err) + } + + // Add dotfiles as separate entries (non-binary only) + for _, dotfile := range backupData.Dotfiles { + if dotfile.IsDir || dotfile.IsBinary { + continue + } + + file, err := os.Open(dotfile.Path) + if err != nil { + fmt.Printf("Warning: Could not open dotfile %s: %v\n", dotfile.Path, err) + continue + } + + info, err := file.Stat() + if err != nil { + file.Close() + continue + } + + hdr, err := tar.FileInfoHeader(info, "") + if err != nil { + file.Close() + continue + } + + hdr.Name = "dotfiles/" + dotfile.RealPath + + if err := tarWriter.WriteHeader(hdr); err != nil { + file.Close() + continue + } + + _, err = io.Copy(tarWriter, file) + file.Close() + + if err != nil { + fmt.Printf("Warning: Failed to add dotfile %s to tarball: %v\n", dotfile.Path, err) + } + } + + return nil +} diff --git a/system/backup_integration.go b/system/backup_integration.go index c84ae5d..b25aad6 100644 --- a/system/backup_integration.go +++ b/system/backup_integration.go @@ -1,9 +1,9 @@ package system import ( + "bufio" "fmt" "log" - "bufio" "os" "strings" @@ -11,6 +11,99 @@ import ( ) // handle backup integration +func RunUnifiedBackup() { + fmt.Println("=== Unified System Backup (Keys + Dotfiles + Packages) ===") + + // unified backup manager + ubm := backup.NewUnifiedBackupManager() + + // gert all the custom key paths from user + fmt.Println("\nOptional: Add custom key locations") + customPaths := backup.GetCustomPaths() + + // Create unified backup + err := ubm.CreateUnifiedBackup(customPaths) + if err != nil { + log.Printf("Unified backup failed: %v", err) + return + } + + fmt.Println("Complete system backup completed successfully!") + fmt.Println("Your backup includes:") + fmt.Println("- SSH/GPG keys (encrypted)") + fmt.Println("- Dotfiles (.bashrc, .vimrc, .gitconfig, etc.)") + fmt.Println("- Package lists for reinstallation") +} + +// system restoration from backup +func RunRestore() { + fmt.Println("=== System Restore from Backup ===") + + scanner := bufio.NewScanner(os.Stdin) + fmt.Print("Enter backup tarball path: ") + + if !scanner.Scan() { + fmt.Println("Failed to read input") + return + } + + tarballPath := strings.TrimSpace(scanner.Text()) + if tarballPath == "" { + fmt.Println("No tarball path provided") + return + } + + // Normalize path separators (handle both Windows and Unix paths) + normalizedPath := strings.ReplaceAll(tarballPath, "\\", "/") + + // Check if file exists and is a file (not directory) + fileInfo, err := os.Stat(normalizedPath) + if os.IsNotExist(err) { + fmt.Printf("Backup file does not exist: %s\n", normalizedPath) + return + } + if err != nil { + fmt.Printf("Error checking backup file: %v\n", err) + return + } + if fileInfo.IsDir() { + fmt.Printf("Path is a directory, not a file: %s\n", normalizedPath) + return + } + + // Use normalized path for restoration + tarballPath = normalizedPath + + // Confirm restoration + fmt.Printf("This will restore your system from: %s\n", tarballPath) + fmt.Print("WARNING: This will overwrite existing files. Continue? (y/N): ") + + if !scanner.Scan() { + return + } + + confirm := strings.ToLower(strings.TrimSpace(scanner.Text())) + if confirm != "y" && confirm != "yes" { + fmt.Println("Restoration cancelled") + return + } + + // creating previously defined restore manager and run restoration + rm := backup.NewRestoreManager() + err = rm.RestoreFromBackup(tarballPath) + if err != nil { + log.Printf("Restoration failed: %v", err) + return + } + + fmt.Println("\nSystem restoration completed!") + fmt.Println("Next steps:") + fmt.Println("1. Run the generated package installation script") + fmt.Println("2. Restart your shell or run 'source ~/.bashrc' (or ~/.zshrc)") + fmt.Println("3. Check that your SSH keys work: 'ssh-add -l'") +} + +// rest of the options func RunBackup() { fmt.Println("=== Key Backup Process ===") @@ -54,13 +147,3 @@ func RunDotfileBackup() { fmt.Println("Backup complete!") } -func restoreBackup() { - fmt.Println("Restoring Backup") - fmt.Println("Enter backup tarball path") - - reader := bufio.NewReader(os.Stdin) - name, _ := reader.ReadString('\n') // reads until newline - name = strings.TrimSpace(name) - - -} diff --git a/system/output/tarball.go b/system/output/tarball.go index 7683da2..e1b929c 100644 --- a/system/output/tarball.go +++ b/system/output/tarball.go @@ -32,13 +32,14 @@ type EncryptedKey struct { } type Dotfile struct { - Path string - RealPath string - IsDir bool - IsBinary bool - Mode os.FileMode - Content string // ignore for the binary files + Path string `json:"path"` + RealPath string `json:"real_path"` + IsDir bool `json:"is_dir"` + IsBinary bool `json:"is_binary"` + Mode os.FileMode `json:"mode"` + Content string `json:"content"` // ignore for the binary files } + type BackupMetadata struct { Timestamp time.Time `json:"timestamp"` Hostname string `json:"hostname"` diff --git a/system/run.go b/system/run.go index 712b91d..5f62ac9 100644 --- a/system/run.go +++ b/system/run.go @@ -32,17 +32,18 @@ func Run() { } // showMenu displays the main menu for Linux users -// MUST BE CHANGED IN THE FUTURE func showMenu() { scanner := bufio.NewScanner(os.Stdin) for { fmt.Println("\n=== SysReplicate - Distro Hopping Tool ===") - fmt.Println("1. Generate package replication files") - fmt.Println("2. Backup SSH/GPG keys") - fmt.Println("3. Backup dotfiles") - fmt.Println("4. Exit") - fmt.Print("Choose an option (1-4): ") + fmt.Println("1. Create Complete System Backup (Recommended)") + fmt.Println("2. Restore System from Backup") + fmt.Println("3. Generate package replication files only") + fmt.Println("4. Backup SSH/GPG keys only") + fmt.Println("5. Backup dotfiles only") + fmt.Println("6. Exit") + fmt.Print("Choose an option (1-6): ") if !scanner.Scan() { break @@ -52,16 +53,20 @@ func showMenu() { switch choice { case "1": - runPackageReplication() + RunUnifiedBackup() case "2": - RunBackup() + RunRestore() case "3": - RunDotfileBackup() + runPackageReplication() case "4": - fmt.Println() //exit + RunBackup() + case "5": + RunDotfileBackup() + case "6": + fmt.Println("Goodbye Captain!") return default: - fmt.Println("Invalid choice. Please select 1, 2, or 3.") + fmt.Println("Invalid choice. Please select 1-6.") } } }