use path struct in kopia DataCollection (#827)
* Use path struct in kopia DataCollection Does not change the external API of the DataCollection any, just updates internals in preparation for switching support of data.Collection.FullPath. * Expand Path interface slightly kopia.Wrapper needs some extra functionality from paths, mostly along the lines of directly manipulating the elements in the path. This gives access to those functions. * Use path struct in kopia.Wrapper for restore Pass path structs to the newly created collections during restore. * Add tests for new path functionality
This commit is contained in:
parent
573f55686f
commit
226489c58f
@ -2,8 +2,10 @@ package kopia
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
|
"github.com/alcionai/corso/src/internal/path"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -12,7 +14,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type kopiaDataCollection struct {
|
type kopiaDataCollection struct {
|
||||||
path []string
|
path path.Path
|
||||||
streams []data.Stream
|
streams []data.Stream
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,7 +33,9 @@ func (kdc *kopiaDataCollection) Items() <-chan data.Stream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (kdc kopiaDataCollection) FullPath() []string {
|
func (kdc kopiaDataCollection) FullPath() []string {
|
||||||
return append([]string{}, kdc.path...)
|
// TODO(ashmrtn): Update this once data.Collection.FullPath supports
|
||||||
|
// path.Path. Assumes no adversarial users that use "/" in their folder names.
|
||||||
|
return strings.Split(kdc.path.String(), "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
type kopiaDataStream struct {
|
type kopiaDataStream struct {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
|
"github.com/alcionai/corso/src/internal/path"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---------------
|
// ---------------
|
||||||
@ -26,15 +27,33 @@ func TestKopiaDataCollectionUnitSuite(t *testing.T) {
|
|||||||
|
|
||||||
func (suite *KopiaDataCollectionUnitSuite) TestReturnsPath() {
|
func (suite *KopiaDataCollectionUnitSuite) TestReturnsPath() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
|
expected := []string{
|
||||||
|
"a-tenant",
|
||||||
|
path.ExchangeService.String(),
|
||||||
|
"a-user",
|
||||||
|
path.EmailCategory.String(),
|
||||||
|
"some",
|
||||||
|
"path",
|
||||||
|
"for",
|
||||||
|
"data",
|
||||||
|
}
|
||||||
|
|
||||||
path := []string{"some", "path", "for", "data"}
|
b := path.Builder{}.Append("some", "path", "for", "data")
|
||||||
|
pth, err := b.ToDataLayerExchangePathForCategory(
|
||||||
|
"a-tenant",
|
||||||
|
"a-user",
|
||||||
|
path.EmailCategory,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
c := kopiaDataCollection{
|
c := kopiaDataCollection{
|
||||||
streams: []data.Stream{},
|
streams: []data.Stream{},
|
||||||
path: path,
|
path: pth,
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, c.FullPath(), path)
|
// TODO(ashmrtn): Update when data.Collection.FullPath supports path.Path
|
||||||
|
assert.Equal(t, expected, c.FullPath())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *KopiaDataCollectionUnitSuite) TestReturnsStreams() {
|
func (suite *KopiaDataCollectionUnitSuite) TestReturnsStreams() {
|
||||||
@ -80,7 +99,7 @@ func (suite *KopiaDataCollectionUnitSuite) TestReturnsStreams() {
|
|||||||
suite.T().Run(test.name, func(t *testing.T) {
|
suite.T().Run(test.name, func(t *testing.T) {
|
||||||
c := kopiaDataCollection{
|
c := kopiaDataCollection{
|
||||||
streams: test.streams,
|
streams: test.streams,
|
||||||
path: []string{},
|
path: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
count := 0
|
count := 0
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package kopia
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"path"
|
stdpath "path"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
|
"github.com/alcionai/corso/src/internal/path"
|
||||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||||
"github.com/alcionai/corso/src/pkg/logger"
|
"github.com/alcionai/corso/src/pkg/logger"
|
||||||
)
|
)
|
||||||
@ -163,7 +164,7 @@ func getStreamItemFunc(
|
|||||||
return errs.ErrorOrNil()
|
return errs.ErrorOrNil()
|
||||||
}
|
}
|
||||||
|
|
||||||
itemPath := path.Join(append(streamedEnts.FullPath(), e.UUID())...)
|
itemPath := stdpath.Join(append(streamedEnts.FullPath(), e.UUID())...)
|
||||||
|
|
||||||
ei, ok := e.(data.StreamInfo)
|
ei, ok := e.(data.StreamInfo)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -179,7 +180,7 @@ func getStreamItemFunc(
|
|||||||
// Relative path given to us in the callback is missing the root
|
// Relative path given to us in the callback is missing the root
|
||||||
// element. Add to pending set before calling the callback to avoid race
|
// element. Add to pending set before calling the callback to avoid race
|
||||||
// conditions when the item is completed.
|
// conditions when the item is completed.
|
||||||
p := path.Join(append(streamedEnts.FullPath()[1:], e.UUID())...)
|
p := stdpath.Join(append(streamedEnts.FullPath()[1:], e.UUID())...)
|
||||||
d := &itemDetails{info: ei.Info(), repoRef: itemPath}
|
d := &itemDetails{info: ei.Info(), repoRef: itemPath}
|
||||||
|
|
||||||
progress.put(p, d)
|
progress.put(p, d)
|
||||||
@ -411,9 +412,9 @@ func (w Wrapper) makeSnapshotWithRoot(
|
|||||||
func (w Wrapper) getEntry(
|
func (w Wrapper) getEntry(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
snapshotID string,
|
snapshotID string,
|
||||||
itemPath []string,
|
itemPath path.Path,
|
||||||
) (fs.Entry, error) {
|
) (fs.Entry, error) {
|
||||||
if len(itemPath) == 0 {
|
if itemPath == nil {
|
||||||
return nil, errors.New("no restore path given")
|
return nil, errors.New("no restore path given")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,7 +429,11 @@ func (w Wrapper) getEntry(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetNestedEntry handles nil properly.
|
// GetNestedEntry handles nil properly.
|
||||||
e, err := snapshotfs.GetNestedEntry(ctx, rootDirEntry, itemPath[1:])
|
e, err := snapshotfs.GetNestedEntry(
|
||||||
|
ctx,
|
||||||
|
rootDirEntry,
|
||||||
|
itemPath.PopFront().Elements(),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "getting nested object handle")
|
return nil, errors.Wrap(err, "getting nested object handle")
|
||||||
}
|
}
|
||||||
@ -447,7 +452,19 @@ func (w Wrapper) collectItems(
|
|||||||
snapshotID string,
|
snapshotID string,
|
||||||
itemPath []string,
|
itemPath []string,
|
||||||
) ([]data.Collection, error) {
|
) ([]data.Collection, error) {
|
||||||
e, err := w.getEntry(ctx, snapshotID, itemPath)
|
// TODO(ashmrtn): Remove this extra parsing once selectors pass path.Path to
|
||||||
|
// this function.
|
||||||
|
pth, err := path.FromDataLayerPath(stdpath.Join(itemPath...), true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "parsing to path struct")
|
||||||
|
}
|
||||||
|
|
||||||
|
parentDir, err := pth.Dir()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "getting parent directory from path")
|
||||||
|
}
|
||||||
|
|
||||||
|
e, err := w.getEntry(ctx, snapshotID, pth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -457,7 +474,7 @@ func (w Wrapper) collectItems(
|
|||||||
return nil, errors.New("requested object is not a file")
|
return nil, errors.New("requested object is not a file")
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := restoreSingleItem(ctx, f, itemPath[:len(itemPath)-1])
|
c, err := restoreSingleItem(ctx, f, parentDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -495,7 +512,7 @@ func (w Wrapper) RestoreSingleItem(
|
|||||||
func restoreSingleItem(
|
func restoreSingleItem(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
f fs.File,
|
f fs.File,
|
||||||
itemPath []string,
|
itemDir path.Path,
|
||||||
) (data.Collection, error) {
|
) (data.Collection, error) {
|
||||||
r, err := f.Open(ctx)
|
r, err := f.Open(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -509,7 +526,7 @@ func restoreSingleItem(
|
|||||||
reader: r,
|
reader: r,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
path: itemPath,
|
path: itemDir,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path"
|
stdpath "path"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/connector/mockconnector"
|
"github.com/alcionai/corso/src/internal/connector/mockconnector"
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
"github.com/alcionai/corso/src/internal/kopia/mockkopia"
|
"github.com/alcionai/corso/src/internal/kopia/mockkopia"
|
||||||
|
"github.com/alcionai/corso/src/internal/path"
|
||||||
"github.com/alcionai/corso/src/internal/tester"
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||||
)
|
)
|
||||||
@ -26,7 +27,6 @@ import (
|
|||||||
const (
|
const (
|
||||||
testTenant = "a-tenant"
|
testTenant = "a-tenant"
|
||||||
testUser = "user1"
|
testUser = "user1"
|
||||||
testEmailDir = "email"
|
|
||||||
testInboxDir = "inbox"
|
testInboxDir = "inbox"
|
||||||
testArchiveDir = "archive"
|
testArchiveDir = "archive"
|
||||||
testFileName = "file1"
|
testFileName = "file1"
|
||||||
@ -38,8 +38,21 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
testPath = []string{testTenant, testUser, testEmailDir, testInboxDir}
|
testEmailDir = path.EmailCategory.String()
|
||||||
testPath2 = []string{testTenant, testUser, testEmailDir, testArchiveDir}
|
testPath = []string{
|
||||||
|
testTenant,
|
||||||
|
path.ExchangeService.String(),
|
||||||
|
testUser,
|
||||||
|
path.EmailCategory.String(),
|
||||||
|
testInboxDir,
|
||||||
|
}
|
||||||
|
testPath2 = []string{
|
||||||
|
testTenant,
|
||||||
|
path.ExchangeService.String(),
|
||||||
|
testUser,
|
||||||
|
path.EmailCategory.String(),
|
||||||
|
testArchiveDir,
|
||||||
|
}
|
||||||
testFileData = []byte("abcdefghijklmnopqrstuvwxyz")
|
testFileData = []byte("abcdefghijklmnopqrstuvwxyz")
|
||||||
testFileData2 = []byte("zyxwvutsrqponmlkjihgfedcba")
|
testFileData2 = []byte("zyxwvutsrqponmlkjihgfedcba")
|
||||||
testFileData3 = []byte("foo")
|
testFileData3 = []byte("foo")
|
||||||
@ -71,7 +84,7 @@ func testForFiles(
|
|||||||
for s := range c.Items() {
|
for s := range c.Items() {
|
||||||
count++
|
count++
|
||||||
|
|
||||||
fullPath := path.Join(append(c.FullPath(), s.UUID())...)
|
fullPath := stdpath.Join(append(c.FullPath(), s.UUID())...)
|
||||||
|
|
||||||
expected, ok := expected[fullPath]
|
expected, ok := expected[fullPath]
|
||||||
require.True(t, ok, "unexpected file with path %q", fullPath)
|
require.True(t, ok, "unexpected file with path %q", fullPath)
|
||||||
@ -503,8 +516,20 @@ func (suite *KopiaIntegrationSuite) TestRestoreAfterCompressionChange() {
|
|||||||
w := &Wrapper{k}
|
w := &Wrapper{k}
|
||||||
|
|
||||||
tid := uuid.NewString()
|
tid := uuid.NewString()
|
||||||
p1 := []string{tid, "uid", "emails", "fid"}
|
p1 := []string{
|
||||||
p2 := []string{tid, "uid2", "emails", "fid"}
|
tid,
|
||||||
|
path.ExchangeService.String(),
|
||||||
|
"uid",
|
||||||
|
path.EmailCategory.String(),
|
||||||
|
"fid",
|
||||||
|
}
|
||||||
|
p2 := []string{
|
||||||
|
tid,
|
||||||
|
path.ExchangeService.String(),
|
||||||
|
"uid2",
|
||||||
|
path.EmailCategory.String(),
|
||||||
|
"fid",
|
||||||
|
}
|
||||||
dc1 := mockconnector.NewMockExchangeCollection(p1, 1)
|
dc1 := mockconnector.NewMockExchangeCollection(p1, 1)
|
||||||
dc2 := mockconnector.NewMockExchangeCollection(p2, 1)
|
dc2 := mockconnector.NewMockExchangeCollection(p2, 1)
|
||||||
|
|
||||||
@ -517,8 +542,8 @@ func (suite *KopiaIntegrationSuite) TestRestoreAfterCompressionChange() {
|
|||||||
require.NoError(t, k.Compression(ctx, "gzip"))
|
require.NoError(t, k.Compression(ctx, "gzip"))
|
||||||
|
|
||||||
expected := map[string][]byte{
|
expected := map[string][]byte{
|
||||||
path.Join(fp1...): dc1.Data[0],
|
stdpath.Join(fp1...): dc1.Data[0],
|
||||||
path.Join(fp2...): dc2.Data[0],
|
stdpath.Join(fp2...): dc2.Data[0],
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := w.RestoreMultipleItems(
|
result, err := w.RestoreMultipleItems(
|
||||||
@ -534,10 +559,29 @@ func (suite *KopiaIntegrationSuite) TestRestoreAfterCompressionChange() {
|
|||||||
|
|
||||||
func (suite *KopiaIntegrationSuite) TestBackupCollections_ReaderError() {
|
func (suite *KopiaIntegrationSuite) TestBackupCollections_ReaderError() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
|
tmpBuilder := path.Builder{}.Append(testInboxDir)
|
||||||
|
|
||||||
|
p1, err := tmpBuilder.ToDataLayerExchangePathForCategory(
|
||||||
|
testTenant,
|
||||||
|
testUser,
|
||||||
|
path.EmailCategory,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tmpBuilder = path.Builder{}.Append(testArchiveDir)
|
||||||
|
|
||||||
|
p2, err := tmpBuilder.ToDataLayerExchangePathForCategory(
|
||||||
|
testTenant,
|
||||||
|
testUser,
|
||||||
|
path.EmailCategory,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
collections := []data.Collection{
|
collections := []data.Collection{
|
||||||
&kopiaDataCollection{
|
&kopiaDataCollection{
|
||||||
path: testPath,
|
path: p1,
|
||||||
streams: []data.Stream{
|
streams: []data.Stream{
|
||||||
&mockconnector.MockExchangeData{
|
&mockconnector.MockExchangeData{
|
||||||
ID: testFileName,
|
ID: testFileName,
|
||||||
@ -550,7 +594,7 @@ func (suite *KopiaIntegrationSuite) TestBackupCollections_ReaderError() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
&kopiaDataCollection{
|
&kopiaDataCollection{
|
||||||
path: testPath2,
|
path: p2,
|
||||||
streams: []data.Stream{
|
streams: []data.Stream{
|
||||||
&mockconnector.MockExchangeData{
|
&mockconnector.MockExchangeData{
|
||||||
ID: testFileName3,
|
ID: testFileName3,
|
||||||
@ -577,7 +621,7 @@ func (suite *KopiaIntegrationSuite) TestBackupCollections_ReaderError() {
|
|||||||
|
|
||||||
assert.Equal(t, 0, stats.ErrorCount)
|
assert.Equal(t, 0, stats.ErrorCount)
|
||||||
assert.Equal(t, 5, stats.TotalFileCount)
|
assert.Equal(t, 5, stats.TotalFileCount)
|
||||||
assert.Equal(t, 5, stats.TotalDirectoryCount)
|
assert.Equal(t, 6, stats.TotalDirectoryCount)
|
||||||
assert.Equal(t, 1, stats.IgnoredErrorCount)
|
assert.Equal(t, 1, stats.IgnoredErrorCount)
|
||||||
assert.False(t, stats.Incomplete)
|
assert.False(t, stats.Incomplete)
|
||||||
assert.Len(t, rp.Entries, 5)
|
assert.Len(t, rp.Entries, 5)
|
||||||
@ -616,10 +660,29 @@ func (suite *KopiaSimpleRepoIntegrationSuite) SetupTest() {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
suite.w = &Wrapper{c}
|
suite.w = &Wrapper{c}
|
||||||
|
tmpBuilder := path.Builder{}.Append(testInboxDir)
|
||||||
|
|
||||||
|
p1, err := tmpBuilder.ToDataLayerExchangePathForCategory(
|
||||||
|
testTenant,
|
||||||
|
testUser,
|
||||||
|
path.EmailCategory,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tmpBuilder = path.Builder{}.Append(testArchiveDir)
|
||||||
|
|
||||||
|
p2, err := tmpBuilder.ToDataLayerExchangePathForCategory(
|
||||||
|
testTenant,
|
||||||
|
testUser,
|
||||||
|
path.EmailCategory,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
collections := []data.Collection{
|
collections := []data.Collection{
|
||||||
&kopiaDataCollection{
|
&kopiaDataCollection{
|
||||||
path: testPath,
|
path: p1,
|
||||||
streams: []data.Stream{
|
streams: []data.Stream{
|
||||||
&mockconnector.MockExchangeData{
|
&mockconnector.MockExchangeData{
|
||||||
ID: testFileName,
|
ID: testFileName,
|
||||||
@ -632,7 +695,7 @@ func (suite *KopiaSimpleRepoIntegrationSuite) SetupTest() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
&kopiaDataCollection{
|
&kopiaDataCollection{
|
||||||
path: testPath2,
|
path: p2,
|
||||||
streams: []data.Stream{
|
streams: []data.Stream{
|
||||||
&mockconnector.MockExchangeData{
|
&mockconnector.MockExchangeData{
|
||||||
ID: testFileName3,
|
ID: testFileName3,
|
||||||
@ -658,7 +721,7 @@ func (suite *KopiaSimpleRepoIntegrationSuite) SetupTest() {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, stats.ErrorCount, 0)
|
require.Equal(t, stats.ErrorCount, 0)
|
||||||
require.Equal(t, stats.TotalFileCount, 6)
|
require.Equal(t, stats.TotalFileCount, 6)
|
||||||
require.Equal(t, stats.TotalDirectoryCount, 5)
|
require.Equal(t, stats.TotalDirectoryCount, 6)
|
||||||
require.Equal(t, stats.IgnoredErrorCount, 0)
|
require.Equal(t, stats.IgnoredErrorCount, 0)
|
||||||
require.False(t, stats.Incomplete)
|
require.False(t, stats.Incomplete)
|
||||||
assert.Len(t, rp.Entries, 6)
|
assert.Len(t, rp.Entries, 6)
|
||||||
@ -667,14 +730,14 @@ func (suite *KopiaSimpleRepoIntegrationSuite) SetupTest() {
|
|||||||
|
|
||||||
// path.Join doesn't like (testPath..., testFileName).
|
// path.Join doesn't like (testPath..., testFileName).
|
||||||
suite.inboxExpectedFiles = map[string][]byte{
|
suite.inboxExpectedFiles = map[string][]byte{
|
||||||
path.Join(append(testPath, testFileName)...): testFileData,
|
stdpath.Join(append(testPath, testFileName)...): testFileData,
|
||||||
path.Join(append(testPath, testFileName2)...): testFileData2,
|
stdpath.Join(append(testPath, testFileName2)...): testFileData2,
|
||||||
}
|
}
|
||||||
suite.archiveExpectedFiles = map[string][]byte{
|
suite.archiveExpectedFiles = map[string][]byte{
|
||||||
path.Join(append(testPath2, testFileName3)...): testFileData3,
|
stdpath.Join(append(testPath2, testFileName3)...): testFileData3,
|
||||||
path.Join(append(testPath2, testFileName4)...): testFileData4,
|
stdpath.Join(append(testPath2, testFileName4)...): testFileData4,
|
||||||
path.Join(append(testPath2, testFileName5)...): testFileData5,
|
stdpath.Join(append(testPath2, testFileName5)...): testFileData5,
|
||||||
path.Join(append(testPath2, testFileName6)...): testFileData6,
|
stdpath.Join(append(testPath2, testFileName6)...): testFileData6,
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.allExpectedFiles = map[string][]byte{}
|
suite.allExpectedFiles = map[string][]byte{}
|
||||||
@ -768,8 +831,20 @@ func (suite *KopiaSimpleRepoIntegrationSuite) TestRestoreMultipleItems() {
|
|||||||
w := &Wrapper{k}
|
w := &Wrapper{k}
|
||||||
|
|
||||||
tid := uuid.NewString()
|
tid := uuid.NewString()
|
||||||
p1 := []string{tid, "uid", "emails", "fid"}
|
p1 := []string{
|
||||||
p2 := []string{tid, "uid2", "emails", "fid"}
|
tid,
|
||||||
|
path.ExchangeService.String(),
|
||||||
|
"uid",
|
||||||
|
path.EmailCategory.String(),
|
||||||
|
"fid",
|
||||||
|
}
|
||||||
|
p2 := []string{
|
||||||
|
tid,
|
||||||
|
path.ExchangeService.String(),
|
||||||
|
"uid2",
|
||||||
|
path.EmailCategory.String(),
|
||||||
|
"fid",
|
||||||
|
}
|
||||||
dc1 := mockconnector.NewMockExchangeCollection(p1, 1)
|
dc1 := mockconnector.NewMockExchangeCollection(p1, 1)
|
||||||
dc2 := mockconnector.NewMockExchangeCollection(p2, 1)
|
dc2 := mockconnector.NewMockExchangeCollection(p2, 1)
|
||||||
|
|
||||||
@ -780,8 +855,8 @@ func (suite *KopiaSimpleRepoIntegrationSuite) TestRestoreMultipleItems() {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
expected := map[string][]byte{
|
expected := map[string][]byte{
|
||||||
path.Join(fp1...): dc1.Data[0],
|
stdpath.Join(fp1...): dc1.Data[0],
|
||||||
path.Join(fp2...): dc2.Data[0],
|
stdpath.Join(fp2...): dc2.Data[0],
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := w.RestoreMultipleItems(
|
result, err := w.RestoreMultipleItems(
|
||||||
|
|||||||
@ -66,6 +66,14 @@ type Path interface {
|
|||||||
ResourceOwner() string
|
ResourceOwner() string
|
||||||
Folder() string
|
Folder() string
|
||||||
Item() string
|
Item() string
|
||||||
|
// PopFront returns a Builder object with the first element (left-side)
|
||||||
|
// removed. As the resulting set of elements is no longer a valid resource
|
||||||
|
// path a Builder is returned instead.
|
||||||
|
PopFront() *Builder
|
||||||
|
// Dir returns a Path object with the right-most element removed if possible.
|
||||||
|
// If removing the right-most element would discard one of the required prefix
|
||||||
|
// elements then an error is returned.
|
||||||
|
Dir() (Path, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Builder is a simple path representation that only tracks path elements. It
|
// Builder is a simple path representation that only tracks path elements. It
|
||||||
@ -136,6 +144,30 @@ func (pb *Builder) appendElements(escaped bool, elements []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pb Builder) PopFront() *Builder {
|
||||||
|
if len(pb.elements) <= 1 {
|
||||||
|
return &Builder{}
|
||||||
|
}
|
||||||
|
|
||||||
|
elements := make([]string, len(pb.elements)-1)
|
||||||
|
copy(elements, pb.elements[1:])
|
||||||
|
|
||||||
|
return &Builder{
|
||||||
|
elements: elements,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pb Builder) dir() *Builder {
|
||||||
|
if len(pb.elements) <= 1 {
|
||||||
|
return &Builder{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Builder{
|
||||||
|
// Safe to use the same elements because Builders are immutable.
|
||||||
|
elements: pb.elements[:len(pb.elements)-1],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// String returns a string that contains all path elements joined together.
|
// String returns a string that contains all path elements joined together.
|
||||||
// Elements of the path that need escaping are escaped.
|
// Elements of the path that need escaping are escaped.
|
||||||
func (pb Builder) String() string {
|
func (pb Builder) String() string {
|
||||||
@ -148,6 +180,13 @@ func (pb Builder) String() string {
|
|||||||
return join(escaped)
|
return join(escaped)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Elements returns all the elements in the path. This is a temporary function
|
||||||
|
// and will likely be updated to handle encoded elements instead of clear-text
|
||||||
|
// elements in the future.
|
||||||
|
func (pb Builder) Elements() []string {
|
||||||
|
return append([]string{}, pb.elements...)
|
||||||
|
}
|
||||||
|
|
||||||
//nolint:unused
|
//nolint:unused
|
||||||
func (pb Builder) join(start, end int) string {
|
func (pb Builder) join(start, end int) string {
|
||||||
return join(pb.elements[start:end])
|
return join(pb.elements[start:end])
|
||||||
|
|||||||
@ -294,6 +294,77 @@ func (suite *PathUnitSuite) TestTrailingEscapeChar() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *PathUnitSuite) TestElements() {
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
input []string
|
||||||
|
output []string
|
||||||
|
pathFunc func(elements []string) (*Builder, error)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "SimpleEscapedPath",
|
||||||
|
input: []string{"this", "is", "a", "path"},
|
||||||
|
output: []string{"this", "is", "a", "path"},
|
||||||
|
pathFunc: func(elements []string) (*Builder, error) {
|
||||||
|
return Builder{}.UnescapeAndAppend(elements...)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SimpleUnescapedPath",
|
||||||
|
input: []string{"this", "is", "a", "path"},
|
||||||
|
output: []string{"this", "is", "a", "path"},
|
||||||
|
pathFunc: func(elements []string) (*Builder, error) {
|
||||||
|
return Builder{}.Append(elements...), nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EscapedPath",
|
||||||
|
input: []string{"this", `is\/`, "a", "path"},
|
||||||
|
output: []string{"this", "is/", "a", "path"},
|
||||||
|
pathFunc: func(elements []string) (*Builder, error) {
|
||||||
|
return Builder{}.UnescapeAndAppend(elements...)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.T().Run(test.name, func(t *testing.T) {
|
||||||
|
p, err := test.pathFunc(test.input)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, test.output, p.Elements())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PathUnitSuite) TestPopFront() {
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
base *Builder
|
||||||
|
expectedString string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Empty",
|
||||||
|
base: &Builder{},
|
||||||
|
expectedString: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OneElement",
|
||||||
|
base: Builder{}.Append("something"),
|
||||||
|
expectedString: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TwoElements",
|
||||||
|
base: Builder{}.Append("something", "else"),
|
||||||
|
expectedString: "else",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.T().Run(test.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, test.expectedString, test.base.PopFront().String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *PathUnitSuite) TestFromStringErrors() {
|
func (suite *PathUnitSuite) TestFromStringErrors() {
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@ -135,6 +135,9 @@ func (rp dataLayerResourcePath) ResourceOwner() string {
|
|||||||
// Folder returns the folder segment embedded in the dataLayerResourcePath.
|
// Folder returns the folder segment embedded in the dataLayerResourcePath.
|
||||||
func (rp dataLayerResourcePath) Folder() string {
|
func (rp dataLayerResourcePath) Folder() string {
|
||||||
endIdx := len(rp.Builder.elements)
|
endIdx := len(rp.Builder.elements)
|
||||||
|
if endIdx == 4 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
if rp.hasItem {
|
if rp.hasItem {
|
||||||
endIdx--
|
endIdx--
|
||||||
@ -152,3 +155,16 @@ func (rp dataLayerResourcePath) Item() string {
|
|||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rp dataLayerResourcePath) Dir() (Path, error) {
|
||||||
|
if len(rp.elements) <= 4 {
|
||||||
|
return nil, errors.Errorf("unable to shorten path %q", rp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dataLayerResourcePath{
|
||||||
|
Builder: *rp.dir(),
|
||||||
|
service: rp.service,
|
||||||
|
category: rp.category,
|
||||||
|
hasItem: false,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package path_test
|
package path_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -139,6 +140,66 @@ func (suite *DataLayerResourcePath) TestMailItemNoFolder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *DataLayerResourcePath) TestPopFront() {
|
||||||
|
expected := path.Builder{}.Append(append(
|
||||||
|
[]string{path.ExchangeService.String(), testUser, path.EmailCategory.String()},
|
||||||
|
rest...,
|
||||||
|
)...)
|
||||||
|
|
||||||
|
for _, m := range modes {
|
||||||
|
suite.T().Run(m.name, func(t *testing.T) {
|
||||||
|
pb := path.Builder{}.Append(rest...)
|
||||||
|
p, err := pb.ToDataLayerExchangePathForCategory(
|
||||||
|
testTenant,
|
||||||
|
testUser,
|
||||||
|
path.EmailCategory,
|
||||||
|
m.isItem,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
b := p.PopFront()
|
||||||
|
assert.Equal(t, expected.String(), b.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *DataLayerResourcePath) TestDir() {
|
||||||
|
elements := []string{
|
||||||
|
testTenant,
|
||||||
|
path.ExchangeService.String(),
|
||||||
|
testUser,
|
||||||
|
path.EmailCategory.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range modes {
|
||||||
|
suite.T().Run(m.name, func(t1 *testing.T) {
|
||||||
|
pb := path.Builder{}.Append(rest...)
|
||||||
|
p, err := pb.ToDataLayerExchangePathForCategory(
|
||||||
|
testTenant,
|
||||||
|
testUser,
|
||||||
|
path.EmailCategory,
|
||||||
|
m.isItem,
|
||||||
|
)
|
||||||
|
require.NoError(t1, err)
|
||||||
|
|
||||||
|
for i := 1; i <= len(rest); i++ {
|
||||||
|
t1.Run(fmt.Sprintf("%v", i), func(t *testing.T) {
|
||||||
|
p, err = p.Dir()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := path.Builder{}.Append(elements...).Append(rest[:len(rest)-i]...)
|
||||||
|
assert.Equal(t, expected.String(), p.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t1.Run("All", func(t *testing.T) {
|
||||||
|
p, err = p.Dir()
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *DataLayerResourcePath) TestToExchangePathForCategory() {
|
func (suite *DataLayerResourcePath) TestToExchangePathForCategory() {
|
||||||
b := path.Builder{}.Append(rest...)
|
b := path.Builder{}.Append(rest...)
|
||||||
table := []struct {
|
table := []struct {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user