consolidate aggregation of parent-item exclude map (#3258)

introduces a new type wrapping a nested map so that aggregation of globally excluded items in driveish services don't need to manage the map updates themselves.

---

#### Does this PR need a docs update or release note?

- [x]  No

#### Type of change

- [x] 🧹 Tech Debt/Cleanup

#### Issue(s)

* #2340

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Keepers 2023-05-04 13:32:47 -06:00 committed by GitHub
parent 9b21699b6b
commit 3a2d0876dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 515 additions and 171 deletions

View File

@ -0,0 +1,44 @@
package mock
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
)
var _ prefixmatcher.StringSetReader = &PrefixMap{}
type PrefixMap struct {
prefixmatcher.StringSetBuilder
}
func NewPrefixMap(m map[string]map[string]struct{}) *PrefixMap {
r := PrefixMap{StringSetBuilder: prefixmatcher.NewMatcher[map[string]struct{}]()}
for k, v := range m {
r.Add(k, v)
}
return &r
}
func (pm PrefixMap) AssertEqual(t *testing.T, r prefixmatcher.StringSetReader) {
if pm.Empty() {
require.True(t, r.Empty(), "both prefix maps are empty")
return
}
pks := pm.Keys()
rks := r.Keys()
assert.ElementsMatch(t, pks, rks, "prefix keys match")
for _, pk := range pks {
p, _ := pm.Get(pk)
r, _ := r.Get(pk)
assert.Equal(t, p, r, "values match")
}
}

View File

@ -2,28 +2,48 @@ package prefixmatcher
import ( import (
"strings" "strings"
"golang.org/x/exp/maps"
) )
type View[T any] interface { type Reader[T any] interface {
Get(key string) (T, bool) Get(key string) (T, bool)
LongestPrefix(key string) (string, T, bool) LongestPrefix(key string) (string, T, bool)
Empty() bool Empty() bool
Keys() []string
} }
type Matcher[T any] interface { type Builder[T any] interface {
// Add adds or updates the item with key to have value value. // Add adds or updates the item with key to have value value.
Add(key string, value T) Add(key string, value T)
View[T] Reader[T]
} }
// ---------------------------------------------------------------------------
// Implementation
// ---------------------------------------------------------------------------
// prefixMatcher implements Builder
type prefixMatcher[T any] struct { type prefixMatcher[T any] struct {
data map[string]T data map[string]T
} }
func (m *prefixMatcher[T]) Add(key string, value T) { func NewMatcher[T any]() Builder[T] {
m.data[key] = value return &prefixMatcher[T]{
data: map[string]T{},
}
} }
func NopReader[T any]() *prefixMatcher[T] {
return &prefixMatcher[T]{
data: make(map[string]T),
}
}
func (m *prefixMatcher[T]) Add(key string, value T) { m.data[key] = value }
func (m prefixMatcher[T]) Empty() bool { return len(m.data) == 0 }
func (m prefixMatcher[T]) Keys() []string { return maps.Keys(m.data) }
func (m *prefixMatcher[T]) Get(key string) (T, bool) { func (m *prefixMatcher[T]) Get(key string) (T, bool) {
if m == nil { if m == nil {
return *new(T), false return *new(T), false
@ -58,11 +78,3 @@ func (m *prefixMatcher[T]) LongestPrefix(key string) (string, T, bool) {
return rk, rv, found return rk, rv, found
} }
func (m prefixMatcher[T]) Empty() bool {
return len(m.data) == 0
}
func NewMatcher[T any]() Matcher[T] {
return &prefixMatcher[T]{data: map[string]T{}}
}

View File

@ -5,6 +5,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"golang.org/x/exp/maps"
"github.com/alcionai/corso/src/internal/common/prefixmatcher" "github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
@ -41,6 +42,8 @@ func (suite *PrefixMatcherUnitSuite) TestAdd_Get() {
assert.True(t, ok, "searching for key", k) assert.True(t, ok, "searching for key", k)
assert.Equal(t, v, val, "returned value") assert.Equal(t, v, val, "returned value")
} }
assert.ElementsMatch(t, maps.Keys(kvs), pm.Keys())
} }
func (suite *PrefixMatcherUnitSuite) TestLongestPrefix() { func (suite *PrefixMatcherUnitSuite) TestLongestPrefix() {

View File

@ -0,0 +1,122 @@
package prefixmatcher
import "golang.org/x/exp/maps"
// StringSetReader is a reader designed specifially to contain a set
// of string values (ie: Reader[map[string]struct{}]).
// This is a quality-of-life typecast for the generic Reader.
type StringSetReader interface {
Reader[map[string]struct{}]
}
// StringSetReader is a builder designed specifially to contain a set
// of string values (ie: Builder[map[string]struct{}]).
// This is a quality-of-life typecast for the generic Builder.
type StringSetBuilder interface {
Builder[map[string]struct{}]
}
// ---------------------------------------------------------------------------
// Implementation
// ---------------------------------------------------------------------------
var (
_ StringSetReader = &StringSetMatcher{}
_ StringSetBuilder = &StringSetMatchBuilder{}
)
// Items that should be excluded when sourcing data from the base backup.
// Parent Path -> item ID -> {}
type StringSetMatcher struct {
ssb StringSetBuilder
}
func (m *StringSetMatcher) LongestPrefix(parent string) (string, map[string]struct{}, bool) {
if m == nil {
return "", nil, false
}
return m.ssb.LongestPrefix(parent)
}
func (m *StringSetMatcher) Empty() bool {
return m == nil || m.ssb.Empty()
}
func (m *StringSetMatcher) Get(parent string) (map[string]struct{}, bool) {
if m == nil {
return nil, false
}
return m.ssb.Get(parent)
}
func (m *StringSetMatcher) Keys() []string {
if m == nil {
return []string{}
}
return m.ssb.Keys()
}
func (m *StringSetMatchBuilder) ToReader() *StringSetMatcher {
if m == nil {
return nil
}
return m.ssm
}
// Items that should be excluded when sourcing data from the base backup.
// Parent Path -> item ID -> {}
type StringSetMatchBuilder struct {
ssm *StringSetMatcher
}
func NewStringSetBuilder() *StringSetMatchBuilder {
return &StringSetMatchBuilder{
ssm: &StringSetMatcher{
ssb: NewMatcher[map[string]struct{}](),
},
}
}
// copies all items into the key's bucket.
func (m *StringSetMatchBuilder) Add(key string, items map[string]struct{}) {
if m == nil {
return
}
vs, ok := m.ssm.Get(key)
if !ok {
m.ssm.ssb.Add(key, items)
return
}
maps.Copy(vs, items)
m.ssm.ssb.Add(key, vs)
}
func (m *StringSetMatchBuilder) LongestPrefix(parent string) (string, map[string]struct{}, bool) {
return m.ssm.LongestPrefix(parent)
}
func (m *StringSetMatchBuilder) Empty() bool {
return m == nil || m.ssm.Empty()
}
func (m *StringSetMatchBuilder) Get(parent string) (map[string]struct{}, bool) {
if m == nil {
return nil, false
}
return m.ssm.Get(parent)
}
func (m *StringSetMatchBuilder) Keys() []string {
if m == nil {
return []string{}
}
return m.ssm.Keys()
}

View File

@ -0,0 +1,166 @@
package prefixmatcher_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"golang.org/x/exp/maps"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/tester"
)
type StringSetUnitSuite struct {
tester.Suite
}
func TestSTringSetUnitSuite(t *testing.T) {
suite.Run(t, &StringSetUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *StringSetUnitSuite) TestEmpty() {
pm := prefixmatcher.NewStringSetBuilder()
assert.True(suite.T(), pm.Empty())
}
func (suite *StringSetUnitSuite) TestToReader() {
var (
pr prefixmatcher.StringSetReader
t = suite.T()
pm = prefixmatcher.NewStringSetBuilder()
)
pr = pm.ToReader()
_, ok := pr.(prefixmatcher.StringSetBuilder)
assert.False(t, ok, "cannot cast to builder")
}
func (suite *StringSetUnitSuite) TestAdd_Get() {
t := suite.T()
pm := prefixmatcher.NewStringSetBuilder()
kvs := map[string]map[string]struct{}{
"hello": {"world": {}},
"hola": {"mundo": {}},
"foo": {"bar": {}},
}
for k, v := range kvs {
pm.Add(k, v)
}
for k, v := range kvs {
val, ok := pm.Get(k)
assert.True(t, ok, "searching for key", k)
assert.Equal(t, v, val, "returned value")
}
assert.ElementsMatch(t, maps.Keys(kvs), pm.Keys())
}
func (suite *StringSetUnitSuite) TestAdd_Union() {
t := suite.T()
pm := prefixmatcher.NewStringSetBuilder()
pm.Add("hello", map[string]struct{}{
"world": {},
"mundo": {},
})
pm.Add("hello", map[string]struct{}{
"goodbye": {},
"aideu": {},
})
expect := map[string]struct{}{
"world": {},
"mundo": {},
"goodbye": {},
"aideu": {},
}
result, _ := pm.Get("hello")
assert.Equal(t, expect, result)
assert.ElementsMatch(t, []string{"hello"}, pm.Keys())
}
func (suite *StringSetUnitSuite) TestLongestPrefix() {
key := "hello"
value := "world"
table := []struct {
name string
inputKVs map[string]map[string]struct{}
searchKey string
expectedKey string
expectedValue map[string]struct{}
expectedFound assert.BoolAssertionFunc
}{
{
name: "Empty Prefix",
inputKVs: map[string]map[string]struct{}{
"": {value: {}},
},
searchKey: key,
expectedKey: "",
expectedValue: map[string]struct{}{value: {}},
expectedFound: assert.True,
},
{
name: "Exact Match",
inputKVs: map[string]map[string]struct{}{
key: {value: {}},
},
searchKey: key,
expectedKey: key,
expectedValue: map[string]struct{}{value: {}},
expectedFound: assert.True,
},
{
name: "Prefix Match",
inputKVs: map[string]map[string]struct{}{
key[:len(key)-2]: {value: {}},
},
searchKey: key,
expectedKey: key[:len(key)-2],
expectedValue: map[string]struct{}{value: {}},
expectedFound: assert.True,
},
{
name: "Longest Prefix Match",
inputKVs: map[string]map[string]struct{}{
key[:len(key)-2]: {value: {}},
"": {value + "2": {}},
key[:len(key)-4]: {value + "3": {}},
},
searchKey: key,
expectedKey: key[:len(key)-2],
expectedValue: map[string]struct{}{value: {}},
expectedFound: assert.True,
},
{
name: "No Match",
inputKVs: map[string]map[string]struct{}{
"foo": {value: {}},
},
searchKey: key,
expectedKey: "",
expectedValue: nil,
expectedFound: assert.False,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
pm := prefixmatcher.NewStringSetBuilder()
for k, v := range test.inputKVs {
pm.Add(k, v)
}
k, v, ok := pm.LongestPrefix(test.searchKey)
assert.Equal(t, test.expectedKey, k, "key")
assert.Equal(t, test.expectedValue, v, "value")
test.expectedFound(t, ok, "found")
})
}
}

View File

@ -7,6 +7,7 @@ import (
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/connector/discovery" "github.com/alcionai/corso/src/internal/connector/discovery"
"github.com/alcionai/corso/src/internal/connector/exchange" "github.com/alcionai/corso/src/internal/connector/exchange"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
@ -41,7 +42,7 @@ func (gc *GraphConnector) ProduceBackupCollections(
lastBackupVersion int, lastBackupVersion int,
ctrlOpts control.Options, ctrlOpts control.Options,
errs *fault.Bus, errs *fault.Bus,
) ([]data.BackupCollection, map[string]map[string]struct{}, error) { ) ([]data.BackupCollection, prefixmatcher.StringSetReader, error) {
ctx, end := diagnostics.Span( ctx, end := diagnostics.Span(
ctx, ctx,
"gc:produceBackupCollections", "gc:produceBackupCollections",
@ -71,13 +72,13 @@ func (gc *GraphConnector) ProduceBackupCollections(
} }
var ( var (
colls []data.BackupCollection colls []data.BackupCollection
excludes map[string]map[string]struct{} ssmb *prefixmatcher.StringSetMatcher
) )
switch sels.Service { switch sels.Service {
case selectors.ServiceExchange: case selectors.ServiceExchange:
colls, excludes, err = exchange.DataCollections( colls, ssmb, err = exchange.DataCollections(
ctx, ctx,
sels, sels,
owner, owner,
@ -91,7 +92,7 @@ func (gc *GraphConnector) ProduceBackupCollections(
} }
case selectors.ServiceOneDrive: case selectors.ServiceOneDrive:
colls, excludes, err = onedrive.DataCollections( colls, ssmb, err = onedrive.DataCollections(
ctx, ctx,
sels, sels,
owner, owner,
@ -108,7 +109,7 @@ func (gc *GraphConnector) ProduceBackupCollections(
} }
case selectors.ServiceSharePoint: case selectors.ServiceSharePoint:
colls, excludes, err = sharepoint.DataCollections( colls, ssmb, err = sharepoint.DataCollections(
ctx, ctx,
gc.itemClient, gc.itemClient,
sels, sels,
@ -139,7 +140,7 @@ func (gc *GraphConnector) ProduceBackupCollections(
} }
} }
return colls, excludes, nil return colls, ssmb, nil
} }
func verifyBackupInputs(sels selectors.Selector, siteIDs []string) error { func verifyBackupInputs(sels selectors.Selector, siteIDs []string) error {

View File

@ -110,7 +110,7 @@ func (suite *DataCollectionIntgSuite) TestExchangeDataCollection() {
control.Defaults(), control.Defaults(),
fault.New(true)) fault.New(true))
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
assert.Empty(t, excludes) assert.True(t, excludes.Empty())
for range collections { for range collections {
connector.incrementAwaitingMessages() connector.incrementAwaitingMessages()
@ -215,7 +215,7 @@ func (suite *DataCollectionIntgSuite) TestDataCollections_invalidResourceOwner()
fault.New(true)) fault.New(true))
assert.Error(t, err, clues.ToCore(err)) assert.Error(t, err, clues.ToCore(err))
assert.Empty(t, collections) assert.Empty(t, collections)
assert.Empty(t, excludes) assert.Nil(t, excludes)
}) })
} }
} }
@ -272,7 +272,7 @@ func (suite *DataCollectionIntgSuite) TestSharePointDataCollection() {
fault.New(true)) fault.New(true))
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
// Not expecting excludes as this isn't an incremental backup. // Not expecting excludes as this isn't an incremental backup.
assert.Empty(t, excludes) assert.True(t, excludes.Empty())
for range collections { for range collections {
connector.incrementAwaitingMessages() connector.incrementAwaitingMessages()
@ -356,7 +356,7 @@ func (suite *SPCollectionIntgSuite) TestCreateSharePointCollection_Libraries() {
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
require.Len(t, cols, 2) // 1 collection, 1 path prefix directory to ensure the root path exists. require.Len(t, cols, 2) // 1 collection, 1 path prefix directory to ensure the root path exists.
// No excludes yet as this isn't an incremental backup. // No excludes yet as this isn't an incremental backup.
assert.Empty(t, excludes) assert.True(t, excludes.Empty())
t.Logf("cols[0] Path: %s\n", cols[0].FullPath().String()) t.Logf("cols[0] Path: %s\n", cols[0].FullPath().String())
assert.Equal( assert.Equal(
@ -401,7 +401,7 @@ func (suite *SPCollectionIntgSuite) TestCreateSharePointCollection_Lists() {
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
assert.Less(t, 0, len(cols)) assert.Less(t, 0, len(cols))
// No excludes yet as this isn't an incremental backup. // No excludes yet as this isn't an incremental backup.
assert.Empty(t, excludes) assert.True(t, excludes.Empty())
for _, collection := range cols { for _, collection := range cols {
t.Logf("Path: %s\n", collection.FullPath().String()) t.Logf("Path: %s\n", collection.FullPath().String())

View File

@ -7,6 +7,7 @@ import (
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/connector/exchange/api" "github.com/alcionai/corso/src/internal/connector/exchange/api"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
@ -170,7 +171,7 @@ func DataCollections(
su support.StatusUpdater, su support.StatusUpdater,
ctrlOpts control.Options, ctrlOpts control.Options,
errs *fault.Bus, errs *fault.Bus,
) ([]data.BackupCollection, map[string]map[string]struct{}, error) { ) ([]data.BackupCollection, *prefixmatcher.StringSetMatcher, error) {
eb, err := selector.ToExchangeBackup() eb, err := selector.ToExchangeBackup()
if err != nil { if err != nil {
return nil, nil, clues.Wrap(err, "exchange dataCollection selector").WithClues(ctx) return nil, nil, clues.Wrap(err, "exchange dataCollection selector").WithClues(ctx)

View File

@ -538,7 +538,7 @@ func runBackupAndCompare(
fault.New(true)) fault.New(true))
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
// No excludes yet because this isn't an incremental backup. // No excludes yet because this isn't an incremental backup.
assert.Empty(t, excludes) assert.True(t, excludes.Empty())
t.Logf("Backup enumeration complete in %v\n", time.Since(start)) t.Logf("Backup enumeration complete in %v\n", time.Since(start))
@ -1121,7 +1121,7 @@ func (suite *GraphConnectorIntegrationSuite) TestMultiFolderBackupDifferentNames
fault.New(true)) fault.New(true))
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
// No excludes yet because this isn't an incremental backup. // No excludes yet because this isn't an incremental backup.
assert.Empty(t, excludes) assert.True(t, excludes.Empty())
t.Log("Backup enumeration complete") t.Log("Backup enumeration complete")
@ -1280,7 +1280,7 @@ func (suite *GraphConnectorIntegrationSuite) TestBackup_CreatesPrefixCollections
fault.New(true)) fault.New(true))
require.NoError(t, err) require.NoError(t, err)
// No excludes yet because this isn't an incremental backup. // No excludes yet because this isn't an incremental backup.
assert.Empty(t, excludes) assert.True(t, excludes.Empty())
t.Logf("Backup enumeration complete in %v\n", time.Since(start)) t.Logf("Backup enumeration complete in %v\n", time.Since(start))

View File

@ -4,7 +4,9 @@ import (
"context" "context"
"github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/operations/inject"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
@ -12,9 +14,11 @@ import (
"github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/selectors"
) )
var _ inject.BackupProducer = &GraphConnector{}
type GraphConnector struct { type GraphConnector struct {
Collections []data.BackupCollection Collections []data.BackupCollection
Exclude map[string]map[string]struct{} Exclude *prefixmatcher.StringSetMatcher
Deets *details.Details Deets *details.Details
@ -33,7 +37,7 @@ func (gc GraphConnector) ProduceBackupCollections(
_ *fault.Bus, _ *fault.Bus,
) ( ) (
[]data.BackupCollection, []data.BackupCollection,
map[string]map[string]struct{}, prefixmatcher.StringSetReader,
error, error,
) { ) {
return gc.Collections, gc.Exclude, gc.Err return gc.Collections, gc.Exclude, gc.Err

View File

@ -12,6 +12,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/onedrive/api" "github.com/alcionai/corso/src/internal/connector/onedrive/api"
@ -271,11 +272,12 @@ func deserializeMap[T any](reader io.ReadCloser, alreadyFound map[string]T) erro
func (c *Collections) Get( func (c *Collections) Get(
ctx context.Context, ctx context.Context,
prevMetadata []data.RestoreCollection, prevMetadata []data.RestoreCollection,
ssmb *prefixmatcher.StringSetMatchBuilder,
errs *fault.Bus, errs *fault.Bus,
) ([]data.BackupCollection, map[string]map[string]struct{}, error) { ) ([]data.BackupCollection, error) {
prevDeltas, oldPathsByDriveID, err := deserializeMetadata(ctx, prevMetadata, errs) prevDeltas, oldPathsByDriveID, err := deserializeMetadata(ctx, prevMetadata, errs)
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
driveComplete, closer := observe.MessageWithCompletion(ctx, observe.Bulletf("files")) driveComplete, closer := observe.MessageWithCompletion(ctx, observe.Bulletf("files"))
@ -285,12 +287,12 @@ func (c *Collections) Get(
// Enumerate drives for the specified resourceOwner // Enumerate drives for the specified resourceOwner
pager, err := c.drivePagerFunc(c.source, c.service, c.resourceOwner, nil) pager, err := c.drivePagerFunc(c.source, c.service, c.resourceOwner, nil)
if err != nil { if err != nil {
return nil, nil, graph.Stack(ctx, err) return nil, graph.Stack(ctx, err)
} }
drives, err := api.GetAllDrives(ctx, pager, true, maxDrivesRetries) drives, err := api.GetAllDrives(ctx, pager, true, maxDrivesRetries)
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
var ( var (
@ -298,9 +300,6 @@ func (c *Collections) Get(
deltaURLs = map[string]string{} deltaURLs = map[string]string{}
// Drive ID -> folder ID -> folder path // Drive ID -> folder ID -> folder path
folderPaths = map[string]map[string]string{} folderPaths = map[string]map[string]string{}
// Items that should be excluded when sourcing data from the base backup.
// Parent Path -> item ID -> {}
excludedItems = map[string]map[string]struct{}{}
) )
for _, d := range drives { for _, d := range drives {
@ -336,7 +335,7 @@ func (c *Collections) Get(
prevDelta, prevDelta,
errs) errs)
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
// Used for logging below. // Used for logging below.
@ -376,19 +375,10 @@ func (c *Collections) Get(
c.resourceOwner, c.resourceOwner,
c.source) c.source)
if err != nil { if err != nil {
return nil, nil, return nil, clues.Wrap(err, "making exclude prefix").WithClues(ictx)
clues.Wrap(err, "making exclude prefix").WithClues(ictx)
} }
pstr := p.String() ssmb.Add(p.String(), excluded)
eidi, ok := excludedItems[pstr]
if !ok {
eidi = map[string]struct{}{}
}
maps.Copy(eidi, excluded)
excludedItems[pstr] = eidi
continue continue
} }
@ -413,7 +403,7 @@ func (c *Collections) Get(
prevPath, err := path.FromDataLayerPath(p, false) prevPath, err := path.FromDataLayerPath(p, false)
if err != nil { if err != nil {
err = clues.Wrap(err, "invalid previous path").WithClues(ictx).With("deleted_path", p) err = clues.Wrap(err, "invalid previous path").WithClues(ictx).With("deleted_path", p)
return nil, map[string]map[string]struct{}{}, err return nil, err
} }
col, err := NewCollection( col, err := NewCollection(
@ -428,7 +418,7 @@ func (c *Collections) Get(
CollectionScopeUnknown, CollectionScopeUnknown,
true) true)
if err != nil { if err != nil {
return nil, map[string]map[string]struct{}{}, clues.Wrap(err, "making collection").WithClues(ictx) return nil, clues.Wrap(err, "making collection").WithClues(ictx)
} }
c.CollectionMap[driveID][fldID] = col c.CollectionMap[driveID][fldID] = col
@ -468,7 +458,7 @@ func (c *Collections) Get(
} }
// TODO(ashmrtn): Track and return the set of items to exclude. // TODO(ashmrtn): Track and return the set of items to exclude.
return collections, excludedItems, nil return collections, nil
} }
func updateCollectionPaths( func updateCollectionPaths(

View File

@ -15,6 +15,8 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
pmMock "github.com/alcionai/corso/src/internal/common/prefixmatcher/mock"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
gapi "github.com/alcionai/corso/src/internal/connector/graph/api" gapi "github.com/alcionai/corso/src/internal/connector/graph/api"
"github.com/alcionai/corso/src/internal/connector/onedrive/api" "github.com/alcionai/corso/src/internal/connector/onedrive/api"
@ -1283,7 +1285,7 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
expectedCollections map[string]map[data.CollectionState][]string expectedCollections map[string]map[data.CollectionState][]string
expectedDeltaURLs map[string]string expectedDeltaURLs map[string]string
expectedFolderPaths map[string]map[string]string expectedFolderPaths map[string]map[string]string
expectedDelList map[string]map[string]struct{} expectedDelList *pmMock.PrefixMap
expectedSkippedCount int expectedSkippedCount int
doNotMergeItems bool doNotMergeItems bool
}{ }{
@ -1314,9 +1316,9 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
expectedFolderPaths: map[string]map[string]string{ expectedFolderPaths: map[string]map[string]string{
driveID1: {"root": rootFolderPath1}, driveID1: {"root": rootFolderPath1},
}, },
expectedDelList: map[string]map[string]struct{}{ expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{
rootFolderPath1: getDelList("file"), rootFolderPath1: getDelList("file"),
}, }),
}, },
{ {
name: "OneDrive_OneItemPage_NoFolders_NoErrors", name: "OneDrive_OneItemPage_NoFolders_NoErrors",
@ -1345,9 +1347,9 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
expectedFolderPaths: map[string]map[string]string{ expectedFolderPaths: map[string]map[string]string{
driveID1: {"root": rootFolderPath1}, driveID1: {"root": rootFolderPath1},
}, },
expectedDelList: map[string]map[string]struct{}{ expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{
rootFolderPath1: getDelList("file"), rootFolderPath1: getDelList("file"),
}, }),
}, },
{ {
name: "OneDrive_OneItemPage_NoErrors", name: "OneDrive_OneItemPage_NoErrors",
@ -1381,9 +1383,9 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"folder": folderPath1, "folder": folderPath1,
}, },
}, },
expectedDelList: map[string]map[string]struct{}{ expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{
rootFolderPath1: getDelList("file"), rootFolderPath1: getDelList("file"),
}, }),
}, },
{ {
name: "OneDrive_OneItemPage_NoErrors_FileRenamedMultiple", name: "OneDrive_OneItemPage_NoErrors_FileRenamedMultiple",
@ -1418,9 +1420,9 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"folder": folderPath1, "folder": folderPath1,
}, },
}, },
expectedDelList: map[string]map[string]struct{}{ expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{
rootFolderPath1: getDelList("file"), rootFolderPath1: getDelList("file"),
}, }),
}, },
{ {
name: "OneDrive_OneItemPage_NoErrors_FileMovedMultiple", name: "OneDrive_OneItemPage_NoErrors_FileMovedMultiple",
@ -1455,9 +1457,9 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"folder": folderPath1, "folder": folderPath1,
}, },
}, },
expectedDelList: map[string]map[string]struct{}{ expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{
rootFolderPath1: getDelList("file"), rootFolderPath1: getDelList("file"),
}, }),
}, },
{ {
name: "OneDrive_OneItemPage_EmptyDelta_NoErrors", name: "OneDrive_OneItemPage_EmptyDelta_NoErrors",
@ -1484,9 +1486,9 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
}, },
expectedDeltaURLs: map[string]string{}, expectedDeltaURLs: map[string]string{},
expectedFolderPaths: map[string]map[string]string{}, expectedFolderPaths: map[string]map[string]string{},
expectedDelList: map[string]map[string]struct{}{ expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{
rootFolderPath1: getDelList("file"), rootFolderPath1: getDelList("file"),
}, }),
}, },
{ {
name: "OneDrive_TwoItemPages_NoErrors", name: "OneDrive_TwoItemPages_NoErrors",
@ -1528,9 +1530,9 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"folder": folderPath1, "folder": folderPath1,
}, },
}, },
expectedDelList: map[string]map[string]struct{}{ expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{
rootFolderPath1: getDelList("file", "file2"), rootFolderPath1: getDelList("file", "file2"),
}, }),
}, },
{ {
name: "TwoDrives_OneItemPageEach_NoErrors", name: "TwoDrives_OneItemPageEach_NoErrors",
@ -1585,10 +1587,10 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"folder2": folderPath2, "folder2": folderPath2,
}, },
}, },
expectedDelList: map[string]map[string]struct{}{ expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{
rootFolderPath1: getDelList("file"), rootFolderPath1: getDelList("file"),
rootFolderPath2: getDelList("file2"), rootFolderPath2: getDelList("file2"),
}, }),
}, },
{ {
name: "TwoDrives_DuplicateIDs_OneItemPageEach_NoErrors", name: "TwoDrives_DuplicateIDs_OneItemPageEach_NoErrors",
@ -1643,10 +1645,10 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"folder": folderPath2, "folder": folderPath2,
}, },
}, },
expectedDelList: map[string]map[string]struct{}{ expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{
rootFolderPath1: getDelList("file"), rootFolderPath1: getDelList("file"),
rootFolderPath2: getDelList("file2"), rootFolderPath2: getDelList("file2"),
}, }),
}, },
{ {
name: "OneDrive_OneItemPage_Errors", name: "OneDrive_OneItemPage_Errors",
@ -1696,7 +1698,7 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"root": rootFolderPath1, "root": rootFolderPath1,
}, },
}, },
expectedDelList: map[string]map[string]struct{}{}, expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{}),
doNotMergeItems: true, doNotMergeItems: true,
}, },
{ {
@ -1738,7 +1740,7 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"folder": folderPath1, "folder": folderPath1,
}, },
}, },
expectedDelList: map[string]map[string]struct{}{}, expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{}),
doNotMergeItems: true, doNotMergeItems: true,
}, },
{ {
@ -1780,9 +1782,9 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"folder": folderPath1, "folder": folderPath1,
}, },
}, },
expectedDelList: map[string]map[string]struct{}{ expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{
rootFolderPath1: getDelList("file", "file2"), rootFolderPath1: getDelList("file", "file2"),
}, }),
doNotMergeItems: false, doNotMergeItems: false,
}, },
{ {
@ -1824,7 +1826,7 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"folder2": expectedPath1("/folder2"), "folder2": expectedPath1("/folder2"),
}, },
}, },
expectedDelList: map[string]map[string]struct{}{}, expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{}),
doNotMergeItems: true, doNotMergeItems: true,
}, },
{ {
@ -1870,7 +1872,7 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"folder2": expectedPath1("/folder"), "folder2": expectedPath1("/folder"),
}, },
}, },
expectedDelList: map[string]map[string]struct{}{}, expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{}),
doNotMergeItems: true, doNotMergeItems: true,
}, },
{ {
@ -1915,9 +1917,9 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"folder": folderPath1, "folder": folderPath1,
}, },
}, },
expectedDelList: map[string]map[string]struct{}{ expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{
rootFolderPath1: getDelList("file", "file2"), rootFolderPath1: getDelList("file", "file2"),
}, }),
expectedSkippedCount: 2, expectedSkippedCount: 2,
}, },
{ {
@ -1970,7 +1972,7 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"folder": folderPath1, "folder": folderPath1,
}, },
}, },
expectedDelList: map[string]map[string]struct{}{}, expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{}),
doNotMergeItems: true, doNotMergeItems: true,
}, },
{ {
@ -2009,7 +2011,7 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"root": rootFolderPath1, "root": rootFolderPath1,
}, },
}, },
expectedDelList: map[string]map[string]struct{}{}, expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{}),
doNotMergeItems: true, doNotMergeItems: true,
}, },
{ {
@ -2046,7 +2048,7 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"root": rootFolderPath1, "root": rootFolderPath1,
}, },
}, },
expectedDelList: map[string]map[string]struct{}{}, expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{}),
doNotMergeItems: true, doNotMergeItems: true,
}, },
{ {
@ -2087,9 +2089,9 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"root": rootFolderPath1, "root": rootFolderPath1,
}, },
}, },
expectedDelList: map[string]map[string]struct{}{ expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{
rootFolderPath1: getDelList("file"), rootFolderPath1: getDelList("file"),
}, }),
}, },
{ {
name: "One Drive Item Made And Deleted", name: "One Drive Item Made And Deleted",
@ -2130,9 +2132,9 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"folder": folderPath1, "folder": folderPath1,
}, },
}, },
expectedDelList: map[string]map[string]struct{}{ expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{
rootFolderPath1: getDelList("file"), rootFolderPath1: getDelList("file"),
}, }),
}, },
{ {
name: "One Drive Random Folder Delete", name: "One Drive Random Folder Delete",
@ -2163,7 +2165,7 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"root": rootFolderPath1, "root": rootFolderPath1,
}, },
}, },
expectedDelList: map[string]map[string]struct{}{}, expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{}),
}, },
{ {
name: "One Drive Random Item Delete", name: "One Drive Random Item Delete",
@ -2194,9 +2196,9 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
"root": rootFolderPath1, "root": rootFolderPath1,
}, },
}, },
expectedDelList: map[string]map[string]struct{}{ expectedDelList: pmMock.NewPrefixMap(map[string]map[string]struct{}{
rootFolderPath1: getDelList("file"), rootFolderPath1: getDelList("file"),
}, }),
}, },
} }
for _, test := range table { for _, test := range table {
@ -2269,7 +2271,9 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
prevMetadata := []data.RestoreCollection{data.NotFoundRestoreCollection{Collection: mc}} prevMetadata := []data.RestoreCollection{data.NotFoundRestoreCollection{Collection: mc}}
errs := fault.New(true) errs := fault.New(true)
cols, delList, err := c.Get(ctx, prevMetadata, errs) delList := prefixmatcher.NewStringSetBuilder()
cols, err := c.Get(ctx, prevMetadata, delList, errs)
test.errCheck(t, err) test.errCheck(t, err)
assert.Equal(t, test.expectedSkippedCount, len(errs.Skipped())) assert.Equal(t, test.expectedSkippedCount, len(errs.Skipped()))
@ -2339,7 +2343,7 @@ func (suite *OneDriveCollectionsUnitSuite) TestGet() {
// collections we expect it to // collections we expect it to
assert.Equal(t, expectedCollectionCount, collectionCount, "number of collections") assert.Equal(t, expectedCollectionCount, collectionCount, "number of collections")
assert.Equal(t, test.expectedDelList, delList, "del list") test.expectedDelList.AssertEqual(t, delList)
}) })
} }
} }

View File

@ -4,9 +4,9 @@ import (
"context" "context"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"golang.org/x/exp/maps"
"github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
@ -44,7 +44,7 @@ func DataCollections(
su support.StatusUpdater, su support.StatusUpdater,
ctrlOpts control.Options, ctrlOpts control.Options,
errs *fault.Bus, errs *fault.Bus,
) ([]data.BackupCollection, map[string]map[string]struct{}, error) { ) ([]data.BackupCollection, *prefixmatcher.StringSetMatcher, error) {
odb, err := selector.ToOneDriveBackup() odb, err := selector.ToOneDriveBackup()
if err != nil { if err != nil {
return nil, nil, clues.Wrap(err, "parsing selector").WithClues(ctx) return nil, nil, clues.Wrap(err, "parsing selector").WithClues(ctx)
@ -54,7 +54,7 @@ func DataCollections(
el = errs.Local() el = errs.Local()
categories = map[path.CategoryType]struct{}{} categories = map[path.CategoryType]struct{}{}
collections = []data.BackupCollection{} collections = []data.BackupCollection{}
allExcludes = map[string]map[string]struct{}{} ssmb = prefixmatcher.NewStringSetBuilder()
) )
// for each scope that includes oneDrive items, get all // for each scope that includes oneDrive items, get all
@ -75,7 +75,7 @@ func DataCollections(
su, su,
ctrlOpts) ctrlOpts)
odcs, excludes, err := nc.Get(ctx, metadata, errs) odcs, err := nc.Get(ctx, metadata, ssmb, errs)
if err != nil { if err != nil {
el.AddRecoverable(clues.Stack(err).Label(fault.LabelForceNoBackupCreation)) el.AddRecoverable(clues.Stack(err).Label(fault.LabelForceNoBackupCreation))
} }
@ -83,14 +83,6 @@ func DataCollections(
categories[scope.Category().PathType()] = struct{}{} categories[scope.Category().PathType()] = struct{}{}
collections = append(collections, odcs...) collections = append(collections, odcs...)
for k, ex := range excludes {
if _, ok := allExcludes[k]; !ok {
allExcludes[k] = map[string]struct{}{}
}
maps.Copy(allExcludes[k], ex)
}
} }
mcs, err := migrationCollections( mcs, err := migrationCollections(
@ -123,7 +115,7 @@ func DataCollections(
collections = append(collections, baseCols...) collections = append(collections, baseCols...)
} }
return collections, allExcludes, el.Failure() return collections, ssmb.ToReader(), el.Failure()
} }
// adds data migrations to the collection set. // adds data migrations to the collection set.

View File

@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/common/dttm" "github.com/alcionai/corso/src/internal/common/dttm"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/onedrive/api" "github.com/alcionai/corso/src/internal/connector/onedrive/api"
@ -442,10 +443,12 @@ func (suite *OneDriveSuite) TestOneDriveNewCollections() {
ToggleFeatures: control.Toggles{}, ToggleFeatures: control.Toggles{},
}) })
odcs, excludes, err := colls.Get(ctx, nil, fault.New(true)) ssmb := prefixmatcher.NewStringSetBuilder()
odcs, err := colls.Get(ctx, nil, ssmb, fault.New(true))
assert.NoError(t, err, clues.ToCore(err)) assert.NoError(t, err, clues.ToCore(err))
// Don't expect excludes as this isn't an incremental backup. // Don't expect excludes as this isn't an incremental backup.
assert.Empty(t, excludes) assert.True(t, ssmb.Empty())
for _, entry := range odcs { for _, entry := range odcs {
assert.NotEmpty(t, entry.FullPath()) assert.NotEmpty(t, entry.FullPath())

View File

@ -4,9 +4,9 @@ import (
"context" "context"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"golang.org/x/exp/maps"
"github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/onedrive" "github.com/alcionai/corso/src/internal/connector/onedrive"
"github.com/alcionai/corso/src/internal/connector/sharepoint/api" "github.com/alcionai/corso/src/internal/connector/sharepoint/api"
@ -39,7 +39,7 @@ func DataCollections(
su statusUpdater, su statusUpdater,
ctrlOpts control.Options, ctrlOpts control.Options,
errs *fault.Bus, errs *fault.Bus,
) ([]data.BackupCollection, map[string]map[string]struct{}, error) { ) ([]data.BackupCollection, *prefixmatcher.StringSetMatcher, error) {
b, err := selector.ToSharePointBackup() b, err := selector.ToSharePointBackup()
if err != nil { if err != nil {
return nil, nil, clues.Wrap(err, "sharePointDataCollection: parsing selector") return nil, nil, clues.Wrap(err, "sharePointDataCollection: parsing selector")
@ -54,7 +54,7 @@ func DataCollections(
el = errs.Local() el = errs.Local()
collections = []data.BackupCollection{} collections = []data.BackupCollection{}
categories = map[path.CategoryType]struct{}{} categories = map[path.CategoryType]struct{}{}
excluded = map[string]map[string]struct{}{} ssmb = prefixmatcher.NewStringSetBuilder()
) )
for _, scope := range b.Scopes() { for _, scope := range b.Scopes() {
@ -86,15 +86,14 @@ func DataCollections(
} }
case path.LibrariesCategory: case path.LibrariesCategory:
var excludes map[string]map[string]struct{} spcs, err = collectLibraries(
spcs, excludes, err = collectLibraries(
ctx, ctx,
itemClient, itemClient,
serv, serv,
creds.AzureTenantID, creds.AzureTenantID,
site, site,
metadata, metadata,
ssmb,
scope, scope,
su, su,
ctrlOpts, ctrlOpts,
@ -104,14 +103,6 @@ func DataCollections(
continue continue
} }
for prefix, excludes := range excludes {
if _, ok := excluded[prefix]; !ok {
excluded[prefix] = map[string]struct{}{}
}
maps.Copy(excluded[prefix], excludes)
}
case path.PagesCategory: case path.PagesCategory:
spcs, err = collectPages( spcs, err = collectPages(
ctx, ctx,
@ -150,7 +141,7 @@ func DataCollections(
collections = append(collections, baseCols...) collections = append(collections, baseCols...)
} }
return collections, excluded, el.Failure() return collections, ssmb.ToReader(), el.Failure()
} }
func collectLists( func collectLists(
@ -208,11 +199,12 @@ func collectLibraries(
tenantID string, tenantID string,
site idname.Provider, site idname.Provider,
metadata []data.RestoreCollection, metadata []data.RestoreCollection,
ssmb *prefixmatcher.StringSetMatchBuilder,
scope selectors.SharePointScope, scope selectors.SharePointScope,
updater statusUpdater, updater statusUpdater,
ctrlOpts control.Options, ctrlOpts control.Options,
errs *fault.Bus, errs *fault.Bus,
) ([]data.BackupCollection, map[string]map[string]struct{}, error) { ) ([]data.BackupCollection, error) {
logger.Ctx(ctx).Debug("creating SharePoint Library collections") logger.Ctx(ctx).Debug("creating SharePoint Library collections")
var ( var (
@ -228,12 +220,12 @@ func collectLibraries(
ctrlOpts) ctrlOpts)
) )
odcs, excludes, err := colls.Get(ctx, metadata, errs) odcs, err := colls.Get(ctx, metadata, ssmb, errs)
if err != nil { if err != nil {
return nil, nil, graph.Wrap(ctx, err, "getting library") return nil, graph.Wrap(ctx, err, "getting library")
} }
return append(collections, odcs...), excludes, nil return append(collections, odcs...), nil
} }
// collectPages constructs a sharepoint Collections struct and Get()s the associated // collectPages constructs a sharepoint Collections struct and Get()s the associated

View File

@ -114,7 +114,7 @@ type locRefs struct {
} }
type locationPrefixMatcher struct { type locationPrefixMatcher struct {
m prefixmatcher.Matcher[locRefs] m prefixmatcher.Builder[locRefs]
} }
func (m *locationPrefixMatcher) add( func (m *locationPrefixMatcher) add(

View File

@ -21,6 +21,7 @@ import (
"github.com/kopia/kopia/repo/manifest" "github.com/kopia/kopia/repo/manifest"
"github.com/kopia/kopia/snapshot/snapshotfs" "github.com/kopia/kopia/snapshot/snapshotfs"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/graph/metadata" "github.com/alcionai/corso/src/internal/connector/graph/metadata"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
@ -413,7 +414,7 @@ func streamBaseEntries(
locationPath *path.Builder, locationPath *path.Builder,
dir fs.Directory, dir fs.Directory,
encodedSeen map[string]struct{}, encodedSeen map[string]struct{},
globalExcludeSet map[string]map[string]struct{}, globalExcludeSet prefixmatcher.StringSetReader,
progress *corsoProgress, progress *corsoProgress,
) error { ) error {
if dir == nil { if dir == nil {
@ -421,20 +422,19 @@ func streamBaseEntries(
} }
var ( var (
longest string
excludeSet map[string]struct{} excludeSet map[string]struct{}
curPrefix string
) )
ctx = clues.Add(ctx, "current_item_path", curPath) if globalExcludeSet != nil {
longest, excludeSet, _ = globalExcludeSet.LongestPrefix(curPath.String())
for prefix, excludes := range globalExcludeSet {
// Select the set with the longest prefix to be most precise.
if strings.HasPrefix(curPath.String(), prefix) && len(prefix) >= len(curPrefix) {
excludeSet = excludes
curPrefix = prefix
}
} }
ctx = clues.Add(
ctx,
"current_item_path", curPath,
"longest_prefix", longest)
err := dir.IterateEntries(ctx, func(innerCtx context.Context, entry fs.Entry) error { err := dir.IterateEntries(ctx, func(innerCtx context.Context, entry fs.Entry) error {
if err := innerCtx.Err(); err != nil { if err := innerCtx.Err(); err != nil {
return err return err
@ -521,7 +521,7 @@ func getStreamItemFunc(
staticEnts []fs.Entry, staticEnts []fs.Entry,
streamedEnts data.BackupCollection, streamedEnts data.BackupCollection,
baseDir fs.Directory, baseDir fs.Directory,
globalExcludeSet map[string]map[string]struct{}, globalExcludeSet prefixmatcher.StringSetReader,
progress *corsoProgress, progress *corsoProgress,
) func(context.Context, func(context.Context, fs.Entry) error) error { ) func(context.Context, func(context.Context, fs.Entry) error) error {
return func(ctx context.Context, cb func(context.Context, fs.Entry) error) error { return func(ctx context.Context, cb func(context.Context, fs.Entry) error) error {
@ -569,7 +569,7 @@ func getStreamItemFunc(
func buildKopiaDirs( func buildKopiaDirs(
dirName string, dirName string,
dir *treeMap, dir *treeMap,
globalExcludeSet map[string]map[string]struct{}, globalExcludeSet prefixmatcher.StringSetReader,
progress *corsoProgress, progress *corsoProgress,
) (fs.Directory, error) { ) (fs.Directory, error) {
// Need to build the directory tree from the leaves up because intermediate // Need to build the directory tree from the leaves up because intermediate
@ -1053,7 +1053,7 @@ func inflateDirTree(
loader snapshotLoader, loader snapshotLoader,
baseSnaps []IncrementalBase, baseSnaps []IncrementalBase,
collections []data.BackupCollection, collections []data.BackupCollection,
globalExcludeSet map[string]map[string]struct{}, globalExcludeSet prefixmatcher.StringSetReader,
progress *corsoProgress, progress *corsoProgress,
) (fs.Directory, error) { ) (fs.Directory, error) {
roots, updatedPaths, err := inflateCollectionTree(ctx, collections, progress.toMerge) roots, updatedPaths, err := inflateCollectionTree(ctx, collections, progress.toMerge)

View File

@ -19,6 +19,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
pmMock "github.com/alcionai/corso/src/internal/common/prefixmatcher/mock"
exchMock "github.com/alcionai/corso/src/internal/connector/exchange/mock" exchMock "github.com/alcionai/corso/src/internal/connector/exchange/mock"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
@ -708,7 +709,7 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTree() {
// - emails // - emails
// - Inbox // - Inbox
// - 42 separate files // - 42 separate files
dirTree, err := inflateDirTree(ctx, nil, nil, collections, nil, progress) dirTree, err := inflateDirTree(ctx, nil, nil, collections, pmMock.NewPrefixMap(nil), progress)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
assert.Equal(t, encodeAsPath(testTenant), dirTree.Name()) assert.Equal(t, encodeAsPath(testTenant), dirTree.Name())
@ -805,7 +806,7 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTree_MixedDirectory()
errs: fault.New(true), errs: fault.New(true),
} }
dirTree, err := inflateDirTree(ctx, nil, nil, test.layout, nil, progress) dirTree, err := inflateDirTree(ctx, nil, nil, test.layout, pmMock.NewPrefixMap(nil), progress)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
assert.Equal(t, encodeAsPath(testTenant), dirTree.Name()) assert.Equal(t, encodeAsPath(testTenant), dirTree.Name())
@ -911,7 +912,7 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTree_Fails() {
errs: fault.New(true), errs: fault.New(true),
} }
_, err := inflateDirTree(ctx, nil, nil, test.layout, nil, progress) _, err := inflateDirTree(ctx, nil, nil, test.layout, pmMock.NewPrefixMap(nil), progress)
assert.Error(t, err, clues.ToCore(err)) assert.Error(t, err, clues.ToCore(err))
}) })
} }
@ -1027,7 +1028,7 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeErrors() {
cols = append(cols, mc) cols = append(cols, mc)
} }
_, err := inflateDirTree(ctx, nil, nil, cols, nil, progress) _, err := inflateDirTree(ctx, nil, nil, cols, pmMock.NewPrefixMap(nil), progress)
require.Error(t, err, clues.ToCore(err)) require.Error(t, err, clues.ToCore(err))
}) })
} }
@ -1312,9 +1313,8 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSingleSubtree() {
mockIncrementalBase("", testTenant, testUser, path.ExchangeService, path.EmailCategory), mockIncrementalBase("", testTenant, testUser, path.ExchangeService, path.EmailCategory),
}, },
test.inputCollections(), test.inputCollections(),
nil, pmMock.NewPrefixMap(nil),
progress, progress)
)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
expectTree(t, ctx, test.expected, dirTree) expectTree(t, ctx, test.expected, dirTree)
@ -1433,7 +1433,7 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeMultipleSubdirecto
table := []struct { table := []struct {
name string name string
inputCollections func(t *testing.T) []data.BackupCollection inputCollections func(t *testing.T) []data.BackupCollection
inputExcludes map[string]map[string]struct{} inputExcludes *pmMock.PrefixMap
expected *expectedNode expected *expectedNode
}{ }{
{ {
@ -1441,11 +1441,11 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeMultipleSubdirecto
inputCollections: func(t *testing.T) []data.BackupCollection { inputCollections: func(t *testing.T) []data.BackupCollection {
return nil return nil
}, },
inputExcludes: map[string]map[string]struct{}{ inputExcludes: pmMock.NewPrefixMap(map[string]map[string]struct{}{
"": { "": {
inboxFileName1: {}, inboxFileName1: {},
}, },
}, }),
expected: expectedTreeWithChildren( expected: expectedTreeWithChildren(
[]string{ []string{
testTenant, testTenant,
@ -2229,6 +2229,11 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeMultipleSubdirecto
snapshotRoot: getBaseSnapshot(), snapshotRoot: getBaseSnapshot(),
} }
ie := pmMock.NewPrefixMap(nil)
if test.inputExcludes != nil {
ie = test.inputExcludes
}
dirTree, err := inflateDirTree( dirTree, err := inflateDirTree(
ctx, ctx,
msw, msw,
@ -2236,7 +2241,7 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeMultipleSubdirecto
mockIncrementalBase("", testTenant, testUser, path.ExchangeService, path.EmailCategory), mockIncrementalBase("", testTenant, testUser, path.ExchangeService, path.EmailCategory),
}, },
test.inputCollections(t), test.inputCollections(t),
test.inputExcludes, ie,
progress) progress)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
@ -2400,7 +2405,7 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSkipsDeletedSubtre
mockIncrementalBase("", testTenant, testUser, path.ExchangeService, path.EmailCategory), mockIncrementalBase("", testTenant, testUser, path.ExchangeService, path.EmailCategory),
}, },
collections, collections,
nil, pmMock.NewPrefixMap(nil),
progress) progress)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
@ -2505,7 +2510,7 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTree_HandleEmptyBase()
mockIncrementalBase("", testTenant, testUser, path.ExchangeService, path.EmailCategory), mockIncrementalBase("", testTenant, testUser, path.ExchangeService, path.EmailCategory),
}, },
collections, collections,
nil, pmMock.NewPrefixMap(nil),
progress) progress)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
@ -2756,9 +2761,8 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSelectsCorrectSubt
mockIncrementalBase("id2", testTenant, testUser, path.ExchangeService, path.EmailCategory), mockIncrementalBase("id2", testTenant, testUser, path.ExchangeService, path.EmailCategory),
}, },
collections, collections,
nil, pmMock.NewPrefixMap(nil),
progress, progress)
)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
expectTree(t, ctx, expected, dirTree) expectTree(t, ctx, expected, dirTree)
@ -2921,7 +2925,7 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSelectsMigrateSubt
mockIncrementalBase("id1", testTenant, testUser, path.ExchangeService, path.EmailCategory, path.ContactsCategory), mockIncrementalBase("id1", testTenant, testUser, path.ExchangeService, path.EmailCategory, path.ContactsCategory),
}, },
[]data.BackupCollection{mce, mcc}, []data.BackupCollection{mce, mcc},
nil, pmMock.NewPrefixMap(nil),
progress) progress)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))

View File

@ -14,6 +14,7 @@ import (
"github.com/kopia/kopia/snapshot/snapshotfs" "github.com/kopia/kopia/snapshot/snapshotfs"
"github.com/kopia/kopia/snapshot/snapshotmaintenance" "github.com/kopia/kopia/snapshot/snapshotmaintenance"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/diagnostics" "github.com/alcionai/corso/src/internal/diagnostics"
"github.com/alcionai/corso/src/internal/observe" "github.com/alcionai/corso/src/internal/observe"
@ -138,7 +139,7 @@ func (w Wrapper) ConsumeBackupCollections(
ctx context.Context, ctx context.Context,
previousSnapshots []IncrementalBase, previousSnapshots []IncrementalBase,
collections []data.BackupCollection, collections []data.BackupCollection,
globalExcludeSet map[string]map[string]struct{}, globalExcludeSet prefixmatcher.StringSetReader,
tags map[string]string, tags map[string]string,
buildTreeWithBase bool, buildTreeWithBase bool,
errs *fault.Bus, errs *fault.Bus,
@ -150,7 +151,7 @@ func (w Wrapper) ConsumeBackupCollections(
ctx, end := diagnostics.Span(ctx, "kopia:consumeBackupCollections") ctx, end := diagnostics.Span(ctx, "kopia:consumeBackupCollections")
defer end() defer end()
if len(collections) == 0 && len(globalExcludeSet) == 0 { if len(collections) == 0 && (globalExcludeSet == nil || globalExcludeSet.Empty()) {
return &BackupStats{}, &details.Builder{}, nil, nil return &BackupStats{}, &details.Builder{}, nil, nil
} }

View File

@ -18,6 +18,7 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
pmMock "github.com/alcionai/corso/src/internal/common/prefixmatcher/mock"
exchMock "github.com/alcionai/corso/src/internal/connector/exchange/mock" exchMock "github.com/alcionai/corso/src/internal/connector/exchange/mock"
"github.com/alcionai/corso/src/internal/connector/onedrive/metadata" "github.com/alcionai/corso/src/internal/connector/onedrive/metadata"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
@ -1178,14 +1179,14 @@ func (suite *KopiaSimpleRepoIntegrationSuite) TestBackupExcludeItem() {
prefix = itemPath.ToBuilder().Dir().Dir().String() prefix = itemPath.ToBuilder().Dir().Dir().String()
} }
var excluded map[string]map[string]struct{} excluded := pmMock.NewPrefixMap(nil)
if test.excludeItem { if test.excludeItem {
excluded = map[string]map[string]struct{}{ excluded = pmMock.NewPrefixMap(map[string]map[string]struct{}{
// Add a prefix if needed. // Add a prefix if needed.
prefix: { prefix: {
itemPath.Item(): {}, itemPath.Item(): {},
}, },
} })
} }
stats, _, _, err := suite.w.ConsumeBackupCollections( stats, _, _, err := suite.w.ConsumeBackupCollections(

View File

@ -10,6 +10,7 @@ import (
"github.com/alcionai/corso/src/internal/common/crash" "github.com/alcionai/corso/src/internal/common/crash"
"github.com/alcionai/corso/src/internal/common/dttm" "github.com/alcionai/corso/src/internal/common/dttm"
"github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/diagnostics" "github.com/alcionai/corso/src/internal/diagnostics"
"github.com/alcionai/corso/src/internal/events" "github.com/alcionai/corso/src/internal/events"
@ -272,7 +273,7 @@ func (op *BackupOperation) do(
} }
} }
cs, excludes, err := produceBackupDataCollections( cs, ssmb, err := produceBackupDataCollections(
ctx, ctx,
op.bp, op.bp,
op.ResourceOwner, op.ResourceOwner,
@ -294,7 +295,7 @@ func (op *BackupOperation) do(
reasons, reasons,
mans, mans,
cs, cs,
excludes, ssmb,
backupID, backupID,
op.incremental && canUseMetaData, op.incremental && canUseMetaData,
op.Errors) op.Errors)
@ -352,7 +353,7 @@ func produceBackupDataCollections(
lastBackupVersion int, lastBackupVersion int,
ctrlOpts control.Options, ctrlOpts control.Options,
errs *fault.Bus, errs *fault.Bus,
) ([]data.BackupCollection, map[string]map[string]struct{}, error) { ) ([]data.BackupCollection, prefixmatcher.StringSetReader, error) {
complete, closer := observe.MessageWithCompletion(ctx, "Discovering items to backup") complete, closer := observe.MessageWithCompletion(ctx, "Discovering items to backup")
defer func() { defer func() {
complete <- struct{}{} complete <- struct{}{}
@ -424,7 +425,7 @@ func consumeBackupCollections(
reasons []kopia.Reason, reasons []kopia.Reason,
mans []*kopia.ManifestEntry, mans []*kopia.ManifestEntry,
cs []data.BackupCollection, cs []data.BackupCollection,
excludes map[string]map[string]struct{}, pmr prefixmatcher.StringSetReader,
backupID model.StableID, backupID model.StableID,
isIncremental bool, isIncremental bool,
errs *fault.Bus, errs *fault.Bus,
@ -497,7 +498,7 @@ func consumeBackupCollections(
ctx, ctx,
bases, bases,
cs, cs,
excludes, pmr,
tags, tags,
isIncremental, isIncremental,
errs) errs)

View File

@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/connector/mock" "github.com/alcionai/corso/src/internal/connector/mock"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
evmock "github.com/alcionai/corso/src/internal/events/mock" evmock "github.com/alcionai/corso/src/internal/events/mock"
@ -98,7 +99,7 @@ func (mbu mockBackupConsumer) ConsumeBackupCollections(
ctx context.Context, ctx context.Context,
bases []kopia.IncrementalBase, bases []kopia.IncrementalBase,
cs []data.BackupCollection, cs []data.BackupCollection,
excluded map[string]map[string]struct{}, excluded prefixmatcher.StringSetReader,
tags map[string]string, tags map[string]string,
buildTreeWithBase bool, buildTreeWithBase bool,
errs *fault.Bus, errs *fault.Bus,

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/kopia" "github.com/alcionai/corso/src/internal/kopia"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
@ -25,7 +26,7 @@ type (
lastBackupVersion int, lastBackupVersion int,
ctrlOpts control.Options, ctrlOpts control.Options,
errs *fault.Bus, errs *fault.Bus,
) ([]data.BackupCollection, map[string]map[string]struct{}, error) ) ([]data.BackupCollection, prefixmatcher.StringSetReader, error)
Wait() *data.CollectionStats Wait() *data.CollectionStats
} }
@ -35,7 +36,7 @@ type (
ctx context.Context, ctx context.Context,
bases []kopia.IncrementalBase, bases []kopia.IncrementalBase,
cs []data.BackupCollection, cs []data.BackupCollection,
excluded map[string]map[string]struct{}, pmr prefixmatcher.StringSetReader,
tags map[string]string, tags map[string]string,
buildTreeWithBase bool, buildTreeWithBase bool,
errs *fault.Bus, errs *fault.Bus,

View File

@ -9,6 +9,7 @@ import (
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/kopia" "github.com/alcionai/corso/src/internal/kopia"
"github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/internal/operations/inject"
@ -232,7 +233,7 @@ func write(
ctx, ctx,
nil, nil,
dbcs, dbcs,
nil, prefixmatcher.NopReader[map[string]struct{}](),
nil, nil,
false, false,
errs) errs)