Skip to content

Commit a5e4898

Browse files
committed
feat(dotfile): adds dotfiles backup and integration
1 parent 2a42ae2 commit a5e4898

17 files changed

+1027
-879
lines changed

go.mod

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
module github.com/mdgspace/sysreplicate
2-
3-
go 1.24.3
4-
5-
require golang.org/x/term v0.32.0
6-
7-
require golang.org/x/sys v0.33.0 // indirect
1+
module github.com/mdgspace/sysreplicate
2+
3+
go 1.24.3
4+
5+
require golang.org/x/term v0.32.0
6+
7+
require golang.org/x/sys v0.33.0 // indirect

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
2-
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
3-
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
4-
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
1+
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
2+
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
3+
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
4+
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=

main.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
package main
2-
3-
import "github.com/mdgspace/sysreplicate/system"
4-
5-
// main is the entry point for the program.
6-
func main() {
7-
system.Run()
8-
}
1+
package main
2+
3+
import (
4+
"github.com/mdgspace/sysreplicate/system"
5+
)
6+
// main is the entry point for the program.
7+
func main() {
8+
system.Run()
9+
}

system/backup/dotfile_scanner.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package backup
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
)
8+
9+
var DotfilePaths = []string{
10+
"~/.bashrc",
11+
"~/.zshrc",
12+
"~/.vimrc",
13+
"~/.config",
14+
"~/.bash_history",
15+
"~/.zsh_history",
16+
"~/.gitconfig",
17+
"~/.profile",
18+
"~/.npmrc",
19+
}
20+
21+
type Dotfile struct {
22+
Path string
23+
RelPath string
24+
IsDir bool
25+
IsBinary bool
26+
Mode os.FileMode
27+
Content string // ignore for the binary files
28+
}
29+
30+
// expand ~ to home dir
31+
func expandHome(path string) string {
32+
if strings.HasPrefix(path, "~") {
33+
home, err := os.UserHomeDir()
34+
if err == nil {
35+
return filepath.Join(home, path[2:])
36+
}
37+
}
38+
return path
39+
}
40+
41+
// check for binary files
42+
func containsNullByte(data []byte) bool {
43+
for _, b := range data {
44+
if b == 0 {
45+
return true
46+
}
47+
}
48+
return false
49+
}
50+
51+
// ScanDotfiles scans all dotfiles and returns their metadata + content
52+
func ScanDotfiles() ([]Dotfile, error) {
53+
var results []Dotfile
54+
home, _ := os.UserHomeDir()
55+
56+
for _, raw := range DotfilePaths {
57+
full := expandHome(raw)
58+
59+
info, err := os.Stat(full)
60+
if err != nil {
61+
continue
62+
}
63+
64+
relPath, _ := filepath.Rel(home, full)
65+
entry := Dotfile{
66+
Path: full,
67+
RelPath: relPath,
68+
IsDir: info.IsDir(),
69+
Mode: info.Mode(),
70+
}
71+
72+
if !info.IsDir() {
73+
data, err := os.ReadFile(full)
74+
if err != nil {
75+
continue
76+
}
77+
if containsNullByte(data) {
78+
entry.IsBinary = true
79+
} else {
80+
entry.Content = string(data)
81+
}
82+
}
83+
84+
results = append(results, entry)
85+
}
86+
87+
return results, nil
88+
}

system/backup/dotfiles_backup.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package backup
2+
3+
import (
4+
"archive/tar"
5+
"compress/gzip"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"os"
10+
"time"
11+
)
12+
13+
type BackupMetadata struct {
14+
Timestamp time.Time `json:"timestamp"`
15+
Hostname string `json:"hostname"`
16+
Files []Dotfile `json:"files"`
17+
}
18+
19+
type DotfileBackupManager struct{}
20+
21+
func NewDotfileBackupManager() *DotfileBackupManager {
22+
return &DotfileBackupManager{}
23+
}
24+
25+
func (db *DotfileBackupManager) CreateDotfileBackup(outputTar string) error {
26+
files, err := ScanDotfiles()
27+
if err != nil {
28+
return fmt.Errorf("error scanning dotfiles: %w", err)
29+
}
30+
31+
hostname, _ := os.Hostname()
32+
33+
meta := BackupMetadata{
34+
Timestamp: time.Now(),
35+
Hostname: hostname,
36+
Files: files,
37+
}
38+
39+
tarFile, err := os.Create(outputTar)
40+
if err != nil {
41+
return fmt.Errorf("failed to create tar file: %w", err)
42+
}
43+
defer tarFile.Close()
44+
45+
gzipWriter := gzip.NewWriter(tarFile)
46+
defer gzipWriter.Close()
47+
tarWriter := tar.NewWriter(gzipWriter)
48+
defer tarWriter.Close()
49+
50+
// Write metadata JSON
51+
metaBytes, _ := json.MarshalIndent(meta, "", " ")
52+
tarWriter.WriteHeader(&tar.Header{
53+
Name: "backup.json",
54+
Mode: 0644,
55+
Size: int64(len(metaBytes)),
56+
})
57+
tarWriter.Write(metaBytes)
58+
59+
// Add dotfiles
60+
for _, f := range files {
61+
if f.IsDir {
62+
continue
63+
}
64+
file, err := os.Open(f.Path)
65+
if err != nil {
66+
continue
67+
}
68+
defer file.Close()
69+
70+
info, _ := file.Stat()
71+
hdr, _ := tar.FileInfoHeader(info, "")
72+
hdr.Name = f.RelPath
73+
tarWriter.WriteHeader(hdr)
74+
io.Copy(tarWriter, file)
75+
}
76+
77+
fmt.Println("Backup complete:", outputTar)
78+
return nil
79+
}

system/backup/encrypt.go

Lines changed: 56 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,56 @@
1-
package backup
2-
3-
import (
4-
"crypto/aes"
5-
"crypto/cipher"
6-
"crypto/rand"
7-
"encoding/base64"
8-
"fmt"
9-
"io"
10-
"os"
11-
)
12-
13-
//simplified encryption config without password
14-
type EncryptionConfig struct {
15-
Key []byte // Direct 32-byte key instead of password+salt
16-
}
17-
18-
//AES-GCM encryption with direct key (no password derivation)
19-
func EncryptFile(filePath string, config *EncryptionConfig) (string, error) {
20-
data, err := os.ReadFile(filePath)
21-
if err != nil {
22-
return "", fmt.Errorf("failed to read file %s: %w", filePath, err)
23-
}
24-
25-
//use direct key (no password derivation)
26-
block, err := aes.NewCipher(config.Key)
27-
if err != nil {
28-
return "", fmt.Errorf("failed to create cipher: %w", err)
29-
}
30-
31-
// GCM mode from AES block
32-
gcm, err := cipher.NewGCM(block)
33-
if err != nil {
34-
return "", fmt.Errorf("failed to create GCM: %w", err)
35-
}
36-
37-
// Generate nonce
38-
nonce := make([]byte, gcm.NonceSize())
39-
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
40-
return "", fmt.Errorf("failed to generate nonce: %w", err)
41-
}
42-
43-
//encrypt data with nonce
44-
ciphertext := gcm.Seal(nonce, nonce, data, nil)
45-
46-
//encode to base64
47-
encoded := base64.StdEncoding.EncodeToString(ciphertext)
48-
return encoded, nil
49-
}
50-
51-
//generate a random 32-byte key for AES-256
52-
func GenerateKey() ([]byte, error) {
53-
key := make([]byte, 32) // 32 bytes for AES-256
54-
_, err := rand.Read(key)
55-
return key, err
56-
}
1+
package backup
2+
3+
import (
4+
"crypto/aes"
5+
"crypto/cipher"
6+
"crypto/rand"
7+
"encoding/base64"
8+
"fmt"
9+
"io"
10+
"os"
11+
)
12+
13+
//simplified encryption config without password
14+
type EncryptionConfig struct {
15+
Key []byte // Direct 32-byte key instead of password+salt
16+
}
17+
18+
//AES-GCM encryption with direct key (no password derivation)
19+
func EncryptFile(filePath string, config *EncryptionConfig) (string, error) {
20+
data, err := os.ReadFile(filePath)
21+
if err != nil {
22+
return "", fmt.Errorf("failed to read file %s: %w", filePath, err)
23+
}
24+
25+
//use direct key (no password derivation)
26+
block, err := aes.NewCipher(config.Key)
27+
if err != nil {
28+
return "", fmt.Errorf("failed to create cipher: %w", err)
29+
}
30+
31+
// GCM mode from AES block
32+
gcm, err := cipher.NewGCM(block)
33+
if err != nil {
34+
return "", fmt.Errorf("failed to create GCM: %w", err)
35+
}
36+
37+
// Generate nonce
38+
nonce := make([]byte, gcm.NonceSize())
39+
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
40+
return "", fmt.Errorf("failed to generate nonce: %w", err)
41+
}
42+
43+
//encrypt data with nonce
44+
ciphertext := gcm.Seal(nonce, nonce, data, nil)
45+
46+
//encode to base64
47+
encoded := base64.StdEncoding.EncodeToString(ciphertext)
48+
return encoded, nil
49+
}
50+
51+
//generate a random 32-byte key for AES-256
52+
func GenerateKey() ([]byte, error) {
53+
key := make([]byte, 32) // 32 bytes for AES-256
54+
_, err := rand.Read(key)
55+
return key, err
56+
}

0 commit comments

Comments
 (0)