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/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_integration.go b/system/backup_integration.go index 7ef8b0a..817c615 100644 --- a/system/backup_integration.go +++ b/system/backup_integration.go @@ -1,28 +1,53 @@ package system import ( - "fmt" - "log" - - "github.com/mdgspace/sysreplicate/system/backup" + "fmt" + "log" + "os" + "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 completed successfully!") + 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!") } diff --git a/system/output/json.go b/system/output/json.go index 303db67..e4435e6 100644 --- a/system/output/json.go +++ b/system/output/json.go @@ -34,4 +34,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/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) + } +}