From 4befbc0afe907b253354503ffc6fbd8b51942e18 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Thu, 23 Apr 2026 18:30:59 +0800 Subject: [PATCH 1/4] add repo snapshot operations Signed-off-by: Lyndon-Li --- pkg/repository/udmrepo/kopialib/lib_repo.go | 78 ++++- .../udmrepo/kopialib/lib_repo_test.go | 268 ++++++++++++++++++ pkg/repository/udmrepo/repo.go | 17 +- 3 files changed, 350 insertions(+), 13 deletions(-) diff --git a/pkg/repository/udmrepo/kopialib/lib_repo.go b/pkg/repository/udmrepo/kopialib/lib_repo.go index 34559baf7..5c89785cf 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo.go @@ -25,12 +25,14 @@ import ( "sync/atomic" "time" + "github.com/kopia/kopia/fs" "github.com/kopia/kopia/repo" "github.com/kopia/kopia/repo/compression" "github.com/kopia/kopia/repo/content/index" "github.com/kopia/kopia/repo/maintenance" "github.com/kopia/kopia/repo/manifest" "github.com/kopia/kopia/repo/object" + "github.com/kopia/kopia/snapshot" "github.com/kopia/kopia/snapshot/snapshotmaintenance" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -446,19 +448,83 @@ func (kr *kopiaRepository) DeleteManifest(ctx context.Context, id udmrepo.ID) er return nil } -// TODO add implementation in following PRs func (kr *kopiaRepository) SaveSnapshot(ctx context.Context, snap udmrepo.Snapshot) (udmrepo.ID, error) { - return "", errors.New("not supported") + if kr.rawWriter == nil { + return "", errors.New("repo writer is closed or not open") + } + + if snap.Source == "" { + return "", errors.New("invalid snapshot source") + } + + rootObj, err := object.ParseID(string(snap.RootObject.ID)) + if err != nil { + return "", errors.Wrapf(err, "error parsing root object ID %v", snap.RootObject.ID) + } + + manifest := snapshot.Manifest{ + Source: snapshot.SourceInfo{ + UserName: udmrepo.GetRepoUser(), + Host: udmrepo.GetRepoDomain(), + Path: snap.Source, + }, + Description: snap.Description, + StartTime: fs.UTCTimestampFromTime(snap.StartTime), + EndTime: fs.UTCTimestampFromTime(snap.EndTime), + RootEntry: &snapshot.DirEntry{ + Type: snapshot.EntryTypeDirectory, + ObjectID: rootObj, + ModTime: fs.UTCTimestampFromTime(snap.RootObject.ModTime), + Permissions: snapshot.Permissions(snap.RootObject.Permissions), + FileSize: snap.RootObject.Size, + UserID: snap.RootObject.UserID, + GroupID: snap.RootObject.GroupID, + }, + Tags: snap.Tags, + } + + id, err := snapshot.SaveSnapshot(ctx, kr.rawWriter, &manifest) + if err != nil { + return "", errors.Wrap(err, "error saving snapshot") + } + + return udmrepo.ID(id), nil } -// TODO add implementation in following PRs func (kr *kopiaRepository) GetSnapshot(ctx context.Context, id udmrepo.ID) (udmrepo.Snapshot, error) { - return udmrepo.Snapshot{}, errors.New("not supported") + snap, err := snapshot.LoadSnapshot(ctx, kr.rawRepo, manifest.ID(id)) + if err != nil { + return udmrepo.Snapshot{}, errors.Wrap(err, "error getting snapshot manifest") + } + + if snap.RootEntry == nil { + return udmrepo.Snapshot{}, errors.Wrap(err, "invalid snapshot root entry") + } + + return udmrepo.Snapshot{ + Source: snap.Source.Path, + Description: snap.Description, + StartTime: snap.StartTime.ToTime(), + EndTime: snap.EndTime.ToTime(), + Tags: snap.Tags, + RootObject: udmrepo.ObjectMetadata{ + ID: udmrepo.ID(snap.RootEntry.ObjectID.String()), + Type: udmrepo.ObjectDataTypeMetadata, + Size: snap.RootEntry.FileSize, + ModTime: snap.RootEntry.ModTime.ToTime(), + Permissions: int(snap.RootEntry.Permissions), + UserID: snap.RootEntry.UserID, + GroupID: snap.RootEntry.GroupID, + }, + }, nil } -// TODO add implementation in following PRs func (kr *kopiaRepository) DeleteSnapshot(ctx context.Context, id udmrepo.ID) error { - return errors.New("not supported") + if _, err := kr.GetSnapshot(ctx, id); err != nil { + return errors.Wrap(err, "error getting snapshot") + } + + return kr.DeleteManifest(ctx, id) } func (kr *kopiaRepository) Flush(ctx context.Context) error { diff --git a/pkg/repository/udmrepo/kopialib/lib_repo_test.go b/pkg/repository/udmrepo/kopialib/lib_repo_test.go index 36e331bef..b05b9dc1d 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo_test.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo_test.go @@ -18,14 +18,17 @@ package kopialib import ( "context" + "encoding/json" "math" "os" "testing" "time" + "github.com/kopia/kopia/fs" "github.com/kopia/kopia/repo" "github.com/kopia/kopia/repo/manifest" "github.com/kopia/kopia/repo/object" + "github.com/kopia/kopia/snapshot" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -1282,3 +1285,268 @@ func TestIsReady(t *testing.T) { }) } } + +func TestSaveSnapshot(t *testing.T) { + testCases := []struct { + name string + rawWriter *repomocks.MockRepositoryWriter + snap udmrepo.Snapshot + rawWriterRetErr error + rawWriterRetID manifest.ID + setWriterMock bool + expectedErr string + expectedID udmrepo.ID + }{ + { + name: "raw writer is nil", + expectedErr: "repo writer is closed or not open", + }, + { + name: "invalid snapshot source", + rawWriter: repomocks.NewMockRepositoryWriter(t), + snap: udmrepo.Snapshot{ + Source: "", + }, + expectedErr: "invalid snapshot source", + }, + { + name: "invalid root object id", + rawWriter: repomocks.NewMockRepositoryWriter(t), + snap: udmrepo.Snapshot{ + Source: "fake-source", + RootObject: udmrepo.ObjectMetadata{ID: "fake-id"}, + }, + expectedErr: "error parsing root object ID fake-id: malformed content ID: \"fake-id\": invalid content prefix", + }, + { + name: "save snapshot fail", + rawWriter: repomocks.NewMockRepositoryWriter(t), + snap: udmrepo.Snapshot{ + Source: "fake-source", + RootObject: udmrepo.ObjectMetadata{ID: "I123456"}, + }, + rawWriterRetErr: errors.New("fake-save-error"), + setWriterMock: true, + expectedErr: "error saving snapshot: error putting manifest: fake-save-error", + }, + { + name: "succeed", + rawWriter: repomocks.NewMockRepositoryWriter(t), + snap: udmrepo.Snapshot{ + Source: "fake-source", + RootObject: udmrepo.ObjectMetadata{ID: "I123456"}, + }, + rawWriterRetID: manifest.ID("fake-manifest-id"), + setWriterMock: true, + expectedID: udmrepo.ID("fake-manifest-id"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + kr := &kopiaRepository{} + + if tc.rawWriter != nil { + if tc.setWriterMock { + tc.rawWriter.On("PutManifest", mock.Anything, mock.Anything, mock.Anything).Return(tc.rawWriterRetID, tc.rawWriterRetErr) + } + kr.rawWriter = tc.rawWriter + } + + id, err := kr.SaveSnapshot(t.Context(), tc.snap) + + if tc.expectedErr == "" { + require.NoError(t, err) + assert.Equal(t, tc.expectedID, id) + } else { + assert.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestGetSnapshot(t *testing.T) { + expectedTime := time.Now() + rawObjID, _ := object.ParseID("I123456") + + mockMani := &snapshot.Manifest{ + Source: snapshot.SourceInfo{Path: "fake-source"}, + Description: "fake-desc", + StartTime: fs.UTCTimestampFromTime(expectedTime), + EndTime: fs.UTCTimestampFromTime(expectedTime.Add(time.Minute)), + RootEntry: &snapshot.DirEntry{ + ObjectID: rawObjID, + }, + Tags: map[string]string{"tag1": "val1"}, + } + + testCases := []struct { + name string + rawRepo *repomocks.MockRepository + snapshotID udmrepo.ID + rawRepoRetErr error + setRepoMock bool + expectedErr string + expectedSnap udmrepo.Snapshot + }{ + { + name: "get snapshot fail", + rawRepo: repomocks.NewMockRepository(t), + snapshotID: udmrepo.ID("fake-id"), + rawRepoRetErr: errors.New("fake-get-error"), + setRepoMock: true, + expectedErr: "error getting snapshot manifest: unable to find manifest entries: fake-get-error", + }, + { + name: "succeed", + rawRepo: repomocks.NewMockRepository(t), + snapshotID: udmrepo.ID("fake-id"), + setRepoMock: true, + expectedSnap: udmrepo.Snapshot{ + Source: "fake-source", + Description: "fake-desc", + StartTime: mockMani.StartTime.ToTime(), + EndTime: mockMani.EndTime.ToTime(), + RootObject: udmrepo.ObjectMetadata{ + ID: udmrepo.ID("I123456"), + Type: udmrepo.ObjectDataTypeMetadata, + }, + Tags: map[string]string{"tag1": "val1"}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + kr := &kopiaRepository{} + + if tc.rawRepo != nil { + if tc.setRepoMock { + tc.rawRepo.On("GetManifest", mock.Anything, mock.Anything, mock.Anything).Return(&manifest.EntryMetadata{ + Labels: map[string]string{ + manifest.TypeLabelKey: snapshot.ManifestType, + }, + }, tc.rawRepoRetErr).Run(func(args mock.Arguments) { + if tc.rawRepoRetErr == nil { + payload := args.Get(2) + if ptr, ok := payload.(*snapshot.Manifest); ok { + *ptr = *mockMani + } else { + b, _ := json.Marshal(mockMani) + json.Unmarshal(b, payload) + } + } + }) + } + kr.rawRepo = tc.rawRepo + } + + snap, err := kr.GetSnapshot(t.Context(), tc.snapshotID) + + if tc.expectedErr == "" { + require.NoError(t, err) + assert.Equal(t, tc.expectedSnap, snap) + } else { + assert.EqualError(t, err, tc.expectedErr) + } + }) + } +} + +func TestDeleteSnapshot(t *testing.T) { + expectedTime := time.Now() + rawObjID, _ := object.ParseID("I123456") + + mockMani := &snapshot.Manifest{ + Source: snapshot.SourceInfo{Path: "fake-source"}, + Description: "fake-desc", + StartTime: fs.UTCTimestampFromTime(expectedTime), + EndTime: fs.UTCTimestampFromTime(expectedTime.Add(time.Minute)), + RootEntry: &snapshot.DirEntry{ + ObjectID: rawObjID, + }, + Tags: map[string]string{"tag1": "val1"}, + } + + testCases := []struct { + name string + rawRepo *repomocks.MockRepository + rawWriter *repomocks.MockRepositoryWriter + snapshotID udmrepo.ID + rawRepoRetErr error + rawWriterRetErr error + setRepoMock bool + setWriterMock bool + expectedErr string + }{ + { + name: "get snapshot fail", + rawRepo: repomocks.NewMockRepository(t), + snapshotID: udmrepo.ID("fake-id"), + rawRepoRetErr: errors.New("fake-get-error"), + setRepoMock: true, + expectedErr: "error getting snapshot: error getting snapshot manifest: unable to find manifest entries: fake-get-error", + }, + { + name: "delete manifest fail", + rawRepo: repomocks.NewMockRepository(t), + rawWriter: repomocks.NewMockRepositoryWriter(t), + snapshotID: udmrepo.ID("fake-id"), + rawWriterRetErr: errors.New("fake-delete-error"), + setRepoMock: true, + setWriterMock: true, + expectedErr: "error to delete manifest: fake-delete-error", + }, + { + name: "succeed", + rawRepo: repomocks.NewMockRepository(t), + rawWriter: repomocks.NewMockRepositoryWriter(t), + snapshotID: udmrepo.ID("fake-id"), + setRepoMock: true, + setWriterMock: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + kr := &kopiaRepository{} + + if tc.rawRepo != nil { + if tc.setRepoMock { + tc.rawRepo.On("GetManifest", mock.Anything, mock.Anything, mock.Anything).Return(&manifest.EntryMetadata{ + Labels: map[string]string{ + manifest.TypeLabelKey: snapshot.ManifestType, + }, + }, tc.rawRepoRetErr).Run(func(args mock.Arguments) { + if tc.rawRepoRetErr == nil { + payload := args.Get(2) + if ptr, ok := payload.(*snapshot.Manifest); ok { + *ptr = *mockMani + } else { + b, _ := json.Marshal(mockMani) + json.Unmarshal(b, payload) + } + } + }) + } + kr.rawRepo = tc.rawRepo + } + + if tc.rawWriter != nil { + if tc.setWriterMock { + tc.rawWriter.On("DeleteManifest", mock.Anything, mock.Anything).Return(tc.rawWriterRetErr) + } + kr.rawWriter = tc.rawWriter + } + + err := kr.DeleteSnapshot(t.Context(), tc.snapshotID) + + if tc.expectedErr == "" { + require.NoError(t, err) + } else { + assert.EqualError(t, err, tc.expectedErr) + } + }) + } +} + diff --git a/pkg/repository/udmrepo/repo.go b/pkg/repository/udmrepo/repo.go index 095d18973..2be015a89 100644 --- a/pkg/repository/udmrepo/repo.go +++ b/pkg/repository/udmrepo/repo.go @@ -77,15 +77,18 @@ type AdvancedFeatureInfo struct { } type ObjectMetadata struct { - ID ID - Type int // OBJECT_DATA_TYPE_* - Size int64 + ID ID + Type int // OBJECT_DATA_TYPE_* + Size int64 + ModTime time.Time + Permissions int + UserID uint32 + GroupID uint32 } type Metadata struct { - SubObjects []ObjectMetadata // For dir metadata only, the sub objects in this dir. - ExtraDataLen int // Extra data associated to this metadata. - ExtraData []byte + SubObjects []ObjectMetadata + Summary string } type Snapshot struct { @@ -94,7 +97,7 @@ type Snapshot struct { StartTime time.Time EndTime time.Time Tags map[string]string - RootObject ID + RootObject ObjectMetadata } // BackupRepoService is used to initialize, open or maintain a backup repository From 6ca73a00b6abace9a6a94536d15f11ce5989c85c Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Fri, 8 May 2026 16:39:24 +0800 Subject: [PATCH 2/4] fix UT error Signed-off-by: Lyndon-Li --- pkg/repository/udmrepo/kopialib/lib_repo_test.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/repository/udmrepo/kopialib/lib_repo_test.go b/pkg/repository/udmrepo/kopialib/lib_repo_test.go index b05b9dc1d..3ca962991 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo_test.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo_test.go @@ -1408,8 +1408,13 @@ func TestGetSnapshot(t *testing.T) { StartTime: mockMani.StartTime.ToTime(), EndTime: mockMani.EndTime.ToTime(), RootObject: udmrepo.ObjectMetadata{ - ID: udmrepo.ID("I123456"), - Type: udmrepo.ObjectDataTypeMetadata, + ID: udmrepo.ID("I123456"), + Type: udmrepo.ObjectDataTypeMetadata, + Size: mockMani.RootEntry.FileSize, + ModTime: mockMani.RootEntry.ModTime.ToTime(), + Permissions: int(mockMani.RootEntry.Permissions), + UserID: mockMani.RootEntry.UserID, + GroupID: mockMani.RootEntry.GroupID, }, Tags: map[string]string{"tag1": "val1"}, }, From 62572821172664171eacd3158eade188df7c8565 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 12 May 2026 09:51:47 +0800 Subject: [PATCH 3/4] add listsnapshot method Signed-off-by: Lyndon-Li --- pkg/repository/udmrepo/kopialib/lib_repo.go | 33 ++++++ .../udmrepo/kopialib/lib_repo_test.go | 110 ++++++++++++++++++ pkg/repository/udmrepo/mocks/BackupRepo.go | 68 +++++++++++ pkg/repository/udmrepo/repo.go | 3 + 4 files changed, 214 insertions(+) diff --git a/pkg/repository/udmrepo/kopialib/lib_repo.go b/pkg/repository/udmrepo/kopialib/lib_repo.go index 5c89785cf..2cc29e0de 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo.go @@ -527,6 +527,39 @@ func (kr *kopiaRepository) DeleteSnapshot(ctx context.Context, id udmrepo.ID) er return kr.DeleteManifest(ctx, id) } +func (kr *kopiaRepository) ListSnapshot(ctx context.Context, source string) ([]udmrepo.Snapshot, error) { + mani, err := snapshot.ListSnapshots(ctx, kr.rawRepo, snapshot.SourceInfo{ + Host: udmrepo.GetRepoDomain(), + UserName: udmrepo.GetRepoUser(), + Path: source, + }) + if err != nil { + return nil, errors.Wrapf(err, "error listing snapshot manifest for source %s", source) + } + + snapshots := []udmrepo.Snapshot{} + for _, snap := range mani { + snapshots = append(snapshots, udmrepo.Snapshot{ + Source: snap.Source.Path, + Description: snap.Description, + StartTime: snap.StartTime.ToTime(), + EndTime: snap.EndTime.ToTime(), + Tags: snap.Tags, + RootObject: udmrepo.ObjectMetadata{ + ID: udmrepo.ID(snap.RootEntry.ObjectID.String()), + Type: udmrepo.ObjectDataTypeMetadata, + Size: snap.RootEntry.FileSize, + ModTime: snap.RootEntry.ModTime.ToTime(), + Permissions: int(snap.RootEntry.Permissions), + UserID: snap.RootEntry.UserID, + GroupID: snap.RootEntry.GroupID, + }, + }) + } + + return snapshots, nil +} + func (kr *kopiaRepository) Flush(ctx context.Context) error { if kr.rawWriter == nil { return errors.New("repo writer is closed or not open") diff --git a/pkg/repository/udmrepo/kopialib/lib_repo_test.go b/pkg/repository/udmrepo/kopialib/lib_repo_test.go index 3ca962991..750d2b9eb 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo_test.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo_test.go @@ -1555,3 +1555,113 @@ func TestDeleteSnapshot(t *testing.T) { } } +func TestListSnapshot(t *testing.T) { + expectedTime := time.Now() + rawObjID, _ := object.ParseID("I123456") + + mockMani := &snapshot.Manifest{ + Source: snapshot.SourceInfo{Path: "fake-source"}, + Description: "fake-desc", + StartTime: fs.UTCTimestampFromTime(expectedTime), + EndTime: fs.UTCTimestampFromTime(expectedTime.Add(time.Minute)), + RootEntry: &snapshot.DirEntry{ + ObjectID: rawObjID, + FileSize: 100, + ModTime: fs.UTCTimestampFromTime(expectedTime), + Permissions: 0o644, + UserID: 1000, + GroupID: 1000, + }, + Tags: map[string]string{"tag1": "val1"}, + } + + testCases := []struct { + name string + rawRepo *repomocks.MockRepository + source string + findRetErr error + setRepoMock bool + expectedErr string + expectedSnaps []udmrepo.Snapshot + }{ + { + name: "find manifest fail", + rawRepo: repomocks.NewMockRepository(t), + source: "fake-source", + findRetErr: errors.New("fake-find-error"), + setRepoMock: true, + expectedErr: "error listing snapshot manifest for source fake-source: unable to find manifest entries: fake-find-error", + }, + { + name: "succeed", + rawRepo: repomocks.NewMockRepository(t), + source: "fake-source", + setRepoMock: true, + expectedSnaps: []udmrepo.Snapshot{ + { + Source: "fake-source", + Description: "fake-desc", + StartTime: mockMani.StartTime.ToTime(), + EndTime: mockMani.EndTime.ToTime(), + RootObject: udmrepo.ObjectMetadata{ + ID: udmrepo.ID("I123456"), + Type: udmrepo.ObjectDataTypeMetadata, + Size: mockMani.RootEntry.FileSize, + ModTime: mockMani.RootEntry.ModTime.ToTime(), + Permissions: int(mockMani.RootEntry.Permissions), + UserID: mockMani.RootEntry.UserID, + GroupID: mockMani.RootEntry.GroupID, + }, + Tags: map[string]string{"tag1": "val1"}, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + kr := &kopiaRepository{} + + if tc.rawRepo != nil { + if tc.setRepoMock { + tc.rawRepo.On("FindManifests", mock.Anything, mock.Anything).Return([]*manifest.EntryMetadata{ + { + ID: "fake-id", + Labels: map[string]string{ + manifest.TypeLabelKey: snapshot.ManifestType, + "hostname": udmrepo.GetRepoDomain(), + "username": udmrepo.GetRepoUser(), + "path": tc.source, + }, + }, + }, tc.findRetErr) + + tc.rawRepo.On("GetManifest", mock.Anything, mock.Anything, mock.Anything).Return(&manifest.EntryMetadata{ + Labels: map[string]string{ + manifest.TypeLabelKey: snapshot.ManifestType, + }, + }, nil).Run(func(args mock.Arguments) { + payload := args.Get(2) + if ptr, ok := payload.(*snapshot.Manifest); ok { + *ptr = *mockMani + } else { + b, _ := json.Marshal(mockMani) + json.Unmarshal(b, payload) + } + }).Maybe() + } + kr.rawRepo = tc.rawRepo + } + + snaps, err := kr.ListSnapshot(t.Context(), tc.source) + + if tc.expectedErr == "" { + require.NoError(t, err) + assert.Equal(t, tc.expectedSnaps, snaps) + } else { + assert.EqualError(t, err, tc.expectedErr) + } + }) + } +} + diff --git a/pkg/repository/udmrepo/mocks/BackupRepo.go b/pkg/repository/udmrepo/mocks/BackupRepo.go index e495b6b00..623c4d70d 100644 --- a/pkg/repository/udmrepo/mocks/BackupRepo.go +++ b/pkg/repository/udmrepo/mocks/BackupRepo.go @@ -562,6 +562,74 @@ func (_c *BackupRepo_GetSnapshot_Call) RunAndReturn(run func(ctx context.Context return _c } +// ListSnapshot provides a mock function for the type BackupRepo +func (_mock *BackupRepo) ListSnapshot(ctx context.Context, source string) ([]udmrepo.Snapshot, error) { + ret := _mock.Called(ctx, source) + + if len(ret) == 0 { + panic("no return value specified for ListSnapshot") + } + + var r0 []udmrepo.Snapshot + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]udmrepo.Snapshot, error)); ok { + return returnFunc(ctx, source) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string) []udmrepo.Snapshot); ok { + r0 = returnFunc(ctx, source) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]udmrepo.Snapshot) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = returnFunc(ctx, source) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// BackupRepo_ListSnapshot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListSnapshot' +type BackupRepo_ListSnapshot_Call struct { + *mock.Call +} + +// ListSnapshot is a helper method to define mock.On call +// - ctx context.Context +// - source string +func (_e *BackupRepo_Expecter) ListSnapshot(ctx interface{}, source interface{}) *BackupRepo_ListSnapshot_Call { + return &BackupRepo_ListSnapshot_Call{Call: _e.mock.On("ListSnapshot", ctx, source)} +} + +func (_c *BackupRepo_ListSnapshot_Call) Run(run func(ctx context.Context, source string)) *BackupRepo_ListSnapshot_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *BackupRepo_ListSnapshot_Call) Return(snapshots []udmrepo.Snapshot, err error) *BackupRepo_ListSnapshot_Call { + _c.Call.Return(snapshots, err) + return _c +} + +func (_c *BackupRepo_ListSnapshot_Call) RunAndReturn(run func(ctx context.Context, source string) ([]udmrepo.Snapshot, error)) *BackupRepo_ListSnapshot_Call { + _c.Call.Return(run) + return _c +} + // NewObjectWriter provides a mock function for the type BackupRepo func (_mock *BackupRepo) NewObjectWriter(ctx context.Context, opt udmrepo.ObjectWriteOptions) (udmrepo.ObjectWriter, error) { ret := _mock.Called(ctx, opt) diff --git a/pkg/repository/udmrepo/repo.go b/pkg/repository/udmrepo/repo.go index 2be015a89..a87ba30db 100644 --- a/pkg/repository/udmrepo/repo.go +++ b/pkg/repository/udmrepo/repo.go @@ -180,6 +180,9 @@ type BackupRepo interface { // DeleteSnapshot deletes a repo snapshot DeleteSnapshot(ctx context.Context, id ID) error + // ListSnapshot lists all snapshots in repo for the given source + ListSnapshot(ctx context.Context, source string) ([]Snapshot, error) + // Close closes the backup repository Close(ctx context.Context) error } From 205ca71588f1bdd092414aaa44a22fa9b1536e62 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Thu, 21 May 2026 13:36:23 +0800 Subject: [PATCH 4/4] kopia repo snapshot operations Signed-off-by: Lyndon-Li --- changelogs/unreleased/9808-Lyndon-Li | 1 + pkg/repository/udmrepo/kopialib/lib_repo.go | 2 +- pkg/repository/udmrepo/kopialib/lib_repo_test.go | 11 +++++------ 3 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/9808-Lyndon-Li diff --git a/changelogs/unreleased/9808-Lyndon-Li b/changelogs/unreleased/9808-Lyndon-Li new file mode 100644 index 000000000..d3564ab2d --- /dev/null +++ b/changelogs/unreleased/9808-Lyndon-Li @@ -0,0 +1 @@ +Add Kopia repo snapshot operations for block data mover \ No newline at end of file diff --git a/pkg/repository/udmrepo/kopialib/lib_repo.go b/pkg/repository/udmrepo/kopialib/lib_repo.go index 2cc29e0de..7298347b7 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo.go @@ -498,7 +498,7 @@ func (kr *kopiaRepository) GetSnapshot(ctx context.Context, id udmrepo.ID) (udmr } if snap.RootEntry == nil { - return udmrepo.Snapshot{}, errors.Wrap(err, "invalid snapshot root entry") + return udmrepo.Snapshot{}, errors.New("invalid snapshot root entry") } return udmrepo.Snapshot{ diff --git a/pkg/repository/udmrepo/kopialib/lib_repo_test.go b/pkg/repository/udmrepo/kopialib/lib_repo_test.go index 750d2b9eb..1770ef83e 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo_test.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo_test.go @@ -1565,12 +1565,12 @@ func TestListSnapshot(t *testing.T) { StartTime: fs.UTCTimestampFromTime(expectedTime), EndTime: fs.UTCTimestampFromTime(expectedTime.Add(time.Minute)), RootEntry: &snapshot.DirEntry{ - ObjectID: rawObjID, - FileSize: 100, - ModTime: fs.UTCTimestampFromTime(expectedTime), + ObjectID: rawObjID, + FileSize: 100, + ModTime: fs.UTCTimestampFromTime(expectedTime), Permissions: 0o644, - UserID: 1000, - GroupID: 1000, + UserID: 1000, + GroupID: 1000, }, Tags: map[string]string{"tag1": "val1"}, } @@ -1664,4 +1664,3 @@ func TestListSnapshot(t *testing.T) { }) } } -