Select subtrees of data from base snapshots based on reason snapshot was picked (#1828)
## Description Pick what data to source from a base snapshot by examining the reason the snapshot was selected as a base. This helps avoid two issues: * pulling in unwanted data when the base has a superset of what is being backed up. Example: pulling in contacts from the base when only email is being backed up * clobbering already selected data from a different base with data in the base currently being examined. Example: two snapshots, one with contacts and emails and the other with just emails. Second snapshot is newer than the first. The email items in the first snapshot should not clobber those in the first when building the hierarchy This PR also has the effect of, under some conditions, reducing the amount of data that is pulled from the remote kopia repo when building the hierarchy. This occurs because only the subtrees that will be used in the new backup are traversed instead of traversing the entire snapshot ## Does this PR need a docs update or release note? - [ ] ✅ Yes, it's included - [ ] 🕐 Yes, but in a later PR - [x] ⛔ No ## Type of change <!--- Please check the type of change your PR introduces: ---> - [x] 🌻 Feature - [ ] 🐛 Bugfix - [ ] 🗺️ Documentation - [ ] 🤖 Test - [ ] 💻 CI/Deployment - [ ] 🐹 Trivial/Minor ## Issue(s) * #1740 ## Test Plan - [ ] 💪 Manual - [x] ⚡ Unit test - [ ] 💚 E2E
This commit is contained in:
parent
da929d8448
commit
8c15c3ce16
@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
"github.com/kopia/kopia/fs"
|
"github.com/kopia/kopia/fs"
|
||||||
"github.com/kopia/kopia/fs/virtualfs"
|
"github.com/kopia/kopia/fs/virtualfs"
|
||||||
"github.com/kopia/kopia/snapshot"
|
|
||||||
"github.com/kopia/kopia/snapshot/snapshotfs"
|
"github.com/kopia/kopia/snapshot/snapshotfs"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
@ -650,10 +649,33 @@ func traverseBaseDir(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(ashmrtn): We may want to move this to BackupOp and pass in
|
||||||
|
// (Manifest, path) to kopia.BackupCollections() instead of passing in
|
||||||
|
// ManifestEntry. That would keep kopia from having to know anything about how
|
||||||
|
// paths are formed. It would just encode/decode them and do basic manipulations
|
||||||
|
// like pushing/popping elements on a path based on location in the hierarchy.
|
||||||
|
func encodedElementsForPath(tenant string, r Reason) (*path.Builder, error) {
|
||||||
|
// This is hacky, but we want the path package to format the path the right
|
||||||
|
// way (e.x. proper order for service, category, etc), but we don't care about
|
||||||
|
// the folders after the prefix.
|
||||||
|
p, err := path.Builder{}.Append("tmp").ToDataLayerPath(
|
||||||
|
tenant,
|
||||||
|
r.ResourceOwner,
|
||||||
|
r.Service,
|
||||||
|
r.Category,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "building path")
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.ToBuilder().Dir(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func inflateBaseTree(
|
func inflateBaseTree(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
loader snapshotLoader,
|
loader snapshotLoader,
|
||||||
snap *snapshot.Manifest,
|
snap *ManifestEntry,
|
||||||
updatedPaths map[string]path.Path,
|
updatedPaths map[string]path.Path,
|
||||||
roots map[string]*treeMap,
|
roots map[string]*treeMap,
|
||||||
) error {
|
) error {
|
||||||
@ -664,7 +686,7 @@ func inflateBaseTree(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
root, err := loader.SnapshotRoot(snap)
|
root, err := loader.SnapshotRoot(snap.Manifest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "getting snapshot %s root directory", snap.ID)
|
return errors.Wrapf(err, "getting snapshot %s root directory", snap.ID)
|
||||||
}
|
}
|
||||||
@ -674,22 +696,47 @@ func inflateBaseTree(
|
|||||||
return errors.Errorf("snapshot %s root is not a directory", snap.ID)
|
return errors.Errorf("snapshot %s root is not a directory", snap.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(ashmrtn): We should actually only traverse a subtree of the snapshot
|
rootName, err := decodeElement(dir.Name())
|
||||||
// where the subtree corresponds to the "reason" this snapshot was chosen.
|
if err != nil {
|
||||||
// Doing so will avoid pulling in data for categories that should not be
|
return errors.Wrapf(err, "snapshot %s root has undecode-able name", snap.ID)
|
||||||
// included in the current backup or overwriting some entries with out-dated
|
}
|
||||||
// information.
|
|
||||||
|
|
||||||
if err = traverseBaseDir(
|
// For each subtree corresponding to the tuple
|
||||||
ctx,
|
// (resource owner, service, category) merge the directories in the base with
|
||||||
0,
|
// what has been reported in the collections we got.
|
||||||
updatedPaths,
|
for _, reason := range snap.Reasons {
|
||||||
&path.Builder{},
|
pb, err := encodedElementsForPath(rootName, reason)
|
||||||
&path.Builder{},
|
if err != nil {
|
||||||
dir,
|
return errors.Wrapf(err, "snapshot %s getting path elements", snap.ID)
|
||||||
roots,
|
}
|
||||||
); err != nil {
|
|
||||||
return errors.Wrapf(err, "traversing base snapshot %s", snap.ID)
|
// We're starting from the root directory so don't need it in the path.
|
||||||
|
pathElems := encodeElements(pb.PopFront().Elements()...)
|
||||||
|
|
||||||
|
ent, err := snapshotfs.GetNestedEntry(ctx, dir, pathElems)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "snapshot %s getting subtree root", snap.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
subtreeDir, ok := ent.(fs.Directory)
|
||||||
|
if !ok {
|
||||||
|
return errors.Wrapf(err, "snapshot %s subtree root is not directory", snap.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're assuming here that the prefix for the path has not changed (i.e.
|
||||||
|
// all of tenant, service, resource owner, and category are the same in the
|
||||||
|
// old snapshot (snap) and the snapshot we're currently trying to make.
|
||||||
|
if err = traverseBaseDir(
|
||||||
|
ctx,
|
||||||
|
0,
|
||||||
|
updatedPaths,
|
||||||
|
pb.Dir(),
|
||||||
|
pb.Dir(),
|
||||||
|
subtreeDir,
|
||||||
|
roots,
|
||||||
|
); err != nil {
|
||||||
|
return errors.Wrapf(err, "traversing base snapshot %s", snap.ID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -704,7 +751,7 @@ func inflateBaseTree(
|
|||||||
func inflateDirTree(
|
func inflateDirTree(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
loader snapshotLoader,
|
loader snapshotLoader,
|
||||||
baseSnaps []*snapshot.Manifest,
|
baseSnaps []*ManifestEntry,
|
||||||
collections []data.Collection,
|
collections []data.Collection,
|
||||||
progress *corsoProgress,
|
progress *corsoProgress,
|
||||||
) (fs.Directory, error) {
|
) (fs.Directory, error) {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/kopia/kopia/fs"
|
"github.com/kopia/kopia/fs"
|
||||||
"github.com/kopia/kopia/fs/virtualfs"
|
"github.com/kopia/kopia/fs/virtualfs"
|
||||||
|
"github.com/kopia/kopia/repo/manifest"
|
||||||
"github.com/kopia/kopia/snapshot"
|
"github.com/kopia/kopia/snapshot"
|
||||||
"github.com/kopia/kopia/snapshot/snapshotfs"
|
"github.com/kopia/kopia/snapshot/snapshotfs"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@ -31,30 +32,28 @@ func makePath(t *testing.T, elements []string) path.Path {
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
// baseWithChildren returns an fs.Entry hierarchy where the first four levels
|
// baseWithChildren returns an fs.Entry hierarchy where the first len(basic)
|
||||||
// are the encoded values of tenant, service, user, and category respectively.
|
// levels are the encoded values of basic in order. All items in children are
|
||||||
// All items in children are made a direct descendent of the category entry.
|
// used as the direct descendents of the final entry in basic.
|
||||||
func baseWithChildren(
|
func baseWithChildren(
|
||||||
tenant, service, user, category string,
|
basic []string,
|
||||||
children []fs.Entry,
|
children []fs.Entry,
|
||||||
) fs.Entry {
|
) fs.Entry {
|
||||||
|
if len(basic) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(basic) == 1 {
|
||||||
|
return virtualfs.NewStaticDirectory(
|
||||||
|
encodeElements(basic[0])[0],
|
||||||
|
children,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return virtualfs.NewStaticDirectory(
|
return virtualfs.NewStaticDirectory(
|
||||||
encodeElements(tenant)[0],
|
encodeElements(basic[0])[0],
|
||||||
[]fs.Entry{
|
[]fs.Entry{
|
||||||
virtualfs.NewStaticDirectory(
|
baseWithChildren(basic[1:], children),
|
||||||
encodeElements(service)[0],
|
|
||||||
[]fs.Entry{
|
|
||||||
virtualfs.NewStaticDirectory(
|
|
||||||
encodeElements(user)[0],
|
|
||||||
[]fs.Entry{
|
|
||||||
virtualfs.NewStaticDirectory(
|
|
||||||
encodeElements(category)[0],
|
|
||||||
children,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -66,29 +65,27 @@ type expectedNode struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// expectedTreeWithChildren returns an expectedNode hierarchy where the first
|
// expectedTreeWithChildren returns an expectedNode hierarchy where the first
|
||||||
// four levels are the tenant, service, user, and category respectively. All
|
// len(basic) levels are the values of basic in order. All items in children are
|
||||||
// items in children are made a direct descendent of the category node.
|
// made a direct descendent of the final entry in basic.
|
||||||
func expectedTreeWithChildren(
|
func expectedTreeWithChildren(
|
||||||
tenant, service, user, category string,
|
basic []string,
|
||||||
children []*expectedNode,
|
children []*expectedNode,
|
||||||
) *expectedNode {
|
) *expectedNode {
|
||||||
|
if len(basic) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(basic) == 1 {
|
||||||
|
return &expectedNode{
|
||||||
|
name: basic[0],
|
||||||
|
children: children,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &expectedNode{
|
return &expectedNode{
|
||||||
name: tenant,
|
name: basic[0],
|
||||||
children: []*expectedNode{
|
children: []*expectedNode{
|
||||||
{
|
expectedTreeWithChildren(basic[1:], children),
|
||||||
name: service,
|
|
||||||
children: []*expectedNode{
|
|
||||||
{
|
|
||||||
name: user,
|
|
||||||
children: []*expectedNode{
|
|
||||||
{
|
|
||||||
name: category,
|
|
||||||
children: children,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -802,6 +799,25 @@ func (msw *mockSnapshotWalker) SnapshotRoot(*snapshot.Manifest) (fs.Entry, error
|
|||||||
return msw.snapshotRoot, nil
|
return msw.snapshotRoot, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mockSnapshotEntry(
|
||||||
|
id, resourceOwner string,
|
||||||
|
service path.ServiceType,
|
||||||
|
category path.CategoryType,
|
||||||
|
) *ManifestEntry {
|
||||||
|
return &ManifestEntry{
|
||||||
|
Manifest: &snapshot.Manifest{
|
||||||
|
ID: manifest.ID(id),
|
||||||
|
},
|
||||||
|
Reasons: []Reason{
|
||||||
|
{
|
||||||
|
ResourceOwner: resourceOwner,
|
||||||
|
Service: service,
|
||||||
|
Category: category,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSingleSubtree() {
|
func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSingleSubtree() {
|
||||||
dirPath := makePath(
|
dirPath := makePath(
|
||||||
suite.T(),
|
suite.T(),
|
||||||
@ -812,10 +828,12 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSingleSubtree() {
|
|||||||
// can only return its Reader once.
|
// can only return its Reader once.
|
||||||
getBaseSnapshot := func() fs.Entry {
|
getBaseSnapshot := func() fs.Entry {
|
||||||
return baseWithChildren(
|
return baseWithChildren(
|
||||||
testTenant,
|
[]string{
|
||||||
service,
|
testTenant,
|
||||||
testUser,
|
service,
|
||||||
category,
|
testUser,
|
||||||
|
category,
|
||||||
|
},
|
||||||
[]fs.Entry{
|
[]fs.Entry{
|
||||||
virtualfs.NewStaticDirectory(
|
virtualfs.NewStaticDirectory(
|
||||||
encodeElements(testInboxDir)[0],
|
encodeElements(testInboxDir)[0],
|
||||||
@ -846,10 +864,12 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSingleSubtree() {
|
|||||||
return []data.Collection{mc}
|
return []data.Collection{mc}
|
||||||
},
|
},
|
||||||
expected: expectedTreeWithChildren(
|
expected: expectedTreeWithChildren(
|
||||||
testTenant,
|
[]string{
|
||||||
service,
|
testTenant,
|
||||||
testUser,
|
service,
|
||||||
category,
|
testUser,
|
||||||
|
category,
|
||||||
|
},
|
||||||
[]*expectedNode{
|
[]*expectedNode{
|
||||||
{
|
{
|
||||||
name: testInboxDir,
|
name: testInboxDir,
|
||||||
@ -869,10 +889,12 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSingleSubtree() {
|
|||||||
return []data.Collection{mc}
|
return []data.Collection{mc}
|
||||||
},
|
},
|
||||||
expected: expectedTreeWithChildren(
|
expected: expectedTreeWithChildren(
|
||||||
testTenant,
|
[]string{
|
||||||
service,
|
testTenant,
|
||||||
testUser,
|
service,
|
||||||
category,
|
testUser,
|
||||||
|
category,
|
||||||
|
},
|
||||||
[]*expectedNode{
|
[]*expectedNode{
|
||||||
{
|
{
|
||||||
name: testInboxDir,
|
name: testInboxDir,
|
||||||
@ -902,10 +924,12 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSingleSubtree() {
|
|||||||
return []data.Collection{mc}
|
return []data.Collection{mc}
|
||||||
},
|
},
|
||||||
expected: expectedTreeWithChildren(
|
expected: expectedTreeWithChildren(
|
||||||
testTenant,
|
[]string{
|
||||||
service,
|
testTenant,
|
||||||
testUser,
|
service,
|
||||||
category,
|
testUser,
|
||||||
|
category,
|
||||||
|
},
|
||||||
[]*expectedNode{
|
[]*expectedNode{
|
||||||
{
|
{
|
||||||
name: testInboxDir,
|
name: testInboxDir,
|
||||||
@ -937,7 +961,9 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSingleSubtree() {
|
|||||||
dirTree, err := inflateDirTree(
|
dirTree, err := inflateDirTree(
|
||||||
ctx,
|
ctx,
|
||||||
msw,
|
msw,
|
||||||
[]*snapshot.Manifest{{}},
|
[]*ManifestEntry{
|
||||||
|
mockSnapshotEntry("", testUser, path.ExchangeService, path.EmailCategory),
|
||||||
|
},
|
||||||
test.inputCollections(),
|
test.inputCollections(),
|
||||||
progress,
|
progress,
|
||||||
)
|
)
|
||||||
@ -987,10 +1013,12 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeMultipleSubdirecto
|
|||||||
// - file3
|
// - file3
|
||||||
getBaseSnapshot := func() fs.Entry {
|
getBaseSnapshot := func() fs.Entry {
|
||||||
return baseWithChildren(
|
return baseWithChildren(
|
||||||
testTenant,
|
[]string{
|
||||||
service,
|
testTenant,
|
||||||
testUser,
|
service,
|
||||||
category,
|
testUser,
|
||||||
|
category,
|
||||||
|
},
|
||||||
[]fs.Entry{
|
[]fs.Entry{
|
||||||
virtualfs.NewStaticDirectory(
|
virtualfs.NewStaticDirectory(
|
||||||
encodeElements(testInboxDir)[0],
|
encodeElements(testInboxDir)[0],
|
||||||
@ -1046,10 +1074,12 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeMultipleSubdirecto
|
|||||||
return []data.Collection{mc}
|
return []data.Collection{mc}
|
||||||
},
|
},
|
||||||
expected: expectedTreeWithChildren(
|
expected: expectedTreeWithChildren(
|
||||||
testTenant,
|
[]string{
|
||||||
service,
|
testTenant,
|
||||||
testUser,
|
service,
|
||||||
category,
|
testUser,
|
||||||
|
category,
|
||||||
|
},
|
||||||
[]*expectedNode{
|
[]*expectedNode{
|
||||||
{
|
{
|
||||||
name: testInboxDir + "2",
|
name: testInboxDir + "2",
|
||||||
@ -1104,10 +1134,12 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeMultipleSubdirecto
|
|||||||
return []data.Collection{inbox, work}
|
return []data.Collection{inbox, work}
|
||||||
},
|
},
|
||||||
expected: expectedTreeWithChildren(
|
expected: expectedTreeWithChildren(
|
||||||
testTenant,
|
[]string{
|
||||||
service,
|
testTenant,
|
||||||
testUser,
|
service,
|
||||||
category,
|
testUser,
|
||||||
|
category,
|
||||||
|
},
|
||||||
[]*expectedNode{
|
[]*expectedNode{
|
||||||
{
|
{
|
||||||
name: testInboxDir + "2",
|
name: testInboxDir + "2",
|
||||||
@ -1158,10 +1190,12 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeMultipleSubdirecto
|
|||||||
return []data.Collection{inbox, work}
|
return []data.Collection{inbox, work}
|
||||||
},
|
},
|
||||||
expected: expectedTreeWithChildren(
|
expected: expectedTreeWithChildren(
|
||||||
testTenant,
|
[]string{
|
||||||
service,
|
testTenant,
|
||||||
testUser,
|
service,
|
||||||
category,
|
testUser,
|
||||||
|
category,
|
||||||
|
},
|
||||||
[]*expectedNode{
|
[]*expectedNode{
|
||||||
{
|
{
|
||||||
name: workDir,
|
name: workDir,
|
||||||
@ -1189,10 +1223,12 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeMultipleSubdirecto
|
|||||||
return []data.Collection{personal, work}
|
return []data.Collection{personal, work}
|
||||||
},
|
},
|
||||||
expected: expectedTreeWithChildren(
|
expected: expectedTreeWithChildren(
|
||||||
testTenant,
|
[]string{
|
||||||
service,
|
testTenant,
|
||||||
testUser,
|
service,
|
||||||
category,
|
testUser,
|
||||||
|
category,
|
||||||
|
},
|
||||||
[]*expectedNode{
|
[]*expectedNode{
|
||||||
{
|
{
|
||||||
name: testInboxDir,
|
name: testInboxDir,
|
||||||
@ -1229,10 +1265,12 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeMultipleSubdirecto
|
|||||||
return []data.Collection{personal, work}
|
return []data.Collection{personal, work}
|
||||||
},
|
},
|
||||||
expected: expectedTreeWithChildren(
|
expected: expectedTreeWithChildren(
|
||||||
testTenant,
|
[]string{
|
||||||
service,
|
testTenant,
|
||||||
testUser,
|
service,
|
||||||
category,
|
testUser,
|
||||||
|
category,
|
||||||
|
},
|
||||||
[]*expectedNode{
|
[]*expectedNode{
|
||||||
{
|
{
|
||||||
name: testInboxDir,
|
name: testInboxDir,
|
||||||
@ -1280,10 +1318,12 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeMultipleSubdirecto
|
|||||||
return []data.Collection{personal}
|
return []data.Collection{personal}
|
||||||
},
|
},
|
||||||
expected: expectedTreeWithChildren(
|
expected: expectedTreeWithChildren(
|
||||||
testTenant,
|
[]string{
|
||||||
service,
|
testTenant,
|
||||||
testUser,
|
service,
|
||||||
category,
|
testUser,
|
||||||
|
category,
|
||||||
|
},
|
||||||
[]*expectedNode{
|
[]*expectedNode{
|
||||||
{
|
{
|
||||||
name: testInboxDir,
|
name: testInboxDir,
|
||||||
@ -1335,7 +1375,9 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeMultipleSubdirecto
|
|||||||
dirTree, err := inflateDirTree(
|
dirTree, err := inflateDirTree(
|
||||||
ctx,
|
ctx,
|
||||||
msw,
|
msw,
|
||||||
[]*snapshot.Manifest{{}},
|
[]*ManifestEntry{
|
||||||
|
mockSnapshotEntry("", testUser, path.ExchangeService, path.EmailCategory),
|
||||||
|
},
|
||||||
test.inputCollections(t),
|
test.inputCollections(t),
|
||||||
progress,
|
progress,
|
||||||
)
|
)
|
||||||
@ -1375,10 +1417,12 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSkipsDeletedSubtre
|
|||||||
// - file4
|
// - file4
|
||||||
getBaseSnapshot := func() fs.Entry {
|
getBaseSnapshot := func() fs.Entry {
|
||||||
return baseWithChildren(
|
return baseWithChildren(
|
||||||
testTenant,
|
[]string{
|
||||||
service,
|
testTenant,
|
||||||
testUser,
|
service,
|
||||||
category,
|
testUser,
|
||||||
|
category,
|
||||||
|
},
|
||||||
[]fs.Entry{
|
[]fs.Entry{
|
||||||
virtualfs.NewStaticDirectory(
|
virtualfs.NewStaticDirectory(
|
||||||
encodeElements(testInboxDir)[0],
|
encodeElements(testInboxDir)[0],
|
||||||
@ -1435,10 +1479,12 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSkipsDeletedSubtre
|
|||||||
}
|
}
|
||||||
|
|
||||||
expected := expectedTreeWithChildren(
|
expected := expectedTreeWithChildren(
|
||||||
testTenant,
|
[]string{
|
||||||
service,
|
testTenant,
|
||||||
testUser,
|
service,
|
||||||
category,
|
testUser,
|
||||||
|
category,
|
||||||
|
},
|
||||||
[]*expectedNode{
|
[]*expectedNode{
|
||||||
{
|
{
|
||||||
name: testArchiveDir,
|
name: testArchiveDir,
|
||||||
@ -1489,7 +1535,254 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSkipsDeletedSubtre
|
|||||||
dirTree, err := inflateDirTree(
|
dirTree, err := inflateDirTree(
|
||||||
ctx,
|
ctx,
|
||||||
msw,
|
msw,
|
||||||
[]*snapshot.Manifest{{}},
|
[]*ManifestEntry{
|
||||||
|
mockSnapshotEntry("", testUser, path.ExchangeService, path.EmailCategory),
|
||||||
|
},
|
||||||
|
collections,
|
||||||
|
progress,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectTree(t, ctx, expected, dirTree)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockMultiSnapshotWalker struct {
|
||||||
|
snaps map[string]fs.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msw *mockMultiSnapshotWalker) SnapshotRoot(man *snapshot.Manifest) (fs.Entry, error) {
|
||||||
|
if snap := msw.snaps[string(man.ID)]; snap != nil {
|
||||||
|
return snap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("snapshot not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSelectsCorrectSubtrees() {
|
||||||
|
tester.LogTimeOfTest(suite.T())
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext()
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
const contactsDir = "contacts"
|
||||||
|
|
||||||
|
inboxPath := makePath(
|
||||||
|
suite.T(),
|
||||||
|
[]string{testTenant, service, testUser, category, testInboxDir},
|
||||||
|
)
|
||||||
|
|
||||||
|
inboxFileName1 := testFileName
|
||||||
|
inboxFileName2 := testFileName2
|
||||||
|
|
||||||
|
inboxFileData1 := testFileData
|
||||||
|
inboxFileData1v2 := testFileData5
|
||||||
|
inboxFileData2 := testFileData2
|
||||||
|
|
||||||
|
contactsFileName1 := testFileName3
|
||||||
|
contactsFileData1 := testFileData3
|
||||||
|
|
||||||
|
eventsFileName1 := testFileName5
|
||||||
|
eventsFileData1 := testFileData
|
||||||
|
|
||||||
|
// Must be a function that returns a new instance each time as StreamingFile
|
||||||
|
// can only return its Reader once.
|
||||||
|
// baseSnapshot with the following layout:
|
||||||
|
// - a-tenant
|
||||||
|
// - exchange
|
||||||
|
// - user1
|
||||||
|
// - email
|
||||||
|
// - Inbox
|
||||||
|
// - file1
|
||||||
|
// - contacts
|
||||||
|
// - contacts
|
||||||
|
// - file2
|
||||||
|
getBaseSnapshot1 := func() fs.Entry {
|
||||||
|
return baseWithChildren(
|
||||||
|
[]string{
|
||||||
|
testTenant,
|
||||||
|
service,
|
||||||
|
testUser,
|
||||||
|
},
|
||||||
|
[]fs.Entry{
|
||||||
|
virtualfs.NewStaticDirectory(
|
||||||
|
encodeElements(category)[0],
|
||||||
|
[]fs.Entry{
|
||||||
|
virtualfs.NewStaticDirectory(
|
||||||
|
encodeElements(testInboxDir)[0],
|
||||||
|
[]fs.Entry{
|
||||||
|
virtualfs.StreamingFileWithModTimeFromReader(
|
||||||
|
encodeElements(inboxFileName1)[0],
|
||||||
|
time.Time{},
|
||||||
|
bytes.NewReader(inboxFileData1),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
virtualfs.NewStaticDirectory(
|
||||||
|
encodeElements(path.ContactsCategory.String())[0],
|
||||||
|
[]fs.Entry{
|
||||||
|
virtualfs.NewStaticDirectory(
|
||||||
|
encodeElements(contactsDir)[0],
|
||||||
|
[]fs.Entry{
|
||||||
|
virtualfs.StreamingFileWithModTimeFromReader(
|
||||||
|
encodeElements(contactsFileName1)[0],
|
||||||
|
time.Time{},
|
||||||
|
bytes.NewReader(contactsFileData1),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must be a function that returns a new instance each time as StreamingFile
|
||||||
|
// can only return its Reader once.
|
||||||
|
// baseSnapshot with the following layout:
|
||||||
|
// - a-tenant
|
||||||
|
// - exchange
|
||||||
|
// - user1
|
||||||
|
// - email
|
||||||
|
// - Inbox
|
||||||
|
// - file1 <- has different data version
|
||||||
|
// - events
|
||||||
|
// - events
|
||||||
|
// - file3
|
||||||
|
getBaseSnapshot2 := func() fs.Entry {
|
||||||
|
return baseWithChildren(
|
||||||
|
[]string{
|
||||||
|
testTenant,
|
||||||
|
service,
|
||||||
|
testUser,
|
||||||
|
},
|
||||||
|
[]fs.Entry{
|
||||||
|
virtualfs.NewStaticDirectory(
|
||||||
|
encodeElements(category)[0],
|
||||||
|
[]fs.Entry{
|
||||||
|
virtualfs.NewStaticDirectory(
|
||||||
|
encodeElements(testInboxDir)[0],
|
||||||
|
[]fs.Entry{
|
||||||
|
virtualfs.StreamingFileWithModTimeFromReader(
|
||||||
|
encodeElements(inboxFileName1)[0],
|
||||||
|
time.Time{},
|
||||||
|
// Wrap with a backup reader so it gets the version injected.
|
||||||
|
newBackupStreamReader(
|
||||||
|
serializationVersion,
|
||||||
|
io.NopCloser(bytes.NewReader(inboxFileData1v2)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
virtualfs.NewStaticDirectory(
|
||||||
|
encodeElements(path.EventsCategory.String())[0],
|
||||||
|
[]fs.Entry{
|
||||||
|
virtualfs.NewStaticDirectory(
|
||||||
|
encodeElements("events")[0],
|
||||||
|
[]fs.Entry{
|
||||||
|
virtualfs.StreamingFileWithModTimeFromReader(
|
||||||
|
encodeElements(eventsFileName1)[0],
|
||||||
|
time.Time{},
|
||||||
|
bytes.NewReader(eventsFileData1),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the following:
|
||||||
|
// * contacts pulled from base1 unchanged even if no collections reference
|
||||||
|
// it
|
||||||
|
// * email pulled from base2
|
||||||
|
// * new email added
|
||||||
|
// * events not pulled from base2 as it's not listed as a Reason
|
||||||
|
//
|
||||||
|
// Expected output:
|
||||||
|
// - a-tenant
|
||||||
|
// - exchange
|
||||||
|
// - user1
|
||||||
|
// - email
|
||||||
|
// - Inbox
|
||||||
|
// - file1 <- version of data from second base
|
||||||
|
// - file2
|
||||||
|
// - contacts
|
||||||
|
// - contacts
|
||||||
|
// - file2
|
||||||
|
expected := expectedTreeWithChildren(
|
||||||
|
[]string{
|
||||||
|
testTenant,
|
||||||
|
service,
|
||||||
|
testUser,
|
||||||
|
},
|
||||||
|
[]*expectedNode{
|
||||||
|
{
|
||||||
|
name: category,
|
||||||
|
children: []*expectedNode{
|
||||||
|
{
|
||||||
|
name: testInboxDir,
|
||||||
|
children: []*expectedNode{
|
||||||
|
{
|
||||||
|
name: inboxFileName1,
|
||||||
|
children: []*expectedNode{},
|
||||||
|
data: inboxFileData1v2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: inboxFileName2,
|
||||||
|
children: []*expectedNode{},
|
||||||
|
data: inboxFileData2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: path.ContactsCategory.String(),
|
||||||
|
children: []*expectedNode{
|
||||||
|
{
|
||||||
|
name: contactsDir,
|
||||||
|
children: []*expectedNode{
|
||||||
|
{
|
||||||
|
name: contactsFileName1,
|
||||||
|
children: []*expectedNode{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
progress := &corsoProgress{pending: map[string]*itemDetails{}}
|
||||||
|
|
||||||
|
mc := mockconnector.NewMockExchangeCollection(inboxPath, 1)
|
||||||
|
mc.PrevPath = mc.FullPath()
|
||||||
|
mc.ColState = data.NotMovedState
|
||||||
|
mc.Names[0] = inboxFileName2
|
||||||
|
mc.Data[0] = inboxFileData2
|
||||||
|
|
||||||
|
msw := &mockMultiSnapshotWalker{
|
||||||
|
snaps: map[string]fs.Entry{
|
||||||
|
"id1": getBaseSnapshot1(),
|
||||||
|
"id2": getBaseSnapshot2(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
collections := []data.Collection{mc}
|
||||||
|
|
||||||
|
dirTree, err := inflateDirTree(
|
||||||
|
ctx,
|
||||||
|
msw,
|
||||||
|
[]*ManifestEntry{
|
||||||
|
mockSnapshotEntry("id1", testUser, path.ExchangeService, path.ContactsCategory),
|
||||||
|
mockSnapshotEntry("id2", testUser, path.ExchangeService, path.EmailCategory),
|
||||||
|
},
|
||||||
collections,
|
collections,
|
||||||
progress,
|
progress,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -360,12 +360,13 @@ func (pb Builder) ToServiceCategoryMetadataPath(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pb Builder) ToDataLayerExchangePathForCategory(
|
func (pb Builder) ToDataLayerPath(
|
||||||
tenant, user string,
|
tenant, user string,
|
||||||
|
service ServiceType,
|
||||||
category CategoryType,
|
category CategoryType,
|
||||||
isItem bool,
|
isItem bool,
|
||||||
) (Path, error) {
|
) (Path, error) {
|
||||||
if err := validateServiceAndCategory(ExchangeService, category); err != nil {
|
if err := validateServiceAndCategory(service, category); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,35 +377,29 @@ func (pb Builder) ToDataLayerExchangePathForCategory(
|
|||||||
return &dataLayerResourcePath{
|
return &dataLayerResourcePath{
|
||||||
Builder: *pb.withPrefix(
|
Builder: *pb.withPrefix(
|
||||||
tenant,
|
tenant,
|
||||||
ExchangeService.String(),
|
service.String(),
|
||||||
user,
|
user,
|
||||||
category.String(),
|
category.String(),
|
||||||
),
|
),
|
||||||
service: ExchangeService,
|
service: service,
|
||||||
category: category,
|
category: category,
|
||||||
hasItem: isItem,
|
hasItem: isItem,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pb Builder) ToDataLayerExchangePathForCategory(
|
||||||
|
tenant, user string,
|
||||||
|
category CategoryType,
|
||||||
|
isItem bool,
|
||||||
|
) (Path, error) {
|
||||||
|
return pb.ToDataLayerPath(tenant, user, ExchangeService, category, isItem)
|
||||||
|
}
|
||||||
|
|
||||||
func (pb Builder) ToDataLayerOneDrivePath(
|
func (pb Builder) ToDataLayerOneDrivePath(
|
||||||
tenant, user string,
|
tenant, user string,
|
||||||
isItem bool,
|
isItem bool,
|
||||||
) (Path, error) {
|
) (Path, error) {
|
||||||
if err := pb.verifyPrefix(tenant, user); err != nil {
|
return pb.ToDataLayerPath(tenant, user, OneDriveService, FilesCategory, isItem)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dataLayerResourcePath{
|
|
||||||
Builder: *pb.withPrefix(
|
|
||||||
tenant,
|
|
||||||
OneDriveService.String(),
|
|
||||||
user,
|
|
||||||
FilesCategory.String(),
|
|
||||||
),
|
|
||||||
service: OneDriveService,
|
|
||||||
category: FilesCategory,
|
|
||||||
hasItem: isItem,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pb Builder) ToDataLayerSharePointPath(
|
func (pb Builder) ToDataLayerSharePointPath(
|
||||||
@ -412,21 +407,7 @@ func (pb Builder) ToDataLayerSharePointPath(
|
|||||||
category CategoryType,
|
category CategoryType,
|
||||||
isItem bool,
|
isItem bool,
|
||||||
) (Path, error) {
|
) (Path, error) {
|
||||||
if err := pb.verifyPrefix(tenant, site); err != nil {
|
return pb.ToDataLayerPath(tenant, site, SharePointService, category, isItem)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dataLayerResourcePath{
|
|
||||||
Builder: *pb.withPrefix(
|
|
||||||
tenant,
|
|
||||||
SharePointService.String(),
|
|
||||||
site,
|
|
||||||
category.String(),
|
|
||||||
),
|
|
||||||
service: SharePointService,
|
|
||||||
category: category,
|
|
||||||
hasItem: isItem,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromDataLayerPath parses the escaped path p, validates the elements in p
|
// FromDataLayerPath parses the escaped path p, validates the elements in p
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user