Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/cloudstic/cmd_backup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ func TestInitSource_Local_VolumeUUID(t *testing.T) {
t.Fatalf("initSource failed: %v", err)
}
info := src.Info()
if info.VolumeUUID != "test-uuid-123" {
t.Errorf("expected VolumeUUID 'test-uuid-123', got %q", info.VolumeUUID)
if info.Identity != "test-uuid-123" {
t.Errorf("expected Identity 'test-uuid-123', got %q", info.Identity)
}
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/cloudstic/cmd_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func TestRunList_Group(t *testing.T) {
Ref: "snapshot/abc",
Snap: core.Snapshot{
Seq: 1, Created: "2024-01-01",
Source: &core.SourceInfo{Type: "gdrive", Account: "a@b.com", Path: "/", VolumeLabel: "My Drive"},
Source: &core.SourceInfo{Type: "gdrive", Account: "a@b.com", Path: "/", DriveName: "My Drive"},
},
},
{
Expand Down
9 changes: 0 additions & 9 deletions cmd/cloudstic/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@ func (r *runner) renderSnapshotTable(entries []engine.SnapshotEntry, reasons map
if e.Snap.Source != nil {
source = e.Snap.Source.Type
driveName := e.Snap.Source.DriveName
if driveName == "" {
driveName = e.Snap.Source.VolumeLabel
}
if driveName != "" {
source += " (" + driveName + ")"
}
Expand Down Expand Up @@ -85,9 +82,6 @@ func sourceGroupKey(s *core.SourceInfo) string {
if s.Identity != "" {
return s.Type + "\x00" + s.Identity + "\x00" + pathToken
}
if s.VolumeUUID != "" {
return s.Type + "\x00" + s.VolumeUUID + "\x00" + pathToken
}
return s.Type + "\x00" + s.Account + "\x00" + pathToken
}

Expand All @@ -99,9 +93,6 @@ func sourceGroupLabel(s *core.SourceInfo) string {
var parts []string
label := s.Type
driveName := s.DriveName
if driveName == "" {
driveName = s.VolumeLabel
}
if driveName != "" {
label += " (" + driveName + ")"
}
Expand Down
26 changes: 13 additions & 13 deletions cmd/cloudstic/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func TestRenderSnapshotTable_WithReasons(t *testing.T) {
}
}

func TestRenderSnapshotTable_VolumeLabel(t *testing.T) {
func TestRenderSnapshotTable_DriveName(t *testing.T) {
var out strings.Builder
r := &runner{out: &out, errOut: &strings.Builder{}}

Expand All @@ -97,10 +97,10 @@ func TestRenderSnapshotTable_VolumeLabel(t *testing.T) {
Seq: 1,
Created: "2024-01-01T00:00:00Z",
Source: &core.SourceInfo{
Type: "gdrive",
Account: "user@gmail.com",
Path: "/",
VolumeLabel: "My Drive",
Type: "gdrive",
Account: "user@gmail.com",
Path: "/",
DriveName: "My Drive",
},
},
},
Expand All @@ -124,18 +124,18 @@ func TestSourceGroupKey(t *testing.T) {
}{
{"nil source", nil, ""},
{
"local with UUID",
&core.SourceInfo{Type: "local", Account: "host", Path: ".", VolumeUUID: "UUID-1"},
"local with identity",
&core.SourceInfo{Type: "local", Account: "host", Path: ".", Identity: "UUID-1"},
"local\x00UUID-1\x00.",
},
{
"gdrive no UUID",
"gdrive no identity",
&core.SourceInfo{Type: "gdrive", Account: "user@gmail.com", Path: "/"},
"gdrive\x00user@gmail.com\x00/",
},
{
"shared drive with UUID",
&core.SourceInfo{Type: "gdrive", Account: "user@gmail.com", Path: "/", VolumeUUID: "drive-123"},
"shared drive with identity",
&core.SourceInfo{Type: "gdrive", Account: "user@gmail.com", Path: "/", Identity: "drive-123"},
"gdrive\x00drive-123\x00/",
},
}
Expand Down Expand Up @@ -163,7 +163,7 @@ func TestSourceGroupLabel(t *testing.T) {
},
{
"gdrive with label",
&core.SourceInfo{Type: "gdrive", Account: "user@gmail.com", Path: "/", VolumeLabel: "My Drive"},
&core.SourceInfo{Type: "gdrive", Account: "user@gmail.com", Path: "/", DriveName: "My Drive"},
"gdrive (My Drive) · user@gmail.com · /",
},
}
Expand All @@ -186,7 +186,7 @@ func TestRenderGroupedSnapshotTables(t *testing.T) {
Ref: "snapshot/aaa",
Snap: core.Snapshot{
Seq: 1, Created: "2024-01-01T00:00:00Z",
Source: &core.SourceInfo{Type: "gdrive", Account: "user@gmail.com", Path: "/", VolumeLabel: "My Drive"},
Source: &core.SourceInfo{Type: "gdrive", Account: "user@gmail.com", Path: "/", DriveName: "My Drive"},
},
},
{
Expand All @@ -200,7 +200,7 @@ func TestRenderGroupedSnapshotTables(t *testing.T) {
Ref: "snapshot/ccc",
Snap: core.Snapshot{
Seq: 3, Created: "2024-01-03T00:00:00Z",
Source: &core.SourceInfo{Type: "gdrive", Account: "user@gmail.com", Path: "/", VolumeLabel: "My Drive"},
Source: &core.SourceInfo{Type: "gdrive", Account: "user@gmail.com", Path: "/", DriveName: "My Drive"},
},
},
}
Expand Down
4 changes: 0 additions & 4 deletions internal/core/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,6 @@ type SourceInfo struct {
PathID string `json:"path_id,omitempty"` // stable selected-root identity within container
DriveName string `json:"drive_name,omitempty"` // human-readable container label (e.g. "My Drive")
FsType string `json:"fs_type,omitempty"` // source filesystem type (e.g. "apfs", "ext4", "sftp")

// Legacy fields (read-only compatibility path; slated for future removal).
VolumeUUID string `json:"volume_uuid,omitempty"`
VolumeLabel string `json:"volume_label,omitempty"`
}

// Snapshot represents a backup checkpoint
Expand Down
19 changes: 1 addition & 18 deletions internal/engine/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,24 +322,7 @@ func (bm *BackupManager) findPreviousSnapshot(info core.SourceInfo) *core.Snapsh
}
}

// Pass 3: legacy UUID + path match.
if info.VolumeUUID != "" {
legacyPath := info.PathID
if legacyPath == "" {
legacyPath = info.Path
}
for _, e := range entries {
if e.Snap.Source != nil &&
e.Snap.Source.Type == info.Type &&
e.Snap.Source.VolumeUUID == info.VolumeUUID &&
(e.Snap.Source.Path == legacyPath || e.Snap.Source.Path == info.Path) {
snap := e.Snap
return &snap
}
}
}

// Pass 4: legacy match (type + account + path)
// Pass 3: legacy match (type + account + path)
for _, e := range entries {
if e.Snap.Source != nil &&
e.Snap.Source.Type == info.Type &&
Expand Down
89 changes: 48 additions & 41 deletions internal/engine/backup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,10 @@ func TestBackupManager_Run(t *testing.T) {
}
}

// TestFindPreviousSnapshot_VolumeUUID verifies that findPreviousSnapshot
// uses VolumeUUID for matching when present, enabling cross-machine
// TestFindPreviousSnapshot_Identity verifies that findPreviousSnapshot
// uses Identity for matching when present, enabling cross-machine
// incremental backup for portable drives.
func TestFindPreviousSnapshot_VolumeUUID(t *testing.T) {
func TestFindPreviousSnapshot_Identity(t *testing.T) {
s := NewMockStore()

// Create snapshots from two different machines backing up the same drive.
Expand All @@ -238,21 +238,23 @@ func TestFindPreviousSnapshot_VolumeUUID(t *testing.T) {
Created: "2026-03-01T10:00:00Z",
Root: "node/mac",
Source: &core.SourceInfo{
Type: "local",
Account: "mac-studio.local",
Path: ".",
VolumeUUID: "A1B2C3D4-1234-5678-ABCD-EF0123456789",
Type: "local",
Account: "mac-studio.local",
Path: ".",
Identity: "A1B2C3D4-1234-5678-ABCD-EF0123456789",
PathID: ".",
},
}
linuxSnap := &core.Snapshot{
Seq: 2,
Created: "2026-03-02T10:00:00Z",
Root: "node/linux",
Source: &core.SourceInfo{
Type: "local",
Account: "linux-workstation",
Path: ".",
VolumeUUID: "A1B2C3D4-1234-5678-ABCD-EF0123456789",
Type: "local",
Account: "linux-workstation",
Path: ".",
Identity: "A1B2C3D4-1234-5678-ABCD-EF0123456789",
PathID: ".",
},
}

Expand All @@ -268,16 +270,17 @@ func TestFindPreviousSnapshot_VolumeUUID(t *testing.T) {
src := NewMockSource()
bm := NewBackupManager(src, s, ui.NewNoOpReporter(), nil)

// Search from the Mac with same UUID and same volume-relative path.
// Search from the Mac with same identity and same selected-root path.
info := core.SourceInfo{
Type: "local",
Account: "mac-studio.local",
Path: ".",
VolumeUUID: "A1B2C3D4-1234-5678-ABCD-EF0123456789",
Type: "local",
Account: "mac-studio.local",
Path: ".",
Identity: "A1B2C3D4-1234-5678-ABCD-EF0123456789",
PathID: ".",
}
prev := bm.findPreviousSnapshot(info)
if prev == nil {
t.Fatal("expected to find previous snapshot via UUID match")
t.Fatal("expected to find previous snapshot via identity match")
}
// Should return the most recent (Linux) snapshot since catalog is newest-first.
if prev.Root != "node/linux" {
Expand All @@ -286,7 +289,7 @@ func TestFindPreviousSnapshot_VolumeUUID(t *testing.T) {
}

// TestFindPreviousSnapshot_LegacyFallback verifies that snapshots without
// VolumeUUID are still found by the traditional account+path match.
// Identity are still found by the traditional account+path match.
func TestFindPreviousSnapshot_LegacyFallback(t *testing.T) {
s := NewMockStore()

Expand Down Expand Up @@ -324,10 +327,10 @@ func TestFindPreviousSnapshot_LegacyFallback(t *testing.T) {
}
}

// TestFindPreviousSnapshot_UUIDPreferredOverLegacy verifies that the UUID
// TestFindPreviousSnapshot_IdentityPreferredOverLegacy verifies that Identity
// match takes precedence when both UUID and account+path could match
// different snapshots.
func TestFindPreviousSnapshot_UUIDPreferredOverLegacy(t *testing.T) {
func TestFindPreviousSnapshot_IdentityPreferredOverLegacy(t *testing.T) {
s := NewMockStore()

// Old snapshot from same machine, same path, no UUID.
Expand All @@ -341,16 +344,17 @@ func TestFindPreviousSnapshot_UUIDPreferredOverLegacy(t *testing.T) {
Path: "/Volumes/MyDrive",
},
}
// Newer snapshot from different machine with UUID (volume-relative path).
// Newer snapshot from different machine with identity (portable path).
newSnap := &core.Snapshot{
Seq: 2,
Created: "2026-03-02T10:00:00Z",
Root: "node/new",
Source: &core.SourceInfo{
Type: "local",
Account: "linux-workstation",
Path: ".",
VolumeUUID: "UUID-1234",
Type: "local",
Account: "linux-workstation",
Path: ".",
Identity: "UUID-1234",
PathID: ".",
},
}

Expand All @@ -365,36 +369,38 @@ func TestFindPreviousSnapshot_UUIDPreferredOverLegacy(t *testing.T) {
src := NewMockSource()
bm := NewBackupManager(src, s, ui.NewNoOpReporter(), nil)

// Search with UUID — should find the UUID-matched snapshot first.
// Search with identity — should find the identity-matched snapshot first.
info := core.SourceInfo{
Type: "local",
Account: "mac-studio.local",
Path: ".",
VolumeUUID: "UUID-1234",
Type: "local",
Account: "mac-studio.local",
Path: ".",
Identity: "UUID-1234",
PathID: ".",
}
prev := bm.findPreviousSnapshot(info)
if prev == nil {
t.Fatal("expected to find previous snapshot")
}
if prev.Root != "node/new" {
t.Errorf("expected UUID-matched snapshot (node/new), got root=%s", prev.Root)
t.Errorf("expected identity-matched snapshot (node/new), got root=%s", prev.Root)
}
}

// TestFindPreviousSnapshot_UUIDDifferentSubdirs verifies that backups of
// TestFindPreviousSnapshot_IdentityDifferentSubdirs verifies that backups of
// different sub-directories on the same drive do not match each other.
func TestFindPreviousSnapshot_UUIDDifferentSubdirs(t *testing.T) {
func TestFindPreviousSnapshot_IdentityDifferentSubdirs(t *testing.T) {
s := NewMockStore()

photosSnap := &core.Snapshot{
Seq: 1,
Created: "2026-03-01T10:00:00Z",
Root: "node/photos",
Source: &core.SourceInfo{
Type: "local",
Account: "mac-studio.local",
Path: "Photos",
VolumeUUID: "UUID-SAME-DRIVE",
Type: "local",
Account: "mac-studio.local",
Path: "Photos",
Identity: "UUID-SAME-DRIVE",
PathID: "Photos",
},
}

Expand All @@ -408,10 +414,11 @@ func TestFindPreviousSnapshot_UUIDDifferentSubdirs(t *testing.T) {

// Search for Documents on the same drive — should NOT match Photos.
info := core.SourceInfo{
Type: "local",
Account: "mac-studio.local",
Path: "Documents",
VolumeUUID: "UUID-SAME-DRIVE",
Type: "local",
Account: "mac-studio.local",
Path: "Documents",
Identity: "UUID-SAME-DRIVE",
PathID: "Documents",
}
prev := bm.findPreviousSnapshot(info)
if prev != nil {
Expand Down
2 changes: 0 additions & 2 deletions internal/engine/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ func (lm *ListManager) Run(ctx context.Context, opts ...ListOption) (*ListResult
source = fmt.Sprintf(" source=%s account=%s path=%s", e.Snap.Source.Type, e.Snap.Source.Account, e.Snap.Source.Path)
if e.Snap.Source.DriveName != "" {
source += fmt.Sprintf(" drive=%s", e.Snap.Source.DriveName)
} else if e.Snap.Source.VolumeLabel != "" {
source += fmt.Sprintf(" drive=%s", e.Snap.Source.VolumeLabel)
}
if e.Snap.Source.Identity != "" {
source += fmt.Sprintf(" identity=%s", e.Snap.Source.Identity)
Expand Down
5 changes: 1 addition & 4 deletions internal/engine/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,6 @@ func makeGroupKey(snap *core.Snapshot, gf groupFields) GroupKey {
switch {
case snap.Source.Identity != "":
k.Account = snap.Source.Identity
case snap.Source.VolumeUUID != "":
k.Account = snap.Source.VolumeUUID
default:
k.Account = snap.Source.Account
}
Expand Down Expand Up @@ -194,8 +192,7 @@ func matchesFilter(snap *core.Snapshot, f snapshotFilter) bool {
}
// Accept display account and identity fields for compatibility.
if snap.Source.Account != f.account &&
snap.Source.Identity != f.account &&
snap.Source.VolumeUUID != f.account {
snap.Source.Identity != f.account {
return false
}
}
Expand Down
Loading
Loading