Borg/pkg/sync/sync.go
google-labs-jules[bot] 99c635d8df feat: Add diff and sync collection functionality
Implement the core logic for comparing two archives (diff) and performing incremental updates (sync).

- Introduces a new `borg diff` command to show differences between two collection archives.
- Adds new `pkg/diff` and `pkg/sync` packages with corresponding business logic and unit tests.
- The `diff` command supports reading compressed archives and prints a formatted summary of added, removed, and modified files.
- The `sync` package includes `append`, `mirror`, and `update` strategies.

Next steps involve integrating the sync logic into the `collect` commands.

Co-authored-by: Snider <631881+Snider@users.noreply.github.com>
2026-02-02 00:49:02 +00:00

93 lines
2.3 KiB
Go

package sync
import (
"bytes"
"fmt"
"io"
"io/fs"
"github.com/Snider/Borg/pkg/datanode"
)
// SyncStrategy defines the strategy for a sync operation.
type SyncStrategy string
const (
// AppendStrategy adds new files only.
AppendStrategy SyncStrategy = "append"
// MirrorStrategy matches the source exactly.
MirrorStrategy SyncStrategy = "mirror"
// UpdateStrategy updates existing files and adds new ones.
UpdateStrategy SyncStrategy = "update"
)
// Sync merges two DataNodes based on a given strategy.
func Sync(a, b *datanode.DataNode, strategy SyncStrategy) (*datanode.DataNode, error) {
result := datanode.New()
filesA := make(map[string][]byte)
filesB := make(map[string][]byte)
// Helper function to walk a DataNode and populate a map
walkAndCollect := func(dn *datanode.DataNode, fileMap map[string][]byte) error {
return dn.Walk(".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
file, err := dn.Open(path)
if err != nil {
return err
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
return err
}
fileMap[path] = content
}
return nil
})
}
if err := walkAndCollect(a, filesA); err != nil {
return nil, fmt.Errorf("failed to walk source datanode: %w", err)
}
if err := walkAndCollect(b, filesB); err != nil {
return nil, fmt.Errorf("failed to walk target datanode: %w", err)
}
switch strategy {
case AppendStrategy:
// Add all files from A first
for path, content := range filesA {
result.AddData(path, content)
}
// Add files from B that are not in A
for path, content := range filesB {
if _, exists := filesA[path]; !exists {
result.AddData(path, content)
}
}
case MirrorStrategy:
// Result is an exact copy of B
for path, content := range filesB {
result.AddData(path, content)
}
case UpdateStrategy:
// Add all files from A first
for path, content := range filesA {
result.AddData(path, content)
}
// Add or update files from B
for path, contentB := range filesB {
contentA, exists := filesA[path]
if !exists || !bytes.Equal(contentA, contentB) {
result.AddData(path, contentB)
}
}
default:
return nil, fmt.Errorf("unknown sync strategy: %s", strategy)
}
return result, nil
}