diff --git a/.gitignore b/.gitignore index 7773828..3d702ca 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -dist/ \ No newline at end of file +dist/ +testing/* diff --git a/go.mod b/go.mod index ac360f6..3806883 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,3 @@ module github.com/mdgspace/sysreplicate go 1.24.3 - -require golang.org/x/term v0.32.0 - -require golang.org/x/sys v0.33.0 // indirect diff --git a/go.sum b/go.sum index 0c9fbb7..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +0,0 @@ -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= diff --git a/main.go b/main.go index 6067699..102f1b3 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,8 @@ package main -import "github.com/mdgspace/sysreplicate/system" - +import ( + "github.com/mdgspace/sysreplicate/system" +) // main is the entry point for the program. func main() { system.Run() diff --git a/sysreplicate b/sysreplicate new file mode 100755 index 0000000..e3e54b1 Binary files /dev/null and b/sysreplicate differ diff --git a/system/backup/dotfile_scanner.go b/system/backup/dotfile_scanner.go new file mode 100644 index 0000000..88b3cbb --- /dev/null +++ b/system/backup/dotfile_scanner.go @@ -0,0 +1,88 @@ +package backup + +import ( + "os" + "path/filepath" + "strings" +) + +var DotfilePaths = []string{ + "~/.bashrc", + "~/.zshrc", + "~/.vimrc", + "~/.config", + "~/.bash_history", + "~/.zsh_history", + "~/.gitconfig", + "~/.profile", + "~/.npmrc", +} + +type Dotfile struct { + Path string + RealPath string + IsDir bool + IsBinary bool + Mode os.FileMode + Content string // ignore for the binary files +} + +// expand ~ to home dir +func expandHome(path string) string { + if strings.HasPrefix(path, "~") { + home, err := os.UserHomeDir() + if err == nil { + return filepath.Join(home, path[2:]) + } + } + return path +} + +// check for binary files +func containsNullByte(data []byte) bool { + for _, b := range data { + if b == 0 { + return true + } + } + return false +} + +// ScanDotfiles scans all dotfiles and returns their metadata + content +func ScanDotfiles() ([]Dotfile, error) { + var results []Dotfile + home, _ := os.UserHomeDir() + + for _, raw := range DotfilePaths { + full := expandHome(raw) + + info, err := os.Stat(full) + if err != nil { + continue + } + + realPath, _ := filepath.Rel(home, full) + entry := Dotfile{ + Path: full, + RealPath: realPath, + IsDir: info.IsDir(), + Mode: info.Mode(), + } + + if !info.IsDir() { + data, err := os.ReadFile(full) + if err != nil { + continue + } + if containsNullByte(data) { + entry.IsBinary = true + } else { + entry.Content = string(data) + } + } + + results = append(results, entry) + } + + return results, nil +} diff --git a/system/backup/dotfiles_backup.go b/system/backup/dotfiles_backup.go new file mode 100644 index 0000000..e69e21d --- /dev/null +++ b/system/backup/dotfiles_backup.go @@ -0,0 +1,59 @@ +package backup + +import ( + "fmt" + "os" + "time" + "github.com/mdgspace/sysreplicate/system/output" +) + +type BackupMetadata struct { + Timestamp time.Time `json:"timestamp"` + Hostname string `json:"hostname"` + Files []Dotfile `json:"files"` +} + +type DotfileBackupManager struct{} + +func NewDotfileBackupManager() *DotfileBackupManager { + return &DotfileBackupManager{} +} + +func (db *DotfileBackupManager) CreateDotfileBackup(outputTar string) error { + // Scan dotfiles + files, err := ScanDotfiles() + if err != nil { + return fmt.Errorf("error scanning dotfiles: %w", err) + } + + hostname, _ := os.Hostname() + + // Convert []Dotfile to []output.Dotfile + 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, + } + } + + + // Create backup metadata + // struct from output + meta := &output.BackupMetadata{ + Timestamp: time.Now(), + Hostname: hostname, + Files: outputFiles, + } + + if err := output.CreateDotfilesBackupTarball(meta, outputTar); err != nil { + return fmt.Errorf("failed to create backup tarball: %w", err) + } + + fmt.Println("Backup complete:", outputTar) + return nil +} diff --git a/system/backup/encrypt.go b/system/backup/encrypt.go index e69e14d..c86f322 100644 --- a/system/backup/encrypt.go +++ b/system/backup/encrypt.go @@ -1,13 +1,13 @@ package backup import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "encoding/base64" - "fmt" - "io" - "os" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "fmt" + "io" + "os" ) //simplified encryption config without password diff --git a/system/backup/key.go b/system/backup/key.go index edd576a..13aca89 100644 --- a/system/backup/key.go +++ b/system/backup/key.go @@ -7,192 +7,192 @@ import ( "path/filepath" "strings" "time" + "github.com/mdgspace/sysreplicate/system/output" ) -//backup and restore operations +// backup and restore operations type BackupManager struct { - config *EncryptionConfig + config *EncryptionConfig } func NewBackupManager() *BackupManager { - return &BackupManager{} + return &BackupManager{} } -//create a complete backup of keys (no password required) +// create a complete backup of keys (no password required) func (bm *BackupManager) CreateBackup(customPaths []string) error { - fmt.Println("Starting key backup process...") - - //generate random encryption key (no password needed) - key, err := GenerateKey() - if err != nil { - return fmt.Errorf("failed to generate encryption key: %w", err) - } - - bm.config = &EncryptionConfig{ - Key: key, - } - - // search standard locations - fmt.Println("searching standard key locations...") - standardLocations, err := searchStandardLocations() - if err != nil { - return fmt.Errorf("failed to search standard locations: %w", err) - } - - //add custom paths - customLocations := bm.processCustomPaths(customPaths) - - //combine all locations - allLocations := append(standardLocations, customLocations...) - if len(allLocations) == 0 { - fmt.Println("No key locations found to backup.") - return nil - } - - //create backup data - backupData := &output.BackupData{ - Timestamp: time.Now(), - SystemInfo: bm.getSystemInfo(), - EncryptedKeys: make(map[string]output.EncryptedKey), - EncryptionKey: key, // Store the key in backup data - } - - //encrypt and store keys - fmt.Println("Encrypting keys...") - for _, location := range allLocations { - err := bm.processLocation(location, backupData) - if err != nil { - fmt.Printf("Warning: Failed to process location %s: %v\n", location.Path, err) - continue - } - } - - //creating tarball for the backup storing - fmt.Println("Creating backup tarball...") - tarballPath := fmt.Sprintf("dist/key-backup-%s.tar.gz", - time.Now().Format("2006-01-02-15-04-05")) - err = output.CreateBackupTarball(backupData, tarballPath) - if err != nil { - return fmt.Errorf("failed to create tarball: %w", err) - } - - fmt.Printf("Backup completed successfully: %s\n", tarballPath) - fmt.Printf("Backed up %d key files\n", len(backupData.EncryptedKeys)) - return nil + fmt.Println("Starting key backup process...") + + //generate random encryption key (no password needed) + key, err := GenerateKey() + if err != nil { + return fmt.Errorf("failed to generate encryption key: %w", err) + } + + bm.config = &EncryptionConfig{ + Key: key, + } + + // search standard locations + fmt.Println("searching standard key locations...") + standardLocations, err := searchStandardLocations() + if err != nil { + return fmt.Errorf("failed to search standard locations: %w", err) + } + + //add custom paths + customLocations := bm.processCustomPaths(customPaths) + + //combine all locations + allLocations := append(standardLocations, customLocations...) + if len(allLocations) == 0 { + fmt.Println("No key locations found to backup.") + return nil + } + + //create backup data + backupData := &output.BackupData{ + Timestamp: time.Now(), + SystemInfo: bm.getSystemInfo(), + EncryptedKeys: make(map[string]output.EncryptedKey), + EncryptionKey: key, // Store the key in backup data + } + + //encrypt and store keys + fmt.Println("Encrypting keys...") + for _, location := range allLocations { + err := bm.processLocation(location, backupData) + if err != nil { + fmt.Printf("Warning: Failed to process location %s: %v\n", location.Path, err) + continue + } + } + + //creating tarball for the backup storing + fmt.Println("Creating backup tarball...") + tarballPath := fmt.Sprintf("dist/key-backup-%s.tar.gz", + time.Now().Format("2006-01-02-15-04-05")) + err = output.CreateBackupTarball(backupData, tarballPath) + if err != nil { + return fmt.Errorf("failed to create tarball: %w", err) + } + + fmt.Printf("Backup completed successfully: %s\n", tarballPath) + fmt.Printf("Backed up %d key files\n", len(backupData.EncryptedKeys)) + return nil } - // processLocation processes a single key location func (bm *BackupManager) processLocation(location KeyLocation, backupData *output.BackupData) error { - for _, filePath := range location.Files { - //get file info for permissions - fileInfo, err := os.Stat(filePath) - if err != nil { - continue - } - - // call encryption of the file - encryptedData, err := EncryptFile(filePath, bm.config) - if err != nil { - return fmt.Errorf("failed to encrypt %s: %w", filePath, err) - } - - // store encrypted key - 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 + for _, filePath := range location.Files { + //get file info for permissions + fileInfo, err := os.Stat(filePath) + if err != nil { + continue + } + + // call encryption of the file + encryptedData, err := EncryptFile(filePath, bm.config) + if err != nil { + return fmt.Errorf("failed to encrypt %s: %w", filePath, err) + } + + // store encrypted key + 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 } // processCustomPaths converts custom paths to KeyLocation objects func (bm *BackupManager) processCustomPaths(customPaths []string) []KeyLocation { - var locations []KeyLocation - for _, path := range customPaths { - if path == "" { - continue - } - - // Expand home directory - if strings.HasPrefix(path, "~/") { - + var locations []KeyLocation + for _, path := range customPaths { + if path == "" { + continue + } + + // Expand home directory + if strings.HasPrefix(path, "~/") { + homeDir, _ := os.UserHomeDir() path = filepath.Join(homeDir, path[2:]) } - fileInfo, err := os.Stat(path) - if err != nil { - fmt.Printf("Warning: Custom path %s does not exist\n", path) - continue - } - - if fileInfo.IsDir() { - // Either Process directory - files, err := discoverKeyFiles(path) - if err != nil { - fmt.Printf("Warning: Failed to scan directory %s: %v\n", path, err) - continue - } - - if len(files) > 0 { - locations = append(locations, KeyLocation{ - Path: path, - Type: "custom", - Files: files, - IsDirectory: true, - }) - } - } else { - // Or Process single file - locations = append(locations, KeyLocation{ - Path: path, - Type: "custom", - Files: []string{path}, - IsDirectory: false, - }) - } - } - return locations + fileInfo, err := os.Stat(path) + if err != nil { + fmt.Printf("Warning: Custom path %s does not exist\n", path) + continue + } + + if fileInfo.IsDir() { + // Either Process directory + files, err := discoverKeyFiles(path) + if err != nil { + fmt.Printf("Warning: Failed to scan directory %s: %v\n", path, err) + continue + } + + if len(files) > 0 { + locations = append(locations, KeyLocation{ + Path: path, + Type: "custom", + Files: files, + IsDirectory: true, + }) + } + } else { + // Or Process single file + locations = append(locations, KeyLocation{ + Path: path, + Type: "custom", + Files: []string{path}, + IsDirectory: false, + }) + } + } + return locations } // collect basic system information func (bm *BackupManager) getSystemInfo() output.SystemInfo { - hostname, _ := os.Hostname() - username := os.Getenv("USER") - if username == "" { - username = os.Getenv("USERNAME") - } - return output.SystemInfo{ - Hostname: hostname, - Username: username, - OS: "linux", - } + hostname, _ := os.Hostname() + username := os.Getenv("USER") + if username == "" { + username = os.Getenv("USERNAME") + } + return output.SystemInfo{ + Hostname: hostname, + Username: username, + OS: "linux", + } } // custom key path prompt to the userss func GetCustomPaths() []string { - var paths []string - scanner := bufio.NewScanner(os.Stdin) - fmt.Println("\nEnter additional key locations (one per line, empty line to finish):") - fmt.Println("Examples: ~/mykeys/, /opt/certificates/, ~/.config/app/keys") - fmt.Println("Note: .ssh and .gnupg are default scouting locations") - - for { - fmt.Print("Path: ") - if !scanner.Scan() { - break - } - - path := strings.TrimSpace(scanner.Text()) - if path == "" { - break - } - paths = append(paths, path) - } - return paths + var paths []string + scanner := bufio.NewScanner(os.Stdin) + fmt.Println("\nEnter additional key locations (one per line, empty line to finish):") + fmt.Println("Examples: ~/mykeys/, /opt/certificates/, ~/.config/app/keys") + fmt.Println("Note: .ssh and .gnupg are default scouting locations") + + for { + fmt.Print("Path: ") + if !scanner.Scan() { + break + } + + path := strings.TrimSpace(scanner.Text()) + if path == "" { + break + } + paths = append(paths, path) + } + return paths } diff --git a/system/backup_integration.go b/system/backup_integration.go index 7ef8b0a..c84ae5d 100644 --- a/system/backup_integration.go +++ b/system/backup_integration.go @@ -1,28 +1,66 @@ package system import ( - "fmt" - "log" - - "github.com/mdgspace/sysreplicate/system/backup" + "fmt" + "log" + "bufio" + "os" + "strings" + + "github.com/mdgspace/sysreplicate/system/backup" ) // handle backup integration func RunBackup() { - fmt.Println("=== Key Backup Process ===") - - //create backup manager - backupManager := backup.NewBackupManager() - - //get custom paths from user - customPaths := backup.GetCustomPaths() - - //create backup - err := backupManager.CreateBackup(customPaths) - if err != nil { - log.Printf("Backup failed: %v", err) - return - } + fmt.Println("=== Key Backup Process ===") + + //create backup manager + backupManager := backup.NewBackupManager() + + //get custom paths from user + customPaths := backup.GetCustomPaths() + + //create backup + err := backupManager.CreateBackup(customPaths) + if err != nil { + log.Printf("Backup failed: %v", err) + return + } + + fmt.Println("Key backup completed successfully!") +} + +func RunDotfileBackup() { + fmt.Println("=== SysReplicate: Distro Dotfile Backup ===") + + // Create a backup manager + manager := backup.NewDotfileBackupManager() + + // Output path + outputPath := "dist/dotfile-backup.tar.gz" + + // Ensure "dist" directory exists + if err := os.MkdirAll("dist", os.ModePerm); err != nil { + fmt.Printf("Failed to create output directory: %v\n", err) + return + } + + // Run the backup + err := manager.CreateDotfileBackup(outputPath) + if err != nil { + fmt.Printf("Backup failed: %v\n", err) + return + } + + 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) - fmt.Println("Key backup completed successfully!") + } diff --git a/system/output/json.go b/system/output/json.go index 303db67..0b791d9 100644 --- a/system/output/json.go +++ b/system/output/json.go @@ -5,28 +5,15 @@ import ( ) // BuildSystemJSON creates a well-structured JSON object for the system info and packages. -func BuildSystemJSON(osType, distro, baseDistro string, packages []string) ([]byte, error) { - type ArchPackages struct { - Official []string `json:"official_packages"` - AUR []string `json:"aur_packages"` - } +func BuildSystemJSON(osType, distro, baseDistro string, packages map[string][]string) ([]byte, error) { + type SystemInfo struct { - OS string `json:"os"` - Distro string `json:"distro"` - BaseDistro string `json:"base_distro"` - Packages interface{} `json:"packages"` - } - if baseDistro == "arch" { - official, aur := SplitArchPackages(packages) - archPkgs := ArchPackages{Official: official, AUR: aur} - info := SystemInfo{ - OS: osType, - Distro: distro, - BaseDistro: baseDistro, - Packages: archPkgs, - } - return json.MarshalIndent(info, "", " ") + OS string `json:"os"` + Distro string `json:"distro"` + BaseDistro string `json:"base_distro"` + Packages map[string][]string `json:"packages"` } + info := SystemInfo{ OS: osType, Distro: distro, @@ -34,4 +21,4 @@ func BuildSystemJSON(osType, distro, baseDistro string, packages []string) ([]by Packages: packages, } return json.MarshalIndent(info, "", " ") -} \ No newline at end of file +} diff --git a/system/output/script.go b/system/output/script.go index 4eb248a..d253ce6 100644 --- a/system/output/script.go +++ b/system/output/script.go @@ -5,103 +5,112 @@ import ( "os" ) -// splitArchPackages splits the combined package list into official and AUR packages for Arch-based distros. -func SplitArchPackages(packages []string) (official, aur []string) { - isAUR := false - for _, pkg := range packages { - if pkg == "YayPackages" { - isAUR = true - continue - } - if isAUR { - if pkg != "" { - aur = append(aur, pkg) - } - } else { - if pkg != "" { - official = append(official, pkg) - } - } - } - return -} - // generateInstallScript creates a shell script to install all packages for the given distro. // Returns an error if the script cannot be created or written. -func GenerateInstallScript(baseDistro string, packages []string, scriptPath string) error { +func GenerateInstallScript(baseDistro string, packages map[string][]string, scriptPath string) error { f, err := os.Create(scriptPath) if err != nil { return err } defer f.Close() - _, err = f.WriteString("#!/bin/bash\nset -e\necho 'Starting package installation...'\n") + _, err = fmt.Fprintln(f, "#!/bin/bash\nset -e\necho 'Starting package installation...'") if err != nil { return err } - var installCmd string + var officialInstallCmd string switch baseDistro { case "debian": - installCmd = "sudo apt-get install -y" + officialInstallCmd = "sudo apt-get install -y" case "arch": - installCmd = "sudo pacman -S --noconfirm" + officialInstallCmd = "sudo pacman -S --noconfirm" case "rhel", "fedora": - installCmd = "sudo dnf install -y" + officialInstallCmd = "sudo dnf install -y" case "void": - installCmd = "sudo xbps-install -y" + officialInstallCmd = "sudo xbps-install -y" default: - _, _ = f.WriteString("echo 'Unsupported distro for script generation.'\n") + _, _ = fmt.Fprintln(f, "echo 'Unsupported distro for script generation.'") return nil } - if baseDistro == "arch" { - official, aur := SplitArchPackages(packages) - _, err = f.WriteString("echo 'Installing official packages with pacman...'\n") - if err != nil { - return err - } - for _, pkg := range official { - if pkg == "" { - continue + for repo, pkgs := range packages { + switch repo { + case "official_packages": + _, err = fmt.Fprintf(f, "echo 'Installing packages with %s...'\n", officialInstallCmd) + if err != nil { + return err } - _, err = f.WriteString(fmt.Sprintf("%s %s || true\n", installCmd, pkg)) + for _, pkg := range pkgs { + if pkg == "" { + continue + } + _, err = fmt.Fprintf(f, "%s %s || true\n", officialInstallCmd, pkg) + if err != nil { + return err + } + } + case "yay_packages": + _, err = fmt.Fprintln(f, "if ! command -v yay >/dev/null; then\n echo 'yay not found, installing yay...'\n sudo pacman -S --noconfirm yay\nfi") if err != nil { return err } - } - _, err = f.WriteString("if ! command -v yay >/dev/null; then\n echo 'yay not found, installing yay...'\n sudo pacman -S --noconfirm yay\nfi\n") - if err != nil { - return err - } - _, err = f.WriteString("echo 'Installing AUR packages with yay...'\n") - if err != nil { - return err - } - for _, pkg := range aur { - if pkg == "" { - continue + _, err = fmt.Fprintln(f, "echo 'Installing AUR packages with yay...'") + if err != nil { + return err + } + for _, pkg := range pkgs { + if pkg == "" { + continue + } + _, err = fmt.Fprintf(f, "yay -S --noconfirm %s || true\n", pkg) + if err != nil { + return err + } } - _, err = f.WriteString(fmt.Sprintf("yay -S --noconfirm %s || true\n", pkg)) + + case "flatpak_packages": + _, err = fmt.Fprintf(f, "if ! command -v flatpak >/dev/null; then\n echo 'flatpak not found, installing flatpak...'\n %s flatpak\nfi\n", officialInstallCmd) if err != nil { return err } - } - return nil - } + _, err = fmt.Fprintln(f, "echo 'Installing Flatpak packages...'") + if err != nil { + return err + } + for _, pkg := range pkgs { + if pkg == "" { + continue + } + _, err = fmt.Fprintf(f, "sudo flatpak install --noninteractive %s || true\n", pkg) + if err != nil { + return err + } + } + + case "snap_packages": + _, err = fmt.Fprintf(f, "if ! command -v snap >/dev/null; then\n echo 'snap not found, installing snapd...'\n %s snapd\nsudo systemctl enable --now snapd.socket\nfi\n", officialInstallCmd) + // this limits it to systemctl, but need to replace this in future to support non systemd systems + if err != nil { + return err + } + _, err = fmt.Fprintln(f, "echo 'Installing Snap packages...'") + if err != nil { + return err + } + for _, pkg := range pkgs { + if pkg == "" { + continue + } + _, err = fmt.Fprintf(f, "sudo snap install %s || true\n", pkg) + if err != nil { + return err + } + } - _, err = f.WriteString(fmt.Sprintf("echo 'Installing packages with %s...'\n", installCmd)) - if err != nil { - return err - } - for _, pkg := range packages { - if pkg == "" { - continue - } - _, err = f.WriteString(fmt.Sprintf("%s %s || true\n", installCmd, pkg)) - if err != nil { - return err } } + return nil + } diff --git a/system/output/tarball.go b/system/output/tarball.go index 6f5382b..7683da2 100644 --- a/system/output/tarball.go +++ b/system/output/tarball.go @@ -4,13 +4,15 @@ import ( "archive/tar" "compress/gzip" "encoding/json" + "fmt" + "io" "os" "time" ) -//backupData structure for tarball creation +// backupData structure for tarball creation type BackupData struct { - Timestamp time.Time `json:"timestamp"` + Timestamp time.Time `json:"timestamp"` SystemInfo SystemInfo `json:"system_info"` EncryptedKeys map[string]EncryptedKey `json:"encrypted_keys"` EncryptionKey []byte `json:"encryption_key"` @@ -29,7 +31,21 @@ type EncryptedKey struct { Permissions uint32 `json:"permissions"` } -//create a compressed tarball with the backup data +type Dotfile struct { + Path string + RealPath string + IsDir bool + IsBinary bool + Mode os.FileMode + Content string // ignore for the binary files +} +type BackupMetadata struct { + Timestamp time.Time `json:"timestamp"` + Hostname string `json:"hostname"` + Files []Dotfile `json:"files"` +} + +// create a compressed tarball with the backup data func CreateBackupTarball(backupData *BackupData, tarballPath string) error { //create tarball file file, err := os.Create(tarballPath) @@ -69,3 +85,55 @@ func CreateBackupTarball(backupData *BackupData, tarballPath string) error { return nil } + +func CreateDotfilesBackupTarball(meta *BackupMetadata, tarballPath string) error { + // Create the tarball file + 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() + + jsonData, err := json.MarshalIndent(meta, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal metadata: %w", err) + } + + // Add metadata as backup.json + header := &tar.Header{ + Name: "backup.json", + Mode: 0644, + Size: int64(len(jsonData)), + } + if err := tarWriter.WriteHeader(header); err != nil { + return fmt.Errorf("failed to write header for metadata: %w", err) + } + if _, err := tarWriter.Write(jsonData); err != nil { + return fmt.Errorf("failed to write metadata to tar: %w", err) + } + + // Add dotfiles + for _, f := range meta.Files { + if f.IsDir { + continue + } + file, err := os.Open(f.Path) + if err != nil { + continue + } + defer file.Close() + + info, _ := file.Stat() + hdr, _ := tar.FileInfoHeader(info, "") + hdr.Name = f.RealPath + tarWriter.WriteHeader(hdr) + io.Copy(tarWriter, file) + } + + return nil +} diff --git a/system/run.go b/system/run.go index 4ae0e34..712b91d 100644 --- a/system/run.go +++ b/system/run.go @@ -1,12 +1,13 @@ package system import ( - "bufio" - "strings" + "bufio" "fmt" "log" "os" "runtime" + "strings" + "github.com/mdgspace/sysreplicate/system/output" "github.com/mdgspace/sysreplicate/system/utils" ) @@ -16,88 +17,91 @@ func Run() { osType := runtime.GOOS fmt.Println("Detected OS Type:", osType) - switch osType { - case "darwin": - fmt.Println("MacOS is not supported") - return - case "windows": - fmt.Println("Windows is not supported") - return - case "linux": - showMenu() ////main menu component - default: - fmt.Println("OS not supported") - } + switch osType { + case "darwin": + fmt.Println("MacOS is not supported") + return + case "windows": + fmt.Println("Windows is not supported") + return + case "linux": + showMenu() ////main menu component + default: + fmt.Println("OS not supported") + } } -//showMenu displays the main menu for Linux users -//MUST BE CHANGED IN THE FUTURE +// 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. Exit") - fmt.Print("Choose an option (1-3): ") - - if !scanner.Scan() { - break - } - - choice := strings.TrimSpace(scanner.Text()) - - switch choice { - case "1": - runPackageReplication() - case "2": - RunBackup() - case "3": - fmt.Println() //exit - return - default: - fmt.Println("Invalid choice. Please select 1, 2, or 3.") - } - } + 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): ") + + if !scanner.Scan() { + break + } + + choice := strings.TrimSpace(scanner.Text()) + + switch choice { + case "1": + runPackageReplication() + case "2": + RunBackup() + case "3": + RunDotfileBackup() + case "4": + fmt.Println() //exit + return + default: + fmt.Println("Invalid choice. Please select 1, 2, or 3.") + } + } } -//this handles the original package replication functionality +// this handles the original package replication functionality func runPackageReplication() { - distro, baseDistro := utils.DetectDistro() - if distro == "unknown" && baseDistro == "unknown" { - log.Println("Failed to fetch the details of your distro") - return - } - - fmt.Println("Distribution:", distro) - fmt.Println("Built On:", baseDistro) - - packages := utils.FetchPackages(baseDistro) - jsonObj, err := output.BuildSystemJSON("linux", distro, baseDistro, packages) - if err != nil { - log.Println("Error marshalling JSON:", err) - return - } - - if err := os.MkdirAll(outputSysDir, 0744); err != nil { - log.Println("Error creating sys output directory:", err) - return - } - - if err := os.WriteFile(jsonOutputPath, jsonObj, 0644); err != nil { - log.Println("Error writing JSON output:", err) - return - } - - if err := os.MkdirAll(outputScriptsDir, 0744); err != nil { - log.Println("Error creating scripts output directory:", err) - return - } - - if err := output.GenerateInstallScript(baseDistro, packages, scriptOutputPath); err != nil { - log.Println("Error generating install script:", err) - } else { - fmt.Println("Script generated successfully at:", scriptOutputPath) - } -} \ No newline at end of file + distro, baseDistro := utils.DetectDistro() + if distro == "unknown" && baseDistro == "unknown" { + log.Println("Failed to fetch the details of your distro") + return + } + + fmt.Println("Distribution:", distro) + fmt.Println("Built On:", baseDistro) + + packages := utils.FetchPackages(baseDistro) + jsonObj, err := output.BuildSystemJSON("linux", distro, baseDistro, packages) + if err != nil { + log.Println("Error marshalling JSON:", err) + return + } + + if err := os.MkdirAll(outputSysDir, 0744); err != nil { + log.Println("Error creating sys output directory:", err) + return + } + + if err := os.WriteFile(jsonOutputPath, jsonObj, 0644); err != nil { + log.Println("Error writing JSON output:", err) + return + } + + if err := os.MkdirAll(outputScriptsDir, 0744); err != nil { + log.Println("Error creating scripts output directory:", err) + return + } + + if err := output.GenerateInstallScript(baseDistro, packages, scriptOutputPath); err != nil { + log.Println("Error generating install script:", err) + } else { + fmt.Println("Script generated successfully at:", scriptOutputPath) + } +} diff --git a/system/utils/fetch_packages.go b/system/utils/fetch_packages.go index 5ba824e..aac118b 100644 --- a/system/utils/fetch_packages.go +++ b/system/utils/fetch_packages.go @@ -7,43 +7,99 @@ import ( ) // FetchPackages returns a list of installed packages for the given base distro. -func FetchPackages(baseDistro string) []string { - var cmd *exec.Cmd - var cmdYay *exec.Cmd +func FetchPackages(baseDistro string) map[string][]string { + cmds := make(map[string]*exec.Cmd) + switch baseDistro { case "debian": - cmd = exec.Command("dpkg", "--get-selections") + // cmds["official_packages"] = exec.Command("dpkg", "--get-selections") + cmds["official_packages"] = exec.Command("sh", "-c", `dpkg-query -W -f='${Package}\n' | sort > /tmp/all.txt +apt-mark showmanual | sort > /tmp/manual.txt +comm -12 /tmp/all.txt /tmp/manual.txt | xargs -r dpkg-query -W -f='${Package}=${Version}\n' +rm /tmp/all.txt /tmp/manual.txt +`) + _, err := exec.LookPath("flatpak") + if err == nil { + cmds["flatpak_packages"] = exec.Command("flatpak", "list", "--app", "--columns=origin,application") + } else { + cmds["flatpak_packages"] = exec.Command("true") + } + + _, err = exec.LookPath("snap") + if err == nil { + cmds["snap_packages"] = exec.Command("sh", "-c", "snap list | awk 'NR>1 {print $1}'") + } else { + cmds["snap_packages"] = exec.Command("true") + } + case "arch": - cmd = exec.Command("pacman", "-Qn") - cmdYay = exec.Command("pacman", "-Qm") + // cmds["official_packages"] = exec.Command("pacman", "-Qen") + // cmds["yay_packages"] = exec.Command("pacman", "-Qem") + cmds["official_packages"] = exec.Command("sh", "-c", `pacman -Qen | cut -d' ' -f1`) + cmds["yay_packages"] = exec.Command("sh", "-c", `pacman -Qem | cut -d' ' -f1`) + _, err := exec.LookPath("flatpak") + if err == nil { + cmds["flatpak_packages"] = exec.Command("flatpak", "list", "--app", "--columns=origin,application") + } else { + cmds["flatpak_packages"] = exec.Command("true") + } + + _, err = exec.LookPath("snap") + if err == nil { + cmds["snap_packages"] = exec.Command("sh", "-c", "snap list | awk 'NR>1 {print $1}'") + } else { + cmds["snap_packages"] = exec.Command("true") + } + case "rhel", "fedora": - cmd = exec.Command("rpm", "-qa") + cmds["official_packages"] = exec.Command("rpm", "-qa") // need to change this later + _, err := exec.LookPath("flatpak") + if err == nil { + cmds["flatpak_packages"] = exec.Command("flatpak", "list", "--app", "--columns=origin,application") + } else { + cmds["flatpak_packages"] = exec.Command("true") + } + + _, err = exec.LookPath("snap") + if err == nil { + cmds["snap_packages"] = exec.Command("sh", "-c", "snap list | awk 'NR>1 {print $1}'") + } else { + cmds["snap_packages"] = exec.Command("true") + } + case "void": - cmd = exec.Command("xbps-query", "-l") + cmds["official_packages"] = exec.Command("xbps-query", "-l") // need to change this later + _, err := exec.LookPath("flatpak") + if err == nil { + cmds["flatpak_packages"] = exec.Command("flatpak", "list", "--app", "--columns=origin,application") + } else { + cmds["flatpak_packages"] = exec.Command("true") + } + + _, err = exec.LookPath("snap") + if err == nil { + cmds["snap_packages"] = exec.Command("sh", "-c", "snap list | awk 'NR>1 {print $1}'") + } else { + cmds["snap_packages"] = exec.Command("true") + } + default: log.Println("Your distro is unsupported, cannot identify package manager!") - return []string{"unknown"} + return map[string][]string{ + "error": {"unsupported distro"}, + } } - if baseDistro != "arch" { - output, err := cmd.CombinedOutput() + packageMap := make(map[string][]string) + + for key, value := range cmds { + output, err := value.CombinedOutput() if err != nil { - log.Println("Error in retrieving packages:", err) + log.Println("Error in retrieving ", key, ": ", err) + continue } - return strings.Split(strings.TrimSpace(string(output)), "\n") + packageMap[key] = strings.Split(strings.TrimSpace((string(output))), "\n") } + return packageMap - outputPacman, err := cmd.CombinedOutput() - outPutYay, errYay := cmdYay.CombinedOutput() - if err != nil { - log.Println("Error in retrieving Pacman packages:", err) - } - if errYay != nil { - log.Println("Error in retrieving Yay packages:", errYay) - } - pacmanPackages := strings.Split(strings.TrimSpace(string(outputPacman)), "\n") - yayPackages := strings.Split(strings.TrimSpace(string(outPutYay)), "\n") - // Mark the split between official and AUR packages - yayPackages = append([]string{"YayPackages"}, yayPackages...) - return append(pacmanPackages, yayPackages...) -} \ No newline at end of file +} diff --git a/system/utils/verify_path.go b/system/utils/verify_path.go new file mode 100644 index 0000000..1e9b3d2 --- /dev/null +++ b/system/utils/verify_path.go @@ -0,0 +1,12 @@ +package utils + +// import ( +// "strings" +// ) + +// func verifyFilePath(path string) int { +// if strings.Contains(path, " ") && !strings.HasPrefix(path, "\"") { +// return 0 +// } + +// }