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