From 756b429362bb2b163006b629d8377ef4bf67c8d9 Mon Sep 17 00:00:00 2001 From: ashmrtn <3891298+ashmrtn@users.noreply.github.com> Date: Wed, 22 Jun 2022 13:14:00 -0700 Subject: [PATCH] Add public kopia function to restore a single item (#225) Returns a DataCollection to the caller on success. Also update tests to use new public function where appropriate. --- src/internal/kopia/kopia.go | 27 ++++++++ src/internal/kopia/kopia_test.go | 104 +++++++++++++++++++++---------- 2 files changed, 98 insertions(+), 33 deletions(-) diff --git a/src/internal/kopia/kopia.go b/src/internal/kopia/kopia.go index e11a2ec87..32340e40f 100644 --- a/src/internal/kopia/kopia.go +++ b/src/internal/kopia/kopia.go @@ -8,6 +8,7 @@ import ( "github.com/kopia/kopia/fs/virtualfs" "github.com/kopia/kopia/repo" "github.com/kopia/kopia/repo/blob" + "github.com/kopia/kopia/repo/manifest" "github.com/kopia/kopia/snapshot" "github.com/kopia/kopia/snapshot/policy" "github.com/kopia/kopia/snapshot/snapshotfs" @@ -363,6 +364,32 @@ func (kw KopiaWrapper) makeSnapshotWithRoot( return &res, nil } +// RestoreSingleItem looks up the item at the given path in the snapshot with id +// snapshotID. The path should be the full path of the item from the root. +// If the item is a file in kopia then it returns a DataCollection with the item +// as its sole element and DataCollection.FullPath() set to +// split(dirname(itemPath), "/"). If the item does not exist in kopia or is not +// a file an error is returned. The UUID of the returned DataStreams will be the +// name of the kopia file the data is sourced from. +func (kw KopiaWrapper) RestoreSingleItem( + ctx context.Context, + snapshotID string, + itemPath []string, +) (connector.DataCollection, error) { + manifest, err := snapshot.LoadSnapshot(ctx, kw.rep, manifest.ID(snapshotID)) + if err != nil { + return nil, errors.Wrap(err, "getting snapshot handle") + } + + rootDirEntry, err := snapshotfs.SnapshotRoot(kw.rep, manifest) + if err != nil { + return nil, errors.Wrap(err, "getting root directory") + } + + // Fine if rootDirEntry is nil, will be checked in called function. + return kw.restoreSingleItem(ctx, rootDirEntry, itemPath[1:]) +} + // restoreSingleItem looks up the item at the given path starting from rootDir // where rootDir is the root of a snapshot. If the item is a file in kopia then // it returns a DataCollection with the item as its sole element and diff --git a/src/internal/kopia/kopia_test.go b/src/internal/kopia/kopia_test.go index 5aea48903..e0b3ff340 100644 --- a/src/internal/kopia/kopia_test.go +++ b/src/internal/kopia/kopia_test.go @@ -10,9 +10,8 @@ import ( "github.com/kopia/kopia/fs" "github.com/kopia/kopia/fs/virtualfs" "github.com/kopia/kopia/repo" + "github.com/kopia/kopia/repo/manifest" "github.com/kopia/kopia/snapshot" - "github.com/kopia/kopia/snapshot/snapshotfs" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -295,14 +294,12 @@ func (suite *KopiaIntegrationSuite) TestBackupCollections() { assert.False(suite.T(), stats.Incomplete) } -// TODO(ashmrtn): Update this once we have a helper for getting the snapshot -// root. -func getSnapshotRoot( +func getSnapshotID( t *testing.T, ctx context.Context, rep repo.Repository, rootName string, -) fs.Entry { +) manifest.ID { si := snapshot.SourceInfo{ Host: kTestHost, UserName: kTestUser, @@ -313,16 +310,10 @@ func getSnapshotRoot( require.NoError(t, err) require.Len(t, manifests, 1) - rootDirEntry, err := snapshotfs.SnapshotRoot(rep, manifests[0]) - require.NoError(t, err) - - rootDir, ok := rootDirEntry.(fs.Directory) - require.True(t, ok) - - return rootDir + return manifests[0].ID } -func setupSimpleRepo(t *testing.T, ctx context.Context, k *KopiaWrapper) { +func setupSimpleRepo(t *testing.T, ctx context.Context, k *KopiaWrapper) manifest.ID { collections := []connector.DataCollection{ &singleItemCollection{ path: testPath, @@ -340,6 +331,8 @@ func setupSimpleRepo(t *testing.T, ctx context.Context, k *KopiaWrapper) { require.Equal(t, stats.IgnoredErrorCount, 0) require.Equal(t, stats.ErrorCount, 0) require.False(t, stats.Incomplete) + + return getSnapshotID(t, ctx, k.rep, testPath[0]) } func (suite *KopiaIntegrationSuite) TestBackupAndRestoreSingleItem() { @@ -353,11 +346,13 @@ func (suite *KopiaIntegrationSuite) TestBackupAndRestoreSingleItem() { assert.NoError(t, k.Close(ctx)) }() - setupSimpleRepo(t, ctx, k) + id := setupSimpleRepo(t, ctx, k) - rootDir := getSnapshotRoot(t, ctx, k.rep, testTenant) - - c, err := k.restoreSingleItem(ctx, rootDir, append(testPath[1:], testFileUUID)) + c, err := k.RestoreSingleItem( + ctx, + string(id), + append(testPath, testFileUUID), + ) require.NoError(t, err) assert.Equal(t, c.FullPath(), testPath) @@ -373,7 +368,64 @@ func (suite *KopiaIntegrationSuite) TestBackupAndRestoreSingleItem() { assert.Equal(t, buf, testFileData) } +// TestBackupAndRestoreSingleItem_Errors exercises the public RestoreSingleItem +// function. func (suite *KopiaIntegrationSuite) TestBackupAndRestoreSingleItem_Errors() { + table := []struct { + name string + snapshotIDFunc func(manifest.ID) manifest.ID + path []string + }{ + { + "NoSnapshot", + func(manifest.ID) manifest.ID { + return manifest.ID("foo") + }, + append(testPath, testFileUUID), + }, + { + "TargetNotAFile", + func(m manifest.ID) manifest.ID { + return m + }, + testPath[:2], + }, + { + "NonExistentFile", + func(m manifest.ID) manifest.ID { + return m + }, + append(testPath, "foo"), + }, + } + + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + ctx := context.Background() + timeOfTest := ctesting.LogTimeOfTest(t) + + k, err := openKopiaRepo(ctx, "backup-restore-single-item-error-"+test.name+"-"+timeOfTest) + require.NoError(t, err) + defer func() { + assert.NoError(t, k.Close(ctx)) + }() + + id := setupSimpleRepo(t, ctx, k) + + _, err = k.RestoreSingleItem( + ctx, + string(test.snapshotIDFunc(id)), + test.path, + ) + require.Error(t, err) + }) + } +} + +// TestBackupAndRestoreSingleItem_Errors2 exercises some edge cases in the +// package-private restoreSingleItem function. It helps ensure kopia behaves the +// way we expect. +func (suite *KopiaIntegrationSuite) TestBackupAndRestoreSingleItem_Errors2() { table := []struct { name string rootDirFunc func(*testing.T, context.Context, *KopiaWrapper) fs.Entry @@ -393,20 +445,6 @@ func (suite *KopiaIntegrationSuite) TestBackupAndRestoreSingleItem_Errors() { }, append(testPath[1:], testFileUUID), }, - { - "TargetNotAFile", - func(t *testing.T, ctx context.Context, k *KopiaWrapper) fs.Entry { - return getSnapshotRoot(t, ctx, k.rep, testPath[0]) - }, - []string{testPath[1]}, - }, - { - "NonExistentFile", - func(t *testing.T, ctx context.Context, k *KopiaWrapper) fs.Entry { - return getSnapshotRoot(t, ctx, k.rep, testPath[0]) - }, - append(testPath[1:], "foo"), - }, } for _, test := range table { @@ -414,7 +452,7 @@ func (suite *KopiaIntegrationSuite) TestBackupAndRestoreSingleItem_Errors() { ctx := context.Background() timeOfTest := ctesting.LogTimeOfTest(t) - k, err := openKopiaRepo(ctx, "backup-restore-single-item-error-"+test.name+"-"+timeOfTest) + k, err := openKopiaRepo(ctx, "backup-restore-single-item-error2-"+test.name+"-"+timeOfTest) require.NoError(t, err) defer func() { assert.NoError(t, k.Close(ctx))