Compare commits

...

5 Commits

Author SHA1 Message Date
ryanfkeepers
d0ea40984a rename path ResourceOwner() to ProtectedResource() 2023-08-09 14:59:09 -06:00
ryanfkeepers
be60a6d1e4 small paths code rearrangement
small cleanup in paths, primarily splitting files
so that file contents are more clearly owned, which
should be a little better for readability and code
placement.

Also renames `ServicePrefix` to `BuildPrefix` in
anticipation of multi-service prefixes.
2023-08-09 14:33:20 -06:00
ryanfkeepers
cbbc6c61dd move selectorsToReason into selectors 2023-08-09 14:03:56 -06:00
ryanfkeepers
e06e6b7bb4 selectors cleanup
some code movement and removal in selectors and scopes
before adding scope-to-reason.  Removed code
was only being used in testing.
2023-08-08 18:44:25 -06:00
ryanfkeepers
e3e90e397c move kopia.Reasoner to new package
Moves the kopia.Reasoner interface out of the
kopia package and into pkg/backup/identity.

No logical changes here, just renaming and movement.
2023-08-08 18:06:26 -06:00
52 changed files with 2201 additions and 1735 deletions

View File

@ -163,12 +163,11 @@ func (suite *SharePointUnitSuite) TestSharePointBackupCreateSelectors() {
) )
table := []struct { table := []struct {
name string name string
site []string site []string
weburl []string weburl []string
data []string data []string
expect []string expect []string
expectScopesLen int
}{ }{
{ {
name: "no sites or urls", name: "no sites or urls",
@ -181,63 +180,54 @@ func (suite *SharePointUnitSuite) TestSharePointBackupCreateSelectors() {
expect: selectors.None(), expect: selectors.None(),
}, },
{ {
name: "site wildcard", name: "site wildcard",
site: []string{flags.Wildcard}, site: []string{flags.Wildcard},
expect: bothIDs, expect: bothIDs,
expectScopesLen: 2,
}, },
{ {
name: "url wildcard", name: "url wildcard",
weburl: []string{flags.Wildcard}, weburl: []string{flags.Wildcard},
expect: bothIDs, expect: bothIDs,
expectScopesLen: 2,
}, },
{ {
name: "sites", name: "sites",
site: []string{id1, id2}, site: []string{id1, id2},
expect: []string{id1, id2}, expect: []string{id1, id2},
expectScopesLen: 2,
}, },
{ {
name: "urls", name: "urls",
weburl: []string{url1, url2}, weburl: []string{url1, url2},
expect: []string{url1, url2}, expect: []string{url1, url2},
expectScopesLen: 2,
}, },
{ {
name: "mix sites and urls", name: "mix sites and urls",
site: []string{id1}, site: []string{id1},
weburl: []string{url2}, weburl: []string{url2},
expect: []string{id1, url2}, expect: []string{id1, url2},
expectScopesLen: 2,
}, },
{ {
name: "duplicate sites and urls", name: "duplicate sites and urls",
site: []string{id1, id2}, site: []string{id1, id2},
weburl: []string{url1, url2}, weburl: []string{url1, url2},
expect: []string{id1, id2, url1, url2}, expect: []string{id1, id2, url1, url2},
expectScopesLen: 2,
}, },
{ {
name: "unnecessary site wildcard", name: "unnecessary site wildcard",
site: []string{id1, flags.Wildcard}, site: []string{id1, flags.Wildcard},
weburl: []string{url1, url2}, weburl: []string{url1, url2},
expect: bothIDs, expect: bothIDs,
expectScopesLen: 2,
}, },
{ {
name: "unnecessary url wildcard", name: "unnecessary url wildcard",
site: []string{id1}, site: []string{id1},
weburl: []string{url1, flags.Wildcard}, weburl: []string{url1, flags.Wildcard},
expect: bothIDs, expect: bothIDs,
expectScopesLen: 2,
}, },
{ {
name: "Pages", name: "Pages",
site: bothIDs, site: bothIDs,
data: []string{dataPages}, data: []string{dataPages},
expect: bothIDs, expect: bothIDs,
expectScopesLen: 1,
}, },
} }
for _, test := range table { for _, test := range table {
@ -249,7 +239,7 @@ func (suite *SharePointUnitSuite) TestSharePointBackupCreateSelectors() {
sel, err := sharePointBackupCreateSelectors(ctx, ins, test.site, test.weburl, test.data) sel, err := sharePointBackupCreateSelectors(ctx, ins, test.site, test.weburl, test.data)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
assert.ElementsMatch(t, test.expect, sel.DiscreteResourceOwners()) assert.ElementsMatch(t, test.expect, sel.ResourceOwners.Targets)
}) })
} }
} }

View File

@ -8,6 +8,7 @@ import (
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
"github.com/alcionai/corso/src/internal/version" "github.com/alcionai/corso/src/internal/version"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/logger"
) )
@ -25,7 +26,7 @@ type BackupBases interface {
MergeBackupBases( MergeBackupBases(
ctx context.Context, ctx context.Context,
other BackupBases, other BackupBases,
reasonToKey func(Reasoner) string, reasonToKey func(identity.Reasoner) string,
) BackupBases ) BackupBases
} }
@ -131,7 +132,7 @@ func (bb *backupBases) ClearAssistBases() {
func (bb *backupBases) MergeBackupBases( func (bb *backupBases) MergeBackupBases(
ctx context.Context, ctx context.Context,
other BackupBases, other BackupBases,
reasonToKey func(reason Reasoner) string, reasonToKey func(reason identity.Reasoner) string,
) BackupBases { ) BackupBases {
if other == nil || (len(other.MergeBases()) == 0 && len(other.AssistBases()) == 0) { if other == nil || (len(other.MergeBases()) == 0 && len(other.AssistBases()) == 0) {
return bb return bb
@ -165,7 +166,7 @@ func (bb *backupBases) MergeBackupBases(
// Calculate the set of mergeBases to pull from other into this one. // Calculate the set of mergeBases to pull from other into this one.
for _, m := range other.MergeBases() { for _, m := range other.MergeBases() {
useReasons := []Reasoner{} useReasons := []identity.Reasoner{}
for _, r := range m.Reasons { for _, r := range m.Reasons {
k := reasonToKey(r) k := reasonToKey(r)
@ -216,7 +217,7 @@ func (bb *backupBases) MergeBackupBases(
// Add assistBases from other to this one as needed. // Add assistBases from other to this one as needed.
for _, m := range other.AssistBases() { for _, m := range other.AssistBases() {
useReasons := []Reasoner{} useReasons := []identity.Reasoner{}
// Assume that all complete manifests in assist overlap with MergeBases. // Assume that all complete manifests in assist overlap with MergeBases.
if len(m.IncompleteReason) == 0 { if len(m.IncompleteReason) == 0 {

View File

@ -13,10 +13,11 @@ import (
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/version" "github.com/alcionai/corso/src/internal/version"
"github.com/alcionai/corso/src/pkg/backup" "github.com/alcionai/corso/src/pkg/backup"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
func makeManifest(id, incmpl, bID string, reasons ...Reasoner) ManifestEntry { func makeManifest(id, incmpl, bID string, reasons ...identity.Reasoner) ManifestEntry {
bIDKey, _ := makeTagKV(TagBackupID) bIDKey, _ := makeTagKV(TagBackupID)
return ManifestEntry{ return ManifestEntry{
@ -223,7 +224,7 @@ func (suite *BackupBasesUnitSuite) TestMergeBackupBases() {
ir = "checkpoint" ir = "checkpoint"
} }
reasons := make([]Reasoner, 0, len(i.cat)) reasons := make([]identity.Reasoner, 0, len(i.cat))
for _, c := range i.cat { for _, c := range i.cat {
reasons = append(reasons, NewReason("", ro, path.ExchangeService, c)) reasons = append(reasons, NewReason("", ro, path.ExchangeService, c))
@ -453,7 +454,7 @@ func (suite *BackupBasesUnitSuite) TestMergeBackupBases() {
got := bb.MergeBackupBases( got := bb.MergeBackupBases(
ctx, ctx,
other, other,
func(r Reasoner) string { func(r identity.Reasoner) string {
return r.Service().String() + r.Category().String() return r.Service().String() + r.Category().String()
}) })
AssertBackupBasesEqual(t, expect, got) AssertBackupBasesEqual(t, expect, got)

View File

@ -12,6 +12,7 @@ import (
"github.com/alcionai/corso/src/internal/model" "github.com/alcionai/corso/src/internal/model"
"github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/internal/operations/inject"
"github.com/alcionai/corso/src/pkg/backup" "github.com/alcionai/corso/src/pkg/backup"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
@ -29,23 +30,11 @@ const (
userTagPrefix = "tag:" userTagPrefix = "tag:"
) )
// TODO(ashmrtn): Move this into some inject package. Here to avoid import
// cycles.
type Reasoner interface {
Tenant() string
ProtectedResource() string
Service() path.ServiceType
Category() path.CategoryType
// SubtreePath returns the path prefix for data in existing backups that have
// parameters (tenant, protected resourced, etc) that match this Reasoner.
SubtreePath() (path.Path, error)
}
func NewReason( func NewReason(
tenant, resource string, tenant, resource string,
service path.ServiceType, service path.ServiceType,
category path.CategoryType, category path.CategoryType,
) Reasoner { ) identity.Reasoner {
return reason{ return reason{
tenant: tenant, tenant: tenant,
resource: resource, resource: resource,
@ -81,7 +70,7 @@ func (r reason) Category() path.CategoryType {
} }
func (r reason) SubtreePath() (path.Path, error) { func (r reason) SubtreePath() (path.Path, error) {
p, err := path.ServicePrefix( p, err := path.BuildPrefix(
r.Tenant(), r.Tenant(),
r.ProtectedResource(), r.ProtectedResource(),
r.Service(), r.Service(),
@ -90,7 +79,7 @@ func (r reason) SubtreePath() (path.Path, error) {
return p, clues.Wrap(err, "building path").OrNil() return p, clues.Wrap(err, "building path").OrNil()
} }
func tagKeys(r Reasoner) []string { func tagKeys(r identity.Reasoner) []string {
return []string{ return []string{
r.ProtectedResource(), r.ProtectedResource(),
serviceCatString(r.Service(), r.Category()), serviceCatString(r.Service(), r.Category()),
@ -98,13 +87,13 @@ func tagKeys(r Reasoner) []string {
} }
// reasonKey returns the concatenation of the ProtectedResource, Service, and Category. // reasonKey returns the concatenation of the ProtectedResource, Service, and Category.
func reasonKey(r Reasoner) string { func reasonKey(r identity.Reasoner) string {
return r.ProtectedResource() + r.Service().String() + r.Category().String() return r.ProtectedResource() + r.Service().String() + r.Category().String()
} }
type BackupEntry struct { type BackupEntry struct {
*backup.Backup *backup.Backup
Reasons []Reasoner Reasons []identity.Reasoner
} }
type ManifestEntry struct { type ManifestEntry struct {
@ -116,7 +105,7 @@ type ManifestEntry struct {
// 1. backup user1 email,contacts -> B1 // 1. backup user1 email,contacts -> B1
// 2. backup user1 contacts -> B2 (uses B1 as base) // 2. backup user1 contacts -> B2 (uses B1 as base)
// 3. backup user1 email,contacts,events (uses B1 for email, B2 for contacts) // 3. backup user1 email,contacts,events (uses B1 for email, B2 for contacts)
Reasons []Reasoner Reasons []identity.Reasoner
} }
func (me ManifestEntry) GetTag(key string) (string, bool) { func (me ManifestEntry) GetTag(key string) (string, bool) {
@ -212,7 +201,7 @@ func (b *baseFinder) getBackupModel(
// most recent complete backup as the base. // most recent complete backup as the base.
func (b *baseFinder) findBasesInSet( func (b *baseFinder) findBasesInSet(
ctx context.Context, ctx context.Context,
reason Reasoner, reason identity.Reasoner,
metas []*manifest.EntryMetadata, metas []*manifest.EntryMetadata,
) (*BackupEntry, *ManifestEntry, []ManifestEntry, error) { ) (*BackupEntry, *ManifestEntry, []ManifestEntry, error) {
// Sort manifests by time so we can go through them sequentially. The code in // Sort manifests by time so we can go through them sequentially. The code in
@ -245,7 +234,7 @@ func (b *baseFinder) findBasesInSet(
kopiaAssistSnaps = append(kopiaAssistSnaps, ManifestEntry{ kopiaAssistSnaps = append(kopiaAssistSnaps, ManifestEntry{
Manifest: man, Manifest: man,
Reasons: []Reasoner{reason}, Reasons: []identity.Reasoner{reason},
}) })
logger.Ctx(ictx).Info("found incomplete backup") logger.Ctx(ictx).Info("found incomplete backup")
@ -266,7 +255,7 @@ func (b *baseFinder) findBasesInSet(
kopiaAssistSnaps = append(kopiaAssistSnaps, ManifestEntry{ kopiaAssistSnaps = append(kopiaAssistSnaps, ManifestEntry{
Manifest: man, Manifest: man,
Reasons: []Reasoner{reason}, Reasons: []identity.Reasoner{reason},
}) })
logger.Ctx(ictx).Info("found incomplete backup") logger.Ctx(ictx).Info("found incomplete backup")
@ -290,7 +279,7 @@ func (b *baseFinder) findBasesInSet(
kopiaAssistSnaps = append(kopiaAssistSnaps, ManifestEntry{ kopiaAssistSnaps = append(kopiaAssistSnaps, ManifestEntry{
Manifest: man, Manifest: man,
Reasons: []Reasoner{reason}, Reasons: []identity.Reasoner{reason},
}) })
logger.Ctx(ictx).Infow( logger.Ctx(ictx).Infow(
@ -308,13 +297,13 @@ func (b *baseFinder) findBasesInSet(
me := ManifestEntry{ me := ManifestEntry{
Manifest: man, Manifest: man,
Reasons: []Reasoner{reason}, Reasons: []identity.Reasoner{reason},
} }
kopiaAssistSnaps = append(kopiaAssistSnaps, me) kopiaAssistSnaps = append(kopiaAssistSnaps, me)
return &BackupEntry{ return &BackupEntry{
Backup: bup, Backup: bup,
Reasons: []Reasoner{reason}, Reasons: []identity.Reasoner{reason},
}, &me, kopiaAssistSnaps, nil }, &me, kopiaAssistSnaps, nil
} }
@ -325,7 +314,7 @@ func (b *baseFinder) findBasesInSet(
func (b *baseFinder) getBase( func (b *baseFinder) getBase(
ctx context.Context, ctx context.Context,
r Reasoner, r identity.Reasoner,
tags map[string]string, tags map[string]string,
) (*BackupEntry, *ManifestEntry, []ManifestEntry, error) { ) (*BackupEntry, *ManifestEntry, []ManifestEntry, error) {
allTags := map[string]string{} allTags := map[string]string{}
@ -352,7 +341,7 @@ func (b *baseFinder) getBase(
func (b *baseFinder) FindBases( func (b *baseFinder) FindBases(
ctx context.Context, ctx context.Context,
reasons []Reasoner, reasons []identity.Reasoner,
tags map[string]string, tags map[string]string,
) BackupBases { ) BackupBases {
var ( var (

View File

@ -14,6 +14,7 @@ import (
"github.com/alcionai/corso/src/internal/model" "github.com/alcionai/corso/src/internal/model"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/backup" "github.com/alcionai/corso/src/pkg/backup"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
@ -39,7 +40,7 @@ var (
testUser2 = "user2" testUser2 = "user2"
testUser3 = "user3" testUser3 = "user3"
testAllUsersAllCats = []Reasoner{ testAllUsersAllCats = []identity.Reasoner{
// User1 email and events. // User1 email and events.
NewReason("", testUser1, path.ExchangeService, path.EmailCategory), NewReason("", testUser1, path.ExchangeService, path.EmailCategory),
NewReason("", testUser1, path.ExchangeService, path.EventsCategory), NewReason("", testUser1, path.ExchangeService, path.EventsCategory),
@ -50,12 +51,12 @@ var (
NewReason("", testUser3, path.ExchangeService, path.EmailCategory), NewReason("", testUser3, path.ExchangeService, path.EmailCategory),
NewReason("", testUser3, path.ExchangeService, path.EventsCategory), NewReason("", testUser3, path.ExchangeService, path.EventsCategory),
} }
testAllUsersMail = []Reasoner{ testAllUsersMail = []identity.Reasoner{
NewReason("", testUser1, path.ExchangeService, path.EmailCategory), NewReason("", testUser1, path.ExchangeService, path.EmailCategory),
NewReason("", testUser2, path.ExchangeService, path.EmailCategory), NewReason("", testUser2, path.ExchangeService, path.EmailCategory),
NewReason("", testUser3, path.ExchangeService, path.EmailCategory), NewReason("", testUser3, path.ExchangeService, path.EmailCategory),
} }
testUser1Mail = []Reasoner{ testUser1Mail = []identity.Reasoner{
NewReason("", testUser1, path.ExchangeService, path.EmailCategory), NewReason("", testUser1, path.ExchangeService, path.EmailCategory),
} }
) )
@ -285,7 +286,7 @@ func (suite *BaseFinderUnitSuite) TestNoResult_NoBackupsOrSnapshots() {
sm: mockEmptySnapshotManager{}, sm: mockEmptySnapshotManager{},
bg: mockEmptyModelGetter{}, bg: mockEmptyModelGetter{},
} }
reasons := []Reasoner{ reasons := []identity.Reasoner{
NewReason("", "a-user", path.ExchangeService, path.EmailCategory), NewReason("", "a-user", path.ExchangeService, path.EmailCategory),
} }
@ -304,7 +305,7 @@ func (suite *BaseFinderUnitSuite) TestNoResult_ErrorListingSnapshots() {
sm: &mockSnapshotManager{findErr: assert.AnError}, sm: &mockSnapshotManager{findErr: assert.AnError},
bg: mockEmptyModelGetter{}, bg: mockEmptyModelGetter{},
} }
reasons := []Reasoner{ reasons := []identity.Reasoner{
NewReason("", "a-user", path.ExchangeService, path.EmailCategory), NewReason("", "a-user", path.ExchangeService, path.EmailCategory),
} }
@ -316,14 +317,14 @@ func (suite *BaseFinderUnitSuite) TestNoResult_ErrorListingSnapshots() {
func (suite *BaseFinderUnitSuite) TestGetBases() { func (suite *BaseFinderUnitSuite) TestGetBases() {
table := []struct { table := []struct {
name string name string
input []Reasoner input []identity.Reasoner
manifestData []manifestInfo manifestData []manifestInfo
// Use this to denote the Reasons a base backup or base manifest is // Use this to denote the Reasons a base backup or base manifest is
// selected. The int maps to the index of the backup or manifest in data. // selected. The int maps to the index of the backup or manifest in data.
expectedBaseReasons map[int][]Reasoner expectedBaseReasons map[int][]identity.Reasoner
// Use this to denote the Reasons a kopia assised incrementals manifest is // Use this to denote the Reasons a kopia assised incrementals manifest is
// selected. The int maps to the index of the manifest in data. // selected. The int maps to the index of the manifest in data.
expectedAssistManifestReasons map[int][]Reasoner expectedAssistManifestReasons map[int][]identity.Reasoner
backupData []backupInfo backupData []backupInfo
}{ }{
{ {
@ -349,10 +350,10 @@ func (suite *BaseFinderUnitSuite) TestGetBases() {
testUser1, testUser1,
), ),
}, },
expectedBaseReasons: map[int][]Reasoner{ expectedBaseReasons: map[int][]identity.Reasoner{
1: testUser1Mail, 1: testUser1Mail,
}, },
expectedAssistManifestReasons: map[int][]Reasoner{ expectedAssistManifestReasons: map[int][]identity.Reasoner{
1: testUser1Mail, 1: testUser1Mail,
}, },
backupData: []backupInfo{ backupData: []backupInfo{
@ -383,10 +384,10 @@ func (suite *BaseFinderUnitSuite) TestGetBases() {
testUser1, testUser1,
), ),
}, },
expectedBaseReasons: map[int][]Reasoner{ expectedBaseReasons: map[int][]identity.Reasoner{
1: testUser1Mail, 1: testUser1Mail,
}, },
expectedAssistManifestReasons: map[int][]Reasoner{ expectedAssistManifestReasons: map[int][]identity.Reasoner{
0: testUser1Mail, 0: testUser1Mail,
1: testUser1Mail, 1: testUser1Mail,
}, },
@ -418,10 +419,10 @@ func (suite *BaseFinderUnitSuite) TestGetBases() {
testUser1, testUser1,
), ),
}, },
expectedBaseReasons: map[int][]Reasoner{ expectedBaseReasons: map[int][]identity.Reasoner{
1: testUser1Mail, 1: testUser1Mail,
}, },
expectedAssistManifestReasons: map[int][]Reasoner{ expectedAssistManifestReasons: map[int][]identity.Reasoner{
0: testUser1Mail, 0: testUser1Mail,
1: testUser1Mail, 1: testUser1Mail,
}, },
@ -447,10 +448,10 @@ func (suite *BaseFinderUnitSuite) TestGetBases() {
testUser3, testUser3,
), ),
}, },
expectedBaseReasons: map[int][]Reasoner{ expectedBaseReasons: map[int][]identity.Reasoner{
0: testUser1Mail, 0: testUser1Mail,
}, },
expectedAssistManifestReasons: map[int][]Reasoner{ expectedAssistManifestReasons: map[int][]identity.Reasoner{
0: testUser1Mail, 0: testUser1Mail,
}, },
backupData: []backupInfo{ backupData: []backupInfo{
@ -474,10 +475,10 @@ func (suite *BaseFinderUnitSuite) TestGetBases() {
testUser3, testUser3,
), ),
}, },
expectedBaseReasons: map[int][]Reasoner{ expectedBaseReasons: map[int][]identity.Reasoner{
0: testAllUsersAllCats, 0: testAllUsersAllCats,
}, },
expectedAssistManifestReasons: map[int][]Reasoner{ expectedAssistManifestReasons: map[int][]identity.Reasoner{
0: testAllUsersAllCats, 0: testAllUsersAllCats,
}, },
backupData: []backupInfo{ backupData: []backupInfo{
@ -512,7 +513,7 @@ func (suite *BaseFinderUnitSuite) TestGetBases() {
testUser3, testUser3,
), ),
}, },
expectedBaseReasons: map[int][]Reasoner{ expectedBaseReasons: map[int][]identity.Reasoner{
0: { 0: {
NewReason("", testUser1, path.ExchangeService, path.EmailCategory), NewReason("", testUser1, path.ExchangeService, path.EmailCategory),
NewReason("", testUser2, path.ExchangeService, path.EmailCategory), NewReason("", testUser2, path.ExchangeService, path.EmailCategory),
@ -524,7 +525,7 @@ func (suite *BaseFinderUnitSuite) TestGetBases() {
NewReason("", testUser3, path.ExchangeService, path.EventsCategory), NewReason("", testUser3, path.ExchangeService, path.EventsCategory),
}, },
}, },
expectedAssistManifestReasons: map[int][]Reasoner{ expectedAssistManifestReasons: map[int][]identity.Reasoner{
0: { 0: {
NewReason("", testUser1, path.ExchangeService, path.EmailCategory), NewReason("", testUser1, path.ExchangeService, path.EmailCategory),
NewReason("", testUser2, path.ExchangeService, path.EmailCategory), NewReason("", testUser2, path.ExchangeService, path.EmailCategory),
@ -564,10 +565,10 @@ func (suite *BaseFinderUnitSuite) TestGetBases() {
testUser1, testUser1,
), ),
}, },
expectedBaseReasons: map[int][]Reasoner{ expectedBaseReasons: map[int][]identity.Reasoner{
0: testUser1Mail, 0: testUser1Mail,
}, },
expectedAssistManifestReasons: map[int][]Reasoner{ expectedAssistManifestReasons: map[int][]identity.Reasoner{
0: testUser1Mail, 0: testUser1Mail,
1: testUser1Mail, 1: testUser1Mail,
}, },
@ -600,10 +601,10 @@ func (suite *BaseFinderUnitSuite) TestGetBases() {
testUser1, testUser1,
), ),
}, },
expectedBaseReasons: map[int][]Reasoner{ expectedBaseReasons: map[int][]identity.Reasoner{
1: testUser1Mail, 1: testUser1Mail,
}, },
expectedAssistManifestReasons: map[int][]Reasoner{ expectedAssistManifestReasons: map[int][]identity.Reasoner{
1: testUser1Mail, 1: testUser1Mail,
}, },
backupData: []backupInfo{ backupData: []backupInfo{
@ -635,8 +636,8 @@ func (suite *BaseFinderUnitSuite) TestGetBases() {
testUser1, testUser1,
), ),
}, },
expectedBaseReasons: map[int][]Reasoner{}, expectedBaseReasons: map[int][]identity.Reasoner{},
expectedAssistManifestReasons: map[int][]Reasoner{ expectedAssistManifestReasons: map[int][]identity.Reasoner{
1: testUser1Mail, 1: testUser1Mail,
}, },
backupData: []backupInfo{ backupData: []backupInfo{
@ -659,10 +660,10 @@ func (suite *BaseFinderUnitSuite) TestGetBases() {
testUser1, testUser1,
), ),
}, },
expectedBaseReasons: map[int][]Reasoner{ expectedBaseReasons: map[int][]identity.Reasoner{
0: testUser1Mail, 0: testUser1Mail,
}, },
expectedAssistManifestReasons: map[int][]Reasoner{ expectedAssistManifestReasons: map[int][]identity.Reasoner{
0: testUser1Mail, 0: testUser1Mail,
}, },
backupData: []backupInfo{ backupData: []backupInfo{
@ -694,10 +695,10 @@ func (suite *BaseFinderUnitSuite) TestGetBases() {
testUser1, testUser1,
), ),
}, },
expectedBaseReasons: map[int][]Reasoner{ expectedBaseReasons: map[int][]identity.Reasoner{
0: testUser1Mail, 0: testUser1Mail,
}, },
expectedAssistManifestReasons: map[int][]Reasoner{ expectedAssistManifestReasons: map[int][]identity.Reasoner{
0: testUser1Mail, 0: testUser1Mail,
}, },
backupData: []backupInfo{ backupData: []backupInfo{
@ -764,17 +765,17 @@ func (suite *BaseFinderUnitSuite) TestFindBases_CustomTags() {
table := []struct { table := []struct {
name string name string
input []Reasoner input []identity.Reasoner
tags map[string]string tags map[string]string
// Use this to denote which manifests in data should be expected. Allows // Use this to denote which manifests in data should be expected. Allows
// defining data in a table while not repeating things between data and // defining data in a table while not repeating things between data and
// expected. // expected.
expectedIdxs map[int][]Reasoner expectedIdxs map[int][]identity.Reasoner
}{ }{
{ {
name: "no tags specified", name: "no tags specified",
tags: nil, tags: nil,
expectedIdxs: map[int][]Reasoner{ expectedIdxs: map[int][]identity.Reasoner{
0: testUser1Mail, 0: testUser1Mail,
}, },
}, },
@ -784,14 +785,14 @@ func (suite *BaseFinderUnitSuite) TestFindBases_CustomTags() {
"fnords": "", "fnords": "",
"smarf": "", "smarf": "",
}, },
expectedIdxs: map[int][]Reasoner{ expectedIdxs: map[int][]identity.Reasoner{
0: testUser1Mail, 0: testUser1Mail,
}, },
}, },
{ {
name: "subset of custom tags", name: "subset of custom tags",
tags: map[string]string{"fnords": ""}, tags: map[string]string{"fnords": ""},
expectedIdxs: map[int][]Reasoner{ expectedIdxs: map[int][]identity.Reasoner{
0: testUser1Mail, 0: testUser1Mail,
}, },
}, },
@ -832,7 +833,7 @@ func checkManifestEntriesMatch(
t *testing.T, t *testing.T,
retSnaps []ManifestEntry, retSnaps []ManifestEntry,
allExpected []manifestInfo, allExpected []manifestInfo,
expectedIdxsAndReasons map[int][]Reasoner, expectedIdxsAndReasons map[int][]identity.Reasoner,
) { ) {
// Check the proper snapshot manifests were returned. // Check the proper snapshot manifests were returned.
expected := make([]*snapshot.Manifest, 0, len(expectedIdxsAndReasons)) expected := make([]*snapshot.Manifest, 0, len(expectedIdxsAndReasons))
@ -848,7 +849,7 @@ func checkManifestEntriesMatch(
assert.ElementsMatch(t, expected, got) assert.ElementsMatch(t, expected, got)
// Check the reasons for selecting each manifest are correct. // Check the reasons for selecting each manifest are correct.
expectedReasons := make(map[manifest.ID][]Reasoner, len(expectedIdxsAndReasons)) expectedReasons := make(map[manifest.ID][]identity.Reasoner, len(expectedIdxsAndReasons))
for idx, reasons := range expectedIdxsAndReasons { for idx, reasons := range expectedIdxsAndReasons {
expectedReasons[allExpected[idx].man.ID] = reasons expectedReasons[allExpected[idx].man.ID] = reasons
} }
@ -874,7 +875,7 @@ func checkBackupEntriesMatch(
t *testing.T, t *testing.T,
retBups []BackupEntry, retBups []BackupEntry,
allExpected []backupInfo, allExpected []backupInfo,
expectedIdxsAndReasons map[int][]Reasoner, expectedIdxsAndReasons map[int][]identity.Reasoner,
) { ) {
// Check the proper snapshot manifests were returned. // Check the proper snapshot manifests were returned.
expected := make([]*backup.Backup, 0, len(expectedIdxsAndReasons)) expected := make([]*backup.Backup, 0, len(expectedIdxsAndReasons))
@ -890,7 +891,7 @@ func checkBackupEntriesMatch(
assert.ElementsMatch(t, expected, got) assert.ElementsMatch(t, expected, got)
// Check the reasons for selecting each manifest are correct. // Check the reasons for selecting each manifest are correct.
expectedReasons := make(map[model.StableID][]Reasoner, len(expectedIdxsAndReasons)) expectedReasons := make(map[model.StableID][]identity.Reasoner, len(expectedIdxsAndReasons))
for idx, reasons := range expectedIdxsAndReasons { for idx, reasons := range expectedIdxsAndReasons {
expectedReasons[allExpected[idx].b.ID] = reasons expectedReasons[allExpected[idx].b.ID] = reasons
} }

View File

@ -7,6 +7,7 @@ import (
"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/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
@ -15,7 +16,7 @@ type (
BackupConsumer interface { BackupConsumer interface {
ConsumeBackupCollections( ConsumeBackupCollections(
ctx context.Context, ctx context.Context,
backupReasons []kopia.Reasoner, backupReasons []identity.Reasoner,
bases kopia.BackupBases, bases kopia.BackupBases,
cs []data.BackupCollection, cs []data.BackupCollection,
pmr prefixmatcher.StringSetReader, pmr prefixmatcher.StringSetReader,
@ -38,7 +39,7 @@ type (
BaseFinder interface { BaseFinder interface {
FindBases( FindBases(
ctx context.Context, ctx context.Context,
reasons []kopia.Reasoner, reasons []identity.Reasoner,
tags map[string]string, tags map[string]string,
) kopia.BackupBases ) kopia.BackupBases
} }

View File

@ -24,6 +24,7 @@ import (
exchMock "github.com/alcionai/corso/src/internal/m365/exchange/mock" exchMock "github.com/alcionai/corso/src/internal/m365/exchange/mock"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
@ -951,7 +952,7 @@ func makeManifestEntry(
service path.ServiceType, service path.ServiceType,
categories ...path.CategoryType, categories ...path.CategoryType,
) ManifestEntry { ) ManifestEntry {
var reasons []Reasoner var reasons []identity.Reasoner
for _, c := range categories { for _, c := range categories {
reasons = append(reasons, NewReason(tenant, resourceOwner, service, c)) reasons = append(reasons, NewReason(tenant, resourceOwner, service, c))
@ -2807,16 +2808,16 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSelectsMigrateSubt
migratedUser = "user_migrate" migratedUser = "user_migrate"
) )
oldPrefixPathEmail, err := path.ServicePrefix(testTenant, testUser, path.ExchangeService, path.EmailCategory) oldPrefixPathEmail, err := path.BuildPrefix(testTenant, testUser, path.ExchangeService, path.EmailCategory)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
newPrefixPathEmail, err := path.ServicePrefix(testTenant, migratedUser, path.ExchangeService, path.EmailCategory) newPrefixPathEmail, err := path.BuildPrefix(testTenant, migratedUser, path.ExchangeService, path.EmailCategory)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
oldPrefixPathCont, err := path.ServicePrefix(testTenant, testUser, path.ExchangeService, path.ContactsCategory) oldPrefixPathCont, err := path.BuildPrefix(testTenant, testUser, path.ExchangeService, path.ContactsCategory)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
newPrefixPathCont, err := path.ServicePrefix(testTenant, migratedUser, path.ExchangeService, path.ContactsCategory) newPrefixPathCont, err := path.BuildPrefix(testTenant, migratedUser, path.ExchangeService, path.ContactsCategory)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
var ( var (

View File

@ -23,6 +23,7 @@ import (
"github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/internal/operations/inject"
"github.com/alcionai/corso/src/internal/stats" "github.com/alcionai/corso/src/internal/stats"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/control/repository" "github.com/alcionai/corso/src/pkg/control/repository"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/logger"
@ -137,7 +138,7 @@ func (w *Wrapper) Close(ctx context.Context) error {
// complete backup of all data. // complete backup of all data.
func (w Wrapper) ConsumeBackupCollections( func (w Wrapper) ConsumeBackupCollections(
ctx context.Context, ctx context.Context,
backupReasons []Reasoner, backupReasons []identity.Reasoner,
bases BackupBases, bases BackupBases,
collections []data.BackupCollection, collections []data.BackupCollection,
globalExcludeSet prefixmatcher.StringSetReader, globalExcludeSet prefixmatcher.StringSetReader,

View File

@ -29,6 +29,7 @@ import (
"github.com/alcionai/corso/src/internal/m365/onedrive/metadata" "github.com/alcionai/corso/src/internal/m365/onedrive/metadata"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/control/repository" "github.com/alcionai/corso/src/pkg/control/repository"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/logger"
@ -800,16 +801,16 @@ func (suite *KopiaIntegrationSuite) TestBackupCollections() {
"brunhilda": "", "brunhilda": "",
} }
reasons := []Reasoner{ reasons := []identity.Reasoner{
NewReason( NewReason(
testTenant, testTenant,
suite.storePath1.ResourceOwner(), suite.storePath1.ProtectedResource(),
suite.storePath1.Service(), suite.storePath1.Service(),
suite.storePath1.Category(), suite.storePath1.Category(),
), ),
NewReason( NewReason(
testTenant, testTenant,
suite.storePath2.ResourceOwner(), suite.storePath2.ProtectedResource(),
suite.storePath2.Service(), suite.storePath2.Service(),
suite.storePath2.Category(), suite.storePath2.Category(),
), ),
@ -1072,10 +1073,10 @@ func (suite *KopiaIntegrationSuite) TestBackupCollections_NoDetailsForMeta() {
"brunhilda": "", "brunhilda": "",
} }
reasons := []Reasoner{ reasons := []identity.Reasoner{
NewReason( NewReason(
testTenant, testTenant,
storePath.ResourceOwner(), storePath.ProtectedResource(),
storePath.Service(), storePath.Service(),
storePath.Category()), storePath.Category()),
} }
@ -1267,7 +1268,7 @@ func (suite *KopiaIntegrationSuite) TestRestoreAfterCompressionChange() {
stats, _, _, err := w.ConsumeBackupCollections( stats, _, _, err := w.ConsumeBackupCollections(
ctx, ctx,
[]Reasoner{r}, []identity.Reasoner{r},
nil, nil,
[]data.BackupCollection{dc1, dc2}, []data.BackupCollection{dc1, dc2},
nil, nil,
@ -1385,7 +1386,7 @@ func (suite *KopiaIntegrationSuite) TestBackupCollections_ReaderError() {
stats, deets, _, err := suite.w.ConsumeBackupCollections( stats, deets, _, err := suite.w.ConsumeBackupCollections(
suite.ctx, suite.ctx,
[]Reasoner{r}, []identity.Reasoner{r},
nil, nil,
collections, collections,
nil, nil,
@ -1618,7 +1619,7 @@ func (suite *KopiaSimpleRepoIntegrationSuite) SetupTest() {
stats, deets, _, err := suite.w.ConsumeBackupCollections( stats, deets, _, err := suite.w.ConsumeBackupCollections(
suite.ctx, suite.ctx,
[]Reasoner{r}, []identity.Reasoner{r},
nil, nil,
collections, collections,
nil, nil,
@ -1745,11 +1746,11 @@ func (suite *KopiaSimpleRepoIntegrationSuite) TestBackupExcludeItem() {
stats, _, _, err := suite.w.ConsumeBackupCollections( stats, _, _, err := suite.w.ConsumeBackupCollections(
suite.ctx, suite.ctx,
[]Reasoner{r}, []identity.Reasoner{r},
NewMockBackupBases().WithMergeBases( NewMockBackupBases().WithMergeBases(
ManifestEntry{ ManifestEntry{
Manifest: man, Manifest: man,
Reasons: []Reasoner{r}, Reasons: []identity.Reasoner{r},
}, },
), ),
test.cols(), test.cols(),

View File

@ -79,7 +79,7 @@ func BaseCollections(
for cat := range categories { for cat := range categories {
ictx := clues.Add(ctx, "base_service", service, "base_category", cat) ictx := clues.Add(ctx, "base_service", service, "base_category", cat)
full, err := path.ServicePrefix(tenant, rOwner, service, cat) full, err := path.BuildPrefix(tenant, rOwner, service, cat)
if err != nil { if err != nil {
// Shouldn't happen. // Shouldn't happen.
err = clues.Wrap(err, "making path").WithClues(ictx) err = clues.Wrap(err, "making path").WithClues(ictx)

View File

@ -24,10 +24,10 @@ func (suite *CollectionsUnitSuite) TestNewPrefixCollection() {
serv := path.OneDriveService serv := path.OneDriveService
cat := path.FilesCategory cat := path.FilesCategory
p1, err := path.ServicePrefix("t", "ro1", serv, cat) p1, err := path.BuildPrefix("t", "ro1", serv, cat)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
p2, err := path.ServicePrefix("t", "ro2", serv, cat) p2, err := path.BuildPrefix("t", "ro2", serv, cat)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
items, err := path.Build("t", "ro", serv, cat, true, "fld", "itm") items, err := path.Build("t", "ro", serv, cat, true, "fld", "itm")

View File

@ -57,7 +57,7 @@ func ConsumeRestoreCollections(
ictx = clues.Add(ctx, ictx = clues.Add(ctx,
"category", category, "category", category,
"restore_location", clues.Hide(rcc.RestoreConfig.Location), "restore_location", clues.Hide(rcc.RestoreConfig.Location),
"protected_resource", clues.Hide(dc.FullPath().ResourceOwner()), "protected_resource", clues.Hide(dc.FullPath().ProtectedResource()),
"full_path", dc.FullPath()) "full_path", dc.FullPath())
) )

View File

@ -942,7 +942,7 @@ func checkHasCollections(
p, err := loc.ToDataLayerPath( p, err := loc.ToDataLayerPath(
fp.Tenant(), fp.Tenant(),
fp.ResourceOwner(), fp.ProtectedResource(),
fp.Service(), fp.Service(),
fp.Category(), fp.Category(),
false) false)

View File

@ -108,7 +108,7 @@ func migrationCollections(
// unlike exchange, which enumerates all folders on every // unlike exchange, which enumerates all folders on every
// backup, onedrive needs to force the owner PN -> ID migration // backup, onedrive needs to force the owner PN -> ID migration
mc, err := path.ServicePrefix( mc, err := path.BuildPrefix(
tenant, tenant,
bpc.ProtectedResource.ID(), bpc.ProtectedResource.ID(),
path.OneDriveService, path.OneDriveService,
@ -117,7 +117,7 @@ func migrationCollections(
return nil, clues.Wrap(err, "creating user id migration path") return nil, clues.Wrap(err, "creating user id migration path")
} }
mpc, err := path.ServicePrefix( mpc, err := path.BuildPrefix(
tenant, tenant,
bpc.ProtectedResource.Name(), bpc.ProtectedResource.Name(),
path.OneDriveService, path.OneDriveService,

View File

@ -50,7 +50,7 @@ func mustGetDefaultDriveID(
ctx context.Context, //revive:disable-line:context-as-argument ctx context.Context, //revive:disable-line:context-as-argument
ac api.Client, ac api.Client,
service path.ServiceType, service path.ServiceType,
resourceOwner string, protectedResource string,
) string { ) string {
var ( var (
err error err error
@ -59,9 +59,9 @@ func mustGetDefaultDriveID(
switch service { switch service {
case path.OneDriveService: case path.OneDriveService:
d, err = ac.Users().GetDefaultDrive(ctx, resourceOwner) d, err = ac.Users().GetDefaultDrive(ctx, protectedResource)
case path.SharePointService: case path.SharePointService:
d, err = ac.Sites().GetDefaultDrive(ctx, resourceOwner) d, err = ac.Sites().GetDefaultDrive(ctx, protectedResource)
default: default:
assert.FailNowf(t, "unknown service type %s", service.String()) assert.FailNowf(t, "unknown service type %s", service.String())
} }
@ -86,10 +86,10 @@ type suiteInfo interface {
PrimaryUser() (string, string) PrimaryUser() (string, string)
SecondaryUser() (string, string) SecondaryUser() (string, string)
TertiaryUser() (string, string) TertiaryUser() (string, string)
// ResourceOwner returns the resource owner to run the backup/restore // ProtectedResource returns the resource owner to run the backup/restore
// with. This can be different from the values used for permissions and it can // with. This can be different from the values used for permissions and it can
// also be a site. // also be a site.
ResourceOwner() string ProtectedResource() string
Service() path.ServiceType Service() path.ServiceType
Resource() resource.Category Resource() resource.Category
} }
@ -100,23 +100,23 @@ type oneDriveSuite interface {
} }
type suiteInfoImpl struct { type suiteInfoImpl struct {
ac api.Client ac api.Client
controller *Controller controller *Controller
resourceOwner string protectedResource string
resourceCategory resource.Category resourceCategory resource.Category
secondaryUser string secondaryUser string
secondaryUserID string secondaryUserID string
service path.ServiceType service path.ServiceType
tertiaryUser string tertiaryUser string
tertiaryUserID string tertiaryUserID string
user string user string
userID string userID string
} }
func NewSuiteInfoImpl( func NewSuiteInfoImpl(
t *testing.T, t *testing.T,
ctx context.Context, //revive:disable-line:context-as-argument ctx context.Context, //revive:disable-line:context-as-argument
resourceOwner string, protectedResource string,
service path.ServiceType, service path.ServiceType,
) suiteInfoImpl { ) suiteInfoImpl {
rsc := resource.Users rsc := resource.Users
@ -127,14 +127,14 @@ func NewSuiteInfoImpl(
ctrl := newController(ctx, t, rsc, path.OneDriveService) ctrl := newController(ctx, t, rsc, path.OneDriveService)
return suiteInfoImpl{ return suiteInfoImpl{
ac: ctrl.AC, ac: ctrl.AC,
controller: ctrl, controller: ctrl,
resourceOwner: resourceOwner, protectedResource: protectedResource,
resourceCategory: rsc, resourceCategory: rsc,
secondaryUser: tconfig.SecondaryM365UserID(t), secondaryUser: tconfig.SecondaryM365UserID(t),
service: service, service: service,
tertiaryUser: tconfig.TertiaryM365UserID(t), tertiaryUser: tconfig.TertiaryM365UserID(t),
user: tconfig.M365UserID(t), user: tconfig.M365UserID(t),
} }
} }
@ -158,8 +158,8 @@ func (si suiteInfoImpl) TertiaryUser() (string, string) {
return si.tertiaryUser, si.tertiaryUserID return si.tertiaryUser, si.tertiaryUserID
} }
func (si suiteInfoImpl) ResourceOwner() string { func (si suiteInfoImpl) ProtectedResource() string {
return si.resourceOwner return si.protectedResource
} }
func (si suiteInfoImpl) Service() path.ServiceType { func (si suiteInfoImpl) Service() path.ServiceType {
@ -388,7 +388,7 @@ func testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(
ctx, ctx,
suite.APIClient(), suite.APIClient(),
suite.Service(), suite.Service(),
suite.ResourceOwner()) suite.ProtectedResource())
rootPath := []string{ rootPath := []string{
odConsts.DrivesPathDir, odConsts.DrivesPathDir,
@ -526,7 +526,7 @@ func testRestoreAndBackupMultipleFilesAndFoldersNoPermissions(
t, t,
testData, testData,
suite.Tenant(), suite.Tenant(),
[]string{suite.ResourceOwner()}, []string{suite.ProtectedResource()},
control.DefaultOptions(), control.DefaultOptions(),
restoreCfg) restoreCfg)
}) })
@ -547,7 +547,7 @@ func testPermissionsRestoreAndBackup(suite oneDriveSuite, startVersion int) {
ctx, ctx,
suite.APIClient(), suite.APIClient(),
suite.Service(), suite.Service(),
suite.ResourceOwner()) suite.ProtectedResource())
fileName2 := "test-file2.txt" fileName2 := "test-file2.txt"
folderCName := "folder-c" folderCName := "folder-c"
@ -775,7 +775,7 @@ func testPermissionsRestoreAndBackup(suite oneDriveSuite, startVersion int) {
t, t,
testData, testData,
suite.Tenant(), suite.Tenant(),
[]string{suite.ResourceOwner()}, []string{suite.ProtectedResource()},
control.DefaultOptions(), control.DefaultOptions(),
restoreCfg) restoreCfg)
}) })
@ -796,7 +796,7 @@ func testRestoreNoPermissionsAndBackup(suite oneDriveSuite, startVersion int) {
ctx, ctx,
suite.APIClient(), suite.APIClient(),
suite.Service(), suite.Service(),
suite.ResourceOwner()) suite.ProtectedResource())
inputCols := []stub.ColInfo{ inputCols := []stub.ColInfo{
{ {
@ -867,7 +867,7 @@ func testRestoreNoPermissionsAndBackup(suite oneDriveSuite, startVersion int) {
t, t,
testData, testData,
suite.Tenant(), suite.Tenant(),
[]string{suite.ResourceOwner()}, []string{suite.ProtectedResource()},
control.DefaultOptions(), control.DefaultOptions(),
restoreCfg) restoreCfg)
}) })
@ -891,7 +891,7 @@ func testPermissionsInheritanceRestoreAndBackup(suite oneDriveSuite, startVersio
ctx, ctx,
suite.APIClient(), suite.APIClient(),
suite.Service(), suite.Service(),
suite.ResourceOwner()) suite.ProtectedResource())
folderAName := "custom" folderAName := "custom"
folderBName := "inherited" folderBName := "inherited"
@ -1072,7 +1072,7 @@ func testPermissionsInheritanceRestoreAndBackup(suite oneDriveSuite, startVersio
t, t,
testData, testData,
suite.Tenant(), suite.Tenant(),
[]string{suite.ResourceOwner()}, []string{suite.ProtectedResource()},
control.DefaultOptions(), control.DefaultOptions(),
restoreCfg) restoreCfg)
}) })
@ -1094,7 +1094,7 @@ func testLinkSharesInheritanceRestoreAndBackup(suite oneDriveSuite, startVersion
ctx, ctx,
suite.APIClient(), suite.APIClient(),
suite.Service(), suite.Service(),
suite.ResourceOwner()) suite.ProtectedResource())
folderAName := "custom" folderAName := "custom"
folderBName := "inherited" folderBName := "inherited"
@ -1267,7 +1267,7 @@ func testLinkSharesInheritanceRestoreAndBackup(suite oneDriveSuite, startVersion
t, t,
testData, testData,
suite.Tenant(), suite.Tenant(),
[]string{suite.ResourceOwner()}, []string{suite.ProtectedResource()},
control.DefaultOptions(), control.DefaultOptions(),
restoreCfg) restoreCfg)
}) })
@ -1289,7 +1289,7 @@ func testRestoreFolderNamedFolderRegression(
ctx, ctx,
suite.APIClient(), suite.APIClient(),
suite.Service(), suite.Service(),
suite.ResourceOwner()) suite.ProtectedResource())
rootPath := []string{ rootPath := []string{
odConsts.DrivesPathDir, odConsts.DrivesPathDir,
@ -1383,7 +1383,7 @@ func testRestoreFolderNamedFolderRegression(
t, t,
testData, testData,
suite.Tenant(), suite.Tenant(),
[]string{suite.ResourceOwner()}, []string{suite.ProtectedResource()},
control.DefaultOptions(), control.DefaultOptions(),
restoreCfg) restoreCfg)
}) })

View File

@ -219,7 +219,7 @@ func (sc *Collection) retrieveLists(
lists, err := loadSiteLists( lists, err := loadSiteLists(
ctx, ctx,
sc.client.Stable, sc.client.Stable,
sc.fullPath.ResourceOwner(), sc.fullPath.ProtectedResource(),
sc.jobs, sc.jobs,
errs) errs)
if err != nil { if err != nil {
@ -282,14 +282,14 @@ func (sc *Collection) retrievePages(
return metrics, clues.New("beta service required").WithClues(ctx) return metrics, clues.New("beta service required").WithClues(ctx)
} }
parent, err := as.GetByID(ctx, sc.fullPath.ResourceOwner()) parent, err := as.GetByID(ctx, sc.fullPath.ProtectedResource())
if err != nil { if err != nil {
return metrics, err return metrics, err
} }
root := ptr.Val(parent.GetWebUrl()) root := ptr.Val(parent.GetWebUrl())
pages, err := betaAPI.GetSitePages(ctx, betaService, sc.fullPath.ResourceOwner(), sc.jobs, errs) pages, err := betaAPI.GetSitePages(ctx, betaService, sc.fullPath.ProtectedResource(), sc.jobs, errs)
if err != nil { if err != nil {
return metrics, err return metrics, err
} }

View File

@ -69,7 +69,7 @@ func ConsumeRestoreCollections(
ictx = clues.Add(ctx, ictx = clues.Add(ctx,
"category", category, "category", category,
"restore_location", clues.Hide(rcc.RestoreConfig.Location), "restore_location", clues.Hide(rcc.RestoreConfig.Location),
"resource_owner", clues.Hide(dc.FullPath().ResourceOwner()), "resource_owner", clues.Hide(dc.FullPath().ProtectedResource()),
"full_path", dc.FullPath()) "full_path", dc.FullPath())
) )
@ -219,7 +219,7 @@ func RestoreListCollection(
var ( var (
metrics = support.CollectionMetrics{} metrics = support.CollectionMetrics{}
directory = dc.FullPath() directory = dc.FullPath()
siteID = directory.ResourceOwner() siteID = directory.ProtectedResource()
items = dc.Items(ctx, errs) items = dc.Items(ctx, errs)
el = errs.Local() el = errs.Local()
) )
@ -292,7 +292,7 @@ func RestorePageCollection(
var ( var (
metrics = support.CollectionMetrics{} metrics = support.CollectionMetrics{}
directory = dc.FullPath() directory = dc.FullPath()
siteID = directory.ResourceOwner() siteID = directory.ProtectedResource()
) )
trace.Log(ctx, "m365:sharepoint:restorePageCollection", directory.String()) trace.Log(ctx, "m365:sharepoint:restorePageCollection", directory.String())

View File

@ -183,7 +183,7 @@ func backupOutputPathFromRestore(
return path.Build( return path.Build(
inputPath.Tenant(), inputPath.Tenant(),
inputPath.ResourceOwner(), inputPath.ProtectedResource(),
inputPath.Service(), inputPath.Service(),
inputPath.Category(), inputPath.Category(),
false, false,

View File

@ -26,6 +26,7 @@ import (
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/backup" "github.com/alcionai/corso/src/pkg/backup"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/count" "github.com/alcionai/corso/src/pkg/count"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
@ -276,11 +277,17 @@ func (op *BackupOperation) do(
detailsStore streamstore.Streamer, detailsStore streamstore.Streamer,
backupID model.StableID, backupID model.StableID,
) (*details.Builder, error) { ) (*details.Builder, error) {
var ( lastBackupVersion := version.NoBackup
reasons = selectorToReasons(op.account.ID(), op.Selectors, false)
fallbackReasons = makeFallbackReasons(op.account.ID(), op.Selectors) reasons, err := op.Selectors.Reasons(op.account.ID(), false)
lastBackupVersion = version.NoBackup if err != nil {
) return nil, clues.Wrap(err, "getting reasons")
}
fallbackReasons, err := makeFallbackReasons(op.account.ID(), op.Selectors)
if err != nil {
return nil, clues.Wrap(err, "getting fallback reasons")
}
logger.Ctx(ctx).With( logger.Ctx(ctx).With(
"control_options", op.Options, "control_options", op.Options,
@ -367,13 +374,14 @@ func (op *BackupOperation) do(
return deets, nil return deets, nil
} }
func makeFallbackReasons(tenant string, sel selectors.Selector) []kopia.Reasoner { func makeFallbackReasons(tenant string, sel selectors.Selector) ([]identity.Reasoner, error) {
if sel.PathService() != path.SharePointService && if sel.PathService() != path.SharePointService &&
sel.DiscreteOwner != sel.DiscreteOwnerName { sel.DiscreteOwner != sel.DiscreteOwnerName {
return selectorToReasons(tenant, sel, true) return sel.Reasons(tenant, true)
} }
return nil // return nil for fallback reasons since a nil value will no-op.
return nil, nil
} }
// checker to see if conditions are correct for incremental backup behavior such as // checker to see if conditions are correct for incremental backup behavior such as
@ -415,41 +423,12 @@ func produceBackupDataCollections(
// Consumer funcs // Consumer funcs
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
func selectorToReasons(
tenant string,
sel selectors.Selector,
useOwnerNameForID bool,
) []kopia.Reasoner {
service := sel.PathService()
reasons := []kopia.Reasoner{}
pcs, err := sel.PathCategories()
if err != nil {
// This is technically safe, it's just that the resulting backup won't be
// usable as a base for future incremental backups.
return nil
}
owner := sel.DiscreteOwner
if useOwnerNameForID {
owner = sel.DiscreteOwnerName
}
for _, sl := range [][]path.CategoryType{pcs.Includes, pcs.Filters} {
for _, cat := range sl {
reasons = append(reasons, kopia.NewReason(tenant, owner, service, cat))
}
}
return reasons
}
// calls kopia to backup the collections of data // calls kopia to backup the collections of data
func consumeBackupCollections( func consumeBackupCollections(
ctx context.Context, ctx context.Context,
bc kinject.BackupConsumer, bc kinject.BackupConsumer,
tenantID string, tenantID string,
reasons []kopia.Reasoner, reasons []identity.Reasoner,
bbs kopia.BackupBases, bbs kopia.BackupBases,
cs []data.BackupCollection, cs []data.BackupCollection,
pmr prefixmatcher.StringSetReader, pmr prefixmatcher.StringSetReader,
@ -501,9 +480,9 @@ func consumeBackupCollections(
return kopiaStats, deets, itemsSourcedFromBase, err return kopiaStats, deets, itemsSourcedFromBase, err
} }
func matchesReason(reasons []kopia.Reasoner, p path.Path) bool { func matchesReason(reasons []identity.Reasoner, p path.Path) bool {
for _, reason := range reasons { for _, reason := range reasons {
if p.ResourceOwner() == reason.ProtectedResource() && if p.ProtectedResource() == reason.ProtectedResource() &&
p.Service() == reason.Service() && p.Service() == reason.Service() &&
p.Category() == reason.Category() { p.Category() == reason.Category() {
return true return true

View File

@ -27,6 +27,7 @@ import (
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/backup" "github.com/alcionai/corso/src/pkg/backup"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
@ -107,7 +108,7 @@ func checkPaths(t *testing.T, expected, got []path.Path) {
type mockBackupConsumer struct { type mockBackupConsumer struct {
checkFunc func( checkFunc func(
backupReasons []kopia.Reasoner, backupReasons []identity.Reasoner,
bases kopia.BackupBases, bases kopia.BackupBases,
cs []data.BackupCollection, cs []data.BackupCollection,
tags map[string]string, tags map[string]string,
@ -116,7 +117,7 @@ type mockBackupConsumer struct {
func (mbu mockBackupConsumer) ConsumeBackupCollections( func (mbu mockBackupConsumer) ConsumeBackupCollections(
ctx context.Context, ctx context.Context,
backupReasons []kopia.Reasoner, backupReasons []identity.Reasoner,
bases kopia.BackupBases, bases kopia.BackupBases,
cs []data.BackupCollection, cs []data.BackupCollection,
excluded prefixmatcher.StringSetReader, excluded prefixmatcher.StringSetReader,
@ -406,7 +407,7 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_ConsumeBackupDataCollections
path.ExchangeService, path.ExchangeService,
path.ContactsCategory) path.ContactsCategory)
reasons = []kopia.Reasoner{ reasons = []identity.Reasoner{
emailReason, emailReason,
contactsReason, contactsReason,
} }
@ -421,13 +422,13 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_ConsumeBackupDataCollections
bases = kopia.NewMockBackupBases().WithMergeBases( bases = kopia.NewMockBackupBases().WithMergeBases(
kopia.ManifestEntry{ kopia.ManifestEntry{
Manifest: manifest1, Manifest: manifest1,
Reasons: []kopia.Reasoner{ Reasons: []identity.Reasoner{
emailReason, emailReason,
}, },
}).WithAssistBases( }).WithAssistBases(
kopia.ManifestEntry{ kopia.ManifestEntry{
Manifest: manifest2, Manifest: manifest2,
Reasons: []kopia.Reasoner{ Reasons: []identity.Reasoner{
contactsReason, contactsReason,
}, },
}) })
@ -441,7 +442,7 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_ConsumeBackupDataCollections
mbu := &mockBackupConsumer{ mbu := &mockBackupConsumer{
checkFunc: func( checkFunc: func(
backupReasons []kopia.Reasoner, backupReasons []identity.Reasoner,
gotBases kopia.BackupBases, gotBases kopia.BackupBases,
cs []data.BackupCollection, cs []data.BackupCollection,
gotTags map[string]string, gotTags map[string]string,
@ -537,12 +538,12 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems
pathReason1 = kopia.NewReason( pathReason1 = kopia.NewReason(
"", "",
itemPath1.ResourceOwner(), itemPath1.ProtectedResource(),
itemPath1.Service(), itemPath1.Service(),
itemPath1.Category()) itemPath1.Category())
pathReason3 = kopia.NewReason( pathReason3 = kopia.NewReason(
"", "",
itemPath3.ResourceOwner(), itemPath3.ProtectedResource(),
itemPath3.Service(), itemPath3.Service(),
itemPath3.Category()) itemPath3.Category())
) )
@ -590,7 +591,7 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems
}, },
DetailsID: "foo", DetailsID: "foo",
}, },
Reasons: []kopia.Reasoner{ Reasons: []identity.Reasoner{
pathReason1, pathReason1,
}, },
}, },
@ -609,7 +610,7 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems
inputBackups: []kopia.BackupEntry{ inputBackups: []kopia.BackupEntry{
{ {
Backup: &backup1, Backup: &backup1,
Reasons: []kopia.Reasoner{ Reasons: []identity.Reasoner{
pathReason1, pathReason1,
}, },
}, },
@ -636,13 +637,13 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems
inputBackups: []kopia.BackupEntry{ inputBackups: []kopia.BackupEntry{
{ {
Backup: &backup1, Backup: &backup1,
Reasons: []kopia.Reasoner{ Reasons: []identity.Reasoner{
pathReason1, pathReason1,
}, },
}, },
{ {
Backup: &backup1, Backup: &backup1,
Reasons: []kopia.Reasoner{ Reasons: []identity.Reasoner{
pathReason1, pathReason1,
}, },
}, },
@ -669,7 +670,7 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems
inputBackups: []kopia.BackupEntry{ inputBackups: []kopia.BackupEntry{
{ {
Backup: &backup1, Backup: &backup1,
Reasons: []kopia.Reasoner{ Reasons: []identity.Reasoner{
pathReason1, pathReason1,
}, },
}, },
@ -684,7 +685,7 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems
[]string{ []string{
itemPath1.Tenant(), itemPath1.Tenant(),
itemPath1.Service().String(), itemPath1.Service().String(),
itemPath1.ResourceOwner(), itemPath1.ProtectedResource(),
path.UnknownCategory.String(), path.UnknownCategory.String(),
}, },
itemPath1.Folders()..., itemPath1.Folders()...,
@ -713,7 +714,7 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems
[]string{ []string{
itemPath1.Tenant(), itemPath1.Tenant(),
path.OneDriveService.String(), path.OneDriveService.String(),
itemPath1.ResourceOwner(), itemPath1.ProtectedResource(),
path.FilesCategory.String(), path.FilesCategory.String(),
"personal", "personal",
"item1", "item1",
@ -728,7 +729,7 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems
inputBackups: []kopia.BackupEntry{ inputBackups: []kopia.BackupEntry{
{ {
Backup: &backup1, Backup: &backup1,
Reasons: []kopia.Reasoner{ Reasons: []identity.Reasoner{
pathReason1, pathReason1,
}, },
}, },
@ -755,7 +756,7 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems
inputBackups: []kopia.BackupEntry{ inputBackups: []kopia.BackupEntry{
{ {
Backup: &backup1, Backup: &backup1,
Reasons: []kopia.Reasoner{ Reasons: []identity.Reasoner{
pathReason1, pathReason1,
}, },
}, },
@ -785,7 +786,7 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems
inputBackups: []kopia.BackupEntry{ inputBackups: []kopia.BackupEntry{
{ {
Backup: &backup1, Backup: &backup1,
Reasons: []kopia.Reasoner{ Reasons: []identity.Reasoner{
pathReason1, pathReason1,
}, },
}, },
@ -815,7 +816,7 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems
inputBackups: []kopia.BackupEntry{ inputBackups: []kopia.BackupEntry{
{ {
Backup: &backup1, Backup: &backup1,
Reasons: []kopia.Reasoner{ Reasons: []identity.Reasoner{
pathReason1, pathReason1,
}, },
}, },
@ -846,7 +847,7 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems
inputBackups: []kopia.BackupEntry{ inputBackups: []kopia.BackupEntry{
{ {
Backup: &backup1, Backup: &backup1,
Reasons: []kopia.Reasoner{ Reasons: []identity.Reasoner{
pathReason1, pathReason1,
}, },
}, },
@ -877,13 +878,13 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsItems
inputBackups: []kopia.BackupEntry{ inputBackups: []kopia.BackupEntry{
{ {
Backup: &backup1, Backup: &backup1,
Reasons: []kopia.Reasoner{ Reasons: []identity.Reasoner{
pathReason1, pathReason1,
}, },
}, },
{ {
Backup: &backup2, Backup: &backup2,
Reasons: []kopia.Reasoner{ Reasons: []identity.Reasoner{
pathReason3, pathReason3,
}, },
}, },
@ -972,7 +973,7 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsFolde
pathReason1 = kopia.NewReason( pathReason1 = kopia.NewReason(
"", "",
itemPath1.ResourceOwner(), itemPath1.ProtectedResource(),
itemPath1.Service(), itemPath1.Service(),
itemPath1.Category()) itemPath1.Category())
@ -983,7 +984,7 @@ func (suite *BackupOpUnitSuite) TestBackupOperation_MergeBackupDetails_AddsFolde
}, },
DetailsID: "did1", DetailsID: "did1",
}, },
Reasons: []kopia.Reasoner{ Reasons: []identity.Reasoner{
pathReason1, pathReason1,
}, },
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/alcionai/corso/src/internal/kopia" "github.com/alcionai/corso/src/internal/kopia"
"github.com/alcionai/corso/src/internal/kopia/inject" "github.com/alcionai/corso/src/internal/kopia/inject"
"github.com/alcionai/corso/src/internal/m365/graph" "github.com/alcionai/corso/src/internal/m365/graph"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
@ -23,7 +24,7 @@ func produceManifestsAndMetadata(
ctx context.Context, ctx context.Context,
bf inject.BaseFinder, bf inject.BaseFinder,
rp inject.RestoreProducer, rp inject.RestoreProducer,
reasons, fallbackReasons []kopia.Reasoner, reasons, fallbackReasons []identity.Reasoner,
tenantID string, tenantID string,
getMetadata bool, getMetadata bool,
) (kopia.BackupBases, []data.RestoreCollection, bool, error) { ) (kopia.BackupBases, []data.RestoreCollection, bool, error) {
@ -47,7 +48,7 @@ func produceManifestsAndMetadata(
bb = bb.MergeBackupBases( bb = bb.MergeBackupBases(
ctx, ctx,
fbb, fbb,
func(r kopia.Reasoner) string { func(r identity.Reasoner) string {
return r.Service().String() + r.Category().String() return r.Service().String() + r.Category().String()
}) })

View File

@ -15,6 +15,7 @@ import (
"github.com/alcionai/corso/src/internal/model" "github.com/alcionai/corso/src/internal/model"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/backup" "github.com/alcionai/corso/src/pkg/backup"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
@ -47,7 +48,7 @@ type mockBackupFinder struct {
func (bf *mockBackupFinder) FindBases( func (bf *mockBackupFinder) FindBases(
_ context.Context, _ context.Context,
reasons []kopia.Reasoner, reasons []identity.Reasoner,
_ map[string]string, _ map[string]string,
) kopia.BackupBases { ) kopia.BackupBases {
if len(reasons) == 0 { if len(reasons) == 0 {
@ -102,7 +103,7 @@ func (suite *OperationsManifestsUnitSuite) TestCollectMetadata() {
table := []struct { table := []struct {
name string name string
manID string manID string
reasons []kopia.Reasoner reasons []identity.Reasoner
fileNames []string fileNames []string
expectPaths func(*testing.T, []string) []path.Path expectPaths func(*testing.T, []string) []path.Path
expectErr error expectErr error
@ -110,7 +111,7 @@ func (suite *OperationsManifestsUnitSuite) TestCollectMetadata() {
{ {
name: "single reason, single file", name: "single reason, single file",
manID: "single single", manID: "single single",
reasons: []kopia.Reasoner{ reasons: []identity.Reasoner{
kopia.NewReason(tid, ro, path.ExchangeService, path.EmailCategory), kopia.NewReason(tid, ro, path.ExchangeService, path.EmailCategory),
}, },
expectPaths: func(t *testing.T, files []string) []path.Path { expectPaths: func(t *testing.T, files []string) []path.Path {
@ -129,7 +130,7 @@ func (suite *OperationsManifestsUnitSuite) TestCollectMetadata() {
{ {
name: "single reason, multiple files", name: "single reason, multiple files",
manID: "single multi", manID: "single multi",
reasons: []kopia.Reasoner{ reasons: []identity.Reasoner{
kopia.NewReason(tid, ro, path.ExchangeService, path.EmailCategory), kopia.NewReason(tid, ro, path.ExchangeService, path.EmailCategory),
}, },
expectPaths: func(t *testing.T, files []string) []path.Path { expectPaths: func(t *testing.T, files []string) []path.Path {
@ -148,7 +149,7 @@ func (suite *OperationsManifestsUnitSuite) TestCollectMetadata() {
{ {
name: "multiple reasons, single file", name: "multiple reasons, single file",
manID: "multi single", manID: "multi single",
reasons: []kopia.Reasoner{ reasons: []identity.Reasoner{
kopia.NewReason(tid, ro, path.ExchangeService, path.EmailCategory), kopia.NewReason(tid, ro, path.ExchangeService, path.EmailCategory),
kopia.NewReason(tid, ro, path.ExchangeService, path.ContactsCategory), kopia.NewReason(tid, ro, path.ExchangeService, path.ContactsCategory),
}, },
@ -171,7 +172,7 @@ func (suite *OperationsManifestsUnitSuite) TestCollectMetadata() {
{ {
name: "multiple reasons, multiple file", name: "multiple reasons, multiple file",
manID: "multi multi", manID: "multi multi",
reasons: []kopia.Reasoner{ reasons: []identity.Reasoner{
kopia.NewReason(tid, ro, path.ExchangeService, path.EmailCategory), kopia.NewReason(tid, ro, path.ExchangeService, path.EmailCategory),
kopia.NewReason(tid, ro, path.ExchangeService, path.ContactsCategory), kopia.NewReason(tid, ro, path.ExchangeService, path.ContactsCategory),
}, },
@ -219,8 +220,8 @@ func buildReasons(
ro string, ro string,
service path.ServiceType, service path.ServiceType,
cats ...path.CategoryType, cats ...path.CategoryType,
) []kopia.Reasoner { ) []identity.Reasoner {
var reasons []kopia.Reasoner var reasons []identity.Reasoner
for _, cat := range cats { for _, cat := range cats {
reasons = append( reasons = append(
@ -252,7 +253,7 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() {
name string name string
bf *mockBackupFinder bf *mockBackupFinder
rp mockRestoreProducer rp mockRestoreProducer
reasons []kopia.Reasoner reasons []identity.Reasoner
getMeta bool getMeta bool
assertErr assert.ErrorAssertionFunc assertErr assert.ErrorAssertionFunc
assertB assert.BoolAssertionFunc assertB assert.BoolAssertionFunc
@ -263,7 +264,7 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() {
{ {
name: "don't get metadata, no mans", name: "don't get metadata, no mans",
rp: mockRestoreProducer{}, rp: mockRestoreProducer{},
reasons: []kopia.Reasoner{}, reasons: []identity.Reasoner{},
getMeta: false, getMeta: false,
assertErr: assert.NoError, assertErr: assert.NoError,
assertB: assert.False, assertB: assert.False,
@ -280,7 +281,7 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() {
}, },
}, },
rp: mockRestoreProducer{}, rp: mockRestoreProducer{},
reasons: []kopia.Reasoner{ reasons: []identity.Reasoner{
kopia.NewReason("", ro, path.ExchangeService, path.EmailCategory), kopia.NewReason("", ro, path.ExchangeService, path.EmailCategory),
}, },
getMeta: false, getMeta: false,
@ -301,7 +302,7 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() {
}, },
}, },
rp: mockRestoreProducer{}, rp: mockRestoreProducer{},
reasons: []kopia.Reasoner{ reasons: []identity.Reasoner{
kopia.NewReason("", ro, path.ExchangeService, path.EmailCategory), kopia.NewReason("", ro, path.ExchangeService, path.EmailCategory),
}, },
getMeta: true, getMeta: true,
@ -329,7 +330,7 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() {
"id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id1"}}}, "id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id1"}}},
}, },
}, },
reasons: []kopia.Reasoner{ reasons: []identity.Reasoner{
kopia.NewReason("", ro, path.ExchangeService, path.EmailCategory), kopia.NewReason("", ro, path.ExchangeService, path.EmailCategory),
kopia.NewReason("", ro, path.ExchangeService, path.ContactsCategory), kopia.NewReason("", ro, path.ExchangeService, path.ContactsCategory),
}, },
@ -377,7 +378,7 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() {
"id2": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id2"}}}, "id2": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id2"}}},
}, },
}, },
reasons: []kopia.Reasoner{ reasons: []identity.Reasoner{
kopia.NewReason("", ro, path.ExchangeService, path.EmailCategory), kopia.NewReason("", ro, path.ExchangeService, path.EmailCategory),
}, },
getMeta: true, getMeta: true,
@ -406,7 +407,7 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() {
"id2": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id2"}}}, "id2": {data.NoFetchRestoreCollection{Collection: mockColl{id: "id2"}}},
}, },
}, },
reasons: []kopia.Reasoner{ reasons: []identity.Reasoner{
kopia.NewReason("", ro, path.ExchangeService, path.EmailCategory), kopia.NewReason("", ro, path.ExchangeService, path.EmailCategory),
}, },
getMeta: true, getMeta: true,
@ -428,7 +429,7 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() {
}, },
}, },
rp: mockRestoreProducer{err: assert.AnError}, rp: mockRestoreProducer{err: assert.AnError},
reasons: []kopia.Reasoner{ reasons: []identity.Reasoner{
kopia.NewReason("", ro, path.ExchangeService, path.EmailCategory), kopia.NewReason("", ro, path.ExchangeService, path.EmailCategory),
}, },
getMeta: true, getMeta: true,
@ -548,8 +549,8 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata_Fallb
name string name string
bf *mockBackupFinder bf *mockBackupFinder
rp mockRestoreProducer rp mockRestoreProducer
reasons []kopia.Reasoner reasons []identity.Reasoner
fallbackReasons []kopia.Reasoner fallbackReasons []identity.Reasoner
getMeta bool getMeta bool
assertErr assert.ErrorAssertionFunc assertErr assert.ErrorAssertionFunc
assertB assert.BoolAssertionFunc assertB assert.BoolAssertionFunc
@ -568,7 +569,7 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata_Fallb
}, },
}, },
rp: mockRestoreProducer{}, rp: mockRestoreProducer{},
fallbackReasons: []kopia.Reasoner{fbEmailReason}, fallbackReasons: []identity.Reasoner{fbEmailReason},
getMeta: false, getMeta: false,
assertErr: assert.NoError, assertErr: assert.NoError,
assertB: assert.False, assertB: assert.False,
@ -593,7 +594,7 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata_Fallb
"fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}}, "fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}},
}, },
}, },
fallbackReasons: []kopia.Reasoner{fbEmailReason}, fallbackReasons: []identity.Reasoner{fbEmailReason},
getMeta: true, getMeta: true,
assertErr: assert.NoError, assertErr: assert.NoError,
assertB: assert.True, assertB: assert.True,
@ -624,8 +625,8 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata_Fallb
"fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}}, "fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}},
}, },
}, },
reasons: []kopia.Reasoner{emailReason}, reasons: []identity.Reasoner{emailReason},
fallbackReasons: []kopia.Reasoner{fbEmailReason}, fallbackReasons: []identity.Reasoner{fbEmailReason},
getMeta: true, getMeta: true,
assertErr: assert.NoError, assertErr: assert.NoError,
assertB: assert.True, assertB: assert.True,
@ -652,8 +653,8 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata_Fallb
"fb_id2": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id2"}}}, "fb_id2": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id2"}}},
}, },
}, },
reasons: []kopia.Reasoner{emailReason}, reasons: []identity.Reasoner{emailReason},
fallbackReasons: []kopia.Reasoner{fbEmailReason}, fallbackReasons: []identity.Reasoner{fbEmailReason},
getMeta: true, getMeta: true,
assertErr: assert.NoError, assertErr: assert.NoError,
assertB: assert.True, assertB: assert.True,
@ -688,8 +689,8 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata_Fallb
"fb_id2": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id2"}}}, "fb_id2": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id2"}}},
}, },
}, },
reasons: []kopia.Reasoner{emailReason}, reasons: []identity.Reasoner{emailReason},
fallbackReasons: []kopia.Reasoner{fbEmailReason}, fallbackReasons: []identity.Reasoner{fbEmailReason},
getMeta: true, getMeta: true,
assertErr: assert.NoError, assertErr: assert.NoError,
assertB: assert.True, assertB: assert.True,
@ -720,8 +721,8 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata_Fallb
"fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}}, "fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}},
}, },
}, },
reasons: []kopia.Reasoner{emailReason}, reasons: []identity.Reasoner{emailReason},
fallbackReasons: []kopia.Reasoner{fbEmailReason}, fallbackReasons: []identity.Reasoner{fbEmailReason},
getMeta: true, getMeta: true,
assertErr: assert.NoError, assertErr: assert.NoError,
assertB: assert.True, assertB: assert.True,
@ -752,8 +753,8 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata_Fallb
"fb_id2": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id2"}}}, "fb_id2": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id2"}}},
}, },
}, },
reasons: []kopia.Reasoner{emailReason}, reasons: []identity.Reasoner{emailReason},
fallbackReasons: []kopia.Reasoner{fbEmailReason}, fallbackReasons: []identity.Reasoner{fbEmailReason},
getMeta: true, getMeta: true,
assertErr: assert.NoError, assertErr: assert.NoError,
assertB: assert.True, assertB: assert.True,
@ -782,11 +783,11 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata_Fallb
"fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}}, "fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}},
}, },
}, },
reasons: []kopia.Reasoner{ reasons: []identity.Reasoner{
emailReason, emailReason,
kopia.NewReason("", ro, path.ExchangeService, path.ContactsCategory), kopia.NewReason("", ro, path.ExchangeService, path.ContactsCategory),
}, },
fallbackReasons: []kopia.Reasoner{ fallbackReasons: []identity.Reasoner{
fbEmailReason, fbEmailReason,
kopia.NewReason("", fbro, path.ExchangeService, path.ContactsCategory), kopia.NewReason("", fbro, path.ExchangeService, path.ContactsCategory),
}, },
@ -818,8 +819,8 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata_Fallb
"fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}}, "fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}},
}, },
}, },
reasons: []kopia.Reasoner{emailReason}, reasons: []identity.Reasoner{emailReason},
fallbackReasons: []kopia.Reasoner{ fallbackReasons: []identity.Reasoner{
kopia.NewReason("", fbro, path.ExchangeService, path.ContactsCategory), kopia.NewReason("", fbro, path.ExchangeService, path.ContactsCategory),
}, },
getMeta: true, getMeta: true,
@ -853,11 +854,11 @@ func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata_Fallb
"fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}}, "fb_id1": {data.NoFetchRestoreCollection{Collection: mockColl{id: "fb_id1"}}},
}, },
}, },
reasons: []kopia.Reasoner{ reasons: []identity.Reasoner{
emailReason, emailReason,
kopia.NewReason("", ro, path.ExchangeService, path.ContactsCategory), kopia.NewReason("", ro, path.ExchangeService, path.ContactsCategory),
}, },
fallbackReasons: []kopia.Reasoner{ fallbackReasons: []identity.Reasoner{
fbEmailReason, fbEmailReason,
kopia.NewReason("", fbro, path.ExchangeService, path.ContactsCategory), kopia.NewReason("", fbro, path.ExchangeService, path.ContactsCategory),
}, },

View File

@ -42,9 +42,9 @@ func locationRef(
func basicLocationPath(repoRef path.Path, locRef *path.Builder) (path.Path, error) { func basicLocationPath(repoRef path.Path, locRef *path.Builder) (path.Path, error) {
if len(locRef.Elements()) == 0 { if len(locRef.Elements()) == 0 {
res, err := path.ServicePrefix( res, err := path.BuildPrefix(
repoRef.Tenant(), repoRef.Tenant(),
repoRef.ResourceOwner(), repoRef.ProtectedResource(),
repoRef.Service(), repoRef.Service(),
repoRef.Category()) repoRef.Category())
if err != nil { if err != nil {
@ -56,7 +56,7 @@ func basicLocationPath(repoRef path.Path, locRef *path.Builder) (path.Path, erro
return locRef.ToDataLayerPath( return locRef.ToDataLayerPath(
repoRef.Tenant(), repoRef.Tenant(),
repoRef.ResourceOwner(), repoRef.ProtectedResource(),
repoRef.Service(), repoRef.Service(),
repoRef.Category(), repoRef.Category(),
false) false)

View File

@ -40,7 +40,7 @@ func (suite *RestorePathTransformerUnitSuite) TestGetPaths() {
Append( Append(
repoRef.Tenant(), repoRef.Tenant(),
repoRef.Service().String(), repoRef.Service().String(),
repoRef.ResourceOwner(), repoRef.ProtectedResource(),
repoRef.Category().String()). repoRef.Category().String()).
Append(unescapedFolders...). Append(unescapedFolders...).
String() String()

View File

@ -347,7 +347,7 @@ func testExchangeContinuousBackups(suite *ExchangeBackupIntgSuite, toggles contr
// }, // },
} }
rrPfx, err := path.ServicePrefix(acct.ID(), uidn.ID(), service, path.EmailCategory) rrPfx, err := path.BuildPrefix(acct.ID(), uidn.ID(), service, path.EmailCategory)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
// strip the category from the prefix; we primarily want the tenant and resource owner. // strip the category from the prefix; we primarily want the tenant and resource owner.

View File

@ -32,6 +32,7 @@ import (
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/backup" "github.com/alcionai/corso/src/pkg/backup"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/control/repository" "github.com/alcionai/corso/src/pkg/control/repository"
"github.com/alcionai/corso/src/pkg/count" "github.com/alcionai/corso/src/pkg/count"
@ -251,7 +252,7 @@ func checkBackupIsInManifests(
bf, err := kw.NewBaseFinder(sw) bf, err := kw.NewBaseFinder(sw)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
mans := bf.FindBases(ctx, []kopia.Reasoner{r}, tags) mans := bf.FindBases(ctx, []identity.Reasoner{r}, tags)
for _, man := range mans.MergeBases() { for _, man := range mans.MergeBases() {
bID, ok := man.GetTag(kopia.TagBackupID) bID, ok := man.GetTag(kopia.TagBackupID)
if !assert.Truef(t, ok, "snapshot manifest %s missing backup ID tag", man.ID) { if !assert.Truef(t, ok, "snapshot manifest %s missing backup ID tag", man.ID) {

View File

@ -213,7 +213,7 @@ func runDriveIncrementalTest(
} }
) )
rrPfx, err := path.ServicePrefix(atid, roidn.ID(), service, category) rrPfx, err := path.BuildPrefix(atid, roidn.ID(), service, category)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
// strip the category from the prefix; we primarily want the tenant and resource owner. // strip the category from the prefix; we primarily want the tenant and resource owner.

View File

@ -104,7 +104,7 @@ func (p repoRefAndLocRef) locationAsRepoRef() path.Path {
res, err := tmp.ToDataLayerPath( res, err := tmp.ToDataLayerPath(
p.RR.Tenant(), p.RR.Tenant(),
p.RR.ResourceOwner(), p.RR.ProtectedResource(),
p.RR.Service(), p.RR.Service(),
p.RR.Category(), p.RR.Category(),
len(p.ItemLocation()) > 0) len(p.ItemLocation()) > 0)
@ -133,7 +133,7 @@ func mustPathRep(ref string, isItem bool) repoRefAndLocRef {
rr, err := rrPB.ToDataLayerPath( rr, err := rrPB.ToDataLayerPath(
tmp.Tenant(), tmp.Tenant(),
tmp.ResourceOwner(), tmp.ProtectedResource(),
tmp.Service(), tmp.Service(),
tmp.Category(), tmp.Category(),
isItem) isItem)

View File

@ -0,0 +1,16 @@
package identity
import "github.com/alcionai/corso/src/pkg/path"
// Reasoner describes the parts of the backup that make up its
// data identity: the tenant, protected resources, services, and
// categories which are held within the backup.
type Reasoner interface {
Tenant() string
ProtectedResource() string
Service() path.ServiceType
Category() path.CategoryType
// SubtreePath returns the path prefix for data in existing backups that have
// parameters (tenant, protected resourced, etc) that match this Reasoner.
SubtreePath() (path.Path, error)
}

379
src/pkg/path/builder.go Normal file
View File

@ -0,0 +1,379 @@
package path
import (
"bytes"
"crypto/sha256"
"fmt"
"github.com/alcionai/clues"
)
// interface compliance required for handling PII
var (
_ clues.Concealer = &Builder{}
_ fmt.Stringer = &Builder{}
)
// Builder is a simple path representation that only tracks path elements. It
// can join, escape, and unescape elements. Higher-level packages are expected
// to wrap this struct to build resource-specific contexts (e.x. an
// ExchangeMailPath).
// Resource-specific paths allow access to more information like segments in the
// path. Builders that are turned into resource paths later on do not need to
// manually add prefixes for items that normally appear in the data layer (ex.
// tenant ID, service, user ID, etc).
type Builder struct {
// Unescaped version of elements.
elements Elements
}
// Append creates a copy of this Builder and adds the given elements them to the
// end of the new Builder. Elements are added in the order they are passed.
func (pb Builder) Append(elements ...string) *Builder {
res := &Builder{elements: make([]string, len(pb.elements))}
copy(res.elements, pb.elements)
// Unescaped elements can't fail validation.
//nolint:errcheck
res.appendElements(false, elements)
return res
}
func (pb *Builder) appendElements(escaped bool, elements []string) error {
for _, e := range elements {
if len(e) == 0 {
continue
}
tmp := e
if escaped {
tmp = TrimTrailingSlash(tmp)
// If tmp was just the path separator then it will be empty now.
if len(tmp) == 0 {
continue
}
if err := validateEscapedElement(tmp); err != nil {
return err
}
tmp = unescape(tmp)
}
pb.elements = append(pb.elements, tmp)
}
return nil
}
// UnescapeAndAppend creates a copy of this Builder and adds one or more already
// escaped path elements to the end of the new Builder. Elements are added in
// the order they are passed.
func (pb Builder) UnescapeAndAppend(elements ...string) (*Builder, error) {
res := &Builder{elements: make([]string, 0, len(pb.elements))}
copy(res.elements, pb.elements)
if err := res.appendElements(true, elements); err != nil {
return nil, err
}
return res, nil
}
// SplitUnescapeAppend takes in an escaped string representing a directory
// path, splits the string, and appends it to the current builder.
func (pb Builder) SplitUnescapeAppend(s string) (*Builder, error) {
elems := Split(TrimTrailingSlash(s))
return pb.UnescapeAndAppend(elems...)
}
func (pb Builder) PopFront() *Builder {
if len(pb.elements) <= 1 {
return &Builder{}
}
elements := make([]string, len(pb.elements)-1)
copy(elements, pb.elements[1:])
return &Builder{
elements: elements,
}
}
// Dir removes the last element from the builder.
func (pb Builder) Dir() *Builder {
if len(pb.elements) <= 1 {
return &Builder{}
}
return &Builder{
// Safe to use the same elements because Builders are immutable.
elements: pb.elements[:len(pb.elements)-1],
}
}
// HeadElem returns the first element in the Builder.
func (pb Builder) HeadElem() string {
if len(pb.elements) == 0 {
return ""
}
return pb.elements[0]
}
// LastElem returns the last element in the Builder.
func (pb Builder) LastElem() string {
if len(pb.elements) == 0 {
return ""
}
return pb.elements[len(pb.elements)-1]
}
// UpdateParent updates leading elements matching prev to be cur and returns
// true if it was updated. If prev is not a prefix of this Builder changes
// nothing and returns false. If either prev or cur is nil does nothing and
// returns false.
func (pb *Builder) UpdateParent(prev, cur *Builder) bool {
if prev == cur || prev == nil || cur == nil || len(prev.Elements()) > len(pb.Elements()) {
return false
}
parent := true
for i, e := range prev.Elements() {
if pb.elements[i] != e {
parent = false
break
}
}
if !parent {
return false
}
pb.elements = append(cur.Elements(), pb.elements[len(prev.Elements()):]...)
return true
}
// ShortRef produces a truncated hash of the builder that
// acts as a unique identifier.
func (pb Builder) ShortRef() string {
if len(pb.elements) == 0 {
return ""
}
data := bytes.Buffer{}
for _, element := range pb.elements {
data.WriteString(element)
}
sum := sha256.Sum256(data.Bytes())
// Some conversions to get the right number of characters in the output. This
// outputs hex, so we need to take the target number of characters and do the
// equivalent of (shortRefCharacters * 4) / 8. This is
// <number of bits represented> / <bits per byte> which gets us how many bytes
// to give to our format command.
numBytes := shortRefCharacters / 2
return fmt.Sprintf("%x", sum[:numBytes])
}
// Elements returns all the elements in the path. This is a temporary function
// and will likely be updated to handle encoded elements instead of clear-text
// elements in the future.
func (pb Builder) Elements() Elements {
return append(Elements{}, pb.elements...)
}
// withPrefix creates a Builder prefixed with the parameter values, and
// concatenated with the current builder elements.
func (pb Builder) withPrefix(elements ...string) *Builder {
res := Builder{}.Append(elements...)
res.elements = append(res.elements, pb.elements...)
return res
}
// verifyPrefix ensures that the tenant and protectedResource are valid
// values, and that the builder has some directory structure.
func (pb Builder) verifyPrefix(tenant, protectedResource string) error {
if err := verifyInputValues(tenant, protectedResource); err != nil {
return err
}
if len(pb.elements) == 0 {
return clues.New("missing path beyond prefix")
}
return nil
}
// ---------------------------------------------------------------------------
// Data Layer Path Transformers
// ---------------------------------------------------------------------------
func (pb Builder) ToStreamStorePath(
tenant, purpose string,
service ServiceType,
isItem bool,
) (Path, error) {
if err := verifyInputValues(tenant, purpose); err != nil {
return nil, err
}
if isItem && len(pb.elements) == 0 {
return nil, clues.New("missing path beyond prefix")
}
metadataService := UnknownService
switch service {
case ExchangeService:
metadataService = ExchangeMetadataService
case OneDriveService:
metadataService = OneDriveMetadataService
case SharePointService:
metadataService = SharePointMetadataService
}
return &dataLayerResourcePath{
Builder: *pb.withPrefix(
tenant,
metadataService.String(),
purpose,
DetailsCategory.String()),
service: metadataService,
category: DetailsCategory,
hasItem: isItem,
}, nil
}
func (pb Builder) ToServiceCategoryMetadataPath(
tenant, user string,
service ServiceType,
category CategoryType,
isItem bool,
) (Path, error) {
if err := ValidateServiceAndCategory(service, category); err != nil {
return nil, err
}
if err := verifyInputValues(tenant, user); err != nil {
return nil, err
}
if isItem && len(pb.elements) == 0 {
return nil, clues.New("missing path beyond prefix")
}
metadataService := UnknownService
switch service {
case ExchangeService:
metadataService = ExchangeMetadataService
case OneDriveService:
metadataService = OneDriveMetadataService
case SharePointService:
metadataService = SharePointMetadataService
}
return &dataLayerResourcePath{
Builder: *pb.withPrefix(
tenant,
metadataService.String(),
user,
category.String(),
),
service: metadataService,
category: category,
hasItem: isItem,
}, nil
}
func (pb Builder) ToDataLayerPath(
tenant, user string,
service ServiceType,
category CategoryType,
isItem bool,
) (Path, error) {
if err := ValidateServiceAndCategory(service, category); err != nil {
return nil, err
}
if err := pb.verifyPrefix(tenant, user); err != nil {
return nil, err
}
return &dataLayerResourcePath{
Builder: *pb.withPrefix(
tenant,
service.String(),
user,
category.String()),
service: service,
category: category,
hasItem: isItem,
}, 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(
tenant, user string,
isItem bool,
) (Path, error) {
return pb.ToDataLayerPath(tenant, user, OneDriveService, FilesCategory, isItem)
}
func (pb Builder) ToDataLayerSharePointPath(
tenant, site string,
category CategoryType,
isItem bool,
) (Path, error) {
return pb.ToDataLayerPath(tenant, site, SharePointService, category, isItem)
}
// ---------------------------------------------------------------------------
// Stringers and PII Concealer Compliance
// ---------------------------------------------------------------------------
// Conceal produces a concealed representation of the builder, suitable for
// logging, storing in errors, and other output.
func (pb Builder) Conceal() string {
return pb.elements.Conceal()
}
// Format produces a concealed representation of the builder, even when
// used within a PrintF, suitable for logging, storing in errors,
// and other output.
func (pb Builder) Format(fs fmt.State, _ rune) {
fmt.Fprint(fs, pb.Conceal())
}
// String returns a string that contains all path elements joined together.
// Elements of the path that need escaping are escaped.
// The result is not concealed, and is not suitable for logging or structured
// errors.
func (pb Builder) String() string {
return pb.elements.String()
}
// PlainString returns an unescaped, unmodified string of the builder.
// The result is not concealed, and is not suitable for logging or structured
// errors.
func (pb Builder) PlainString() string {
return pb.elements.PlainString()
}

View File

@ -0,0 +1,369 @@
package path
import (
"fmt"
"strings"
"testing"
"github.com/alcionai/clues"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/tester"
)
type BuilderUnitSuite struct {
tester.Suite
}
func TestBuilderUnitSuite(t *testing.T) {
suite.Run(t, &BuilderUnitSuite{Suite: tester.NewUnitSuite(t)})
}
// set the clues hashing to mask for the span of this suite
func (suite *BuilderUnitSuite) SetupSuite() {
clues.SetHasher(clues.HashCfg{HashAlg: clues.Flatmask})
}
// revert clues hashing to plaintext for all other tests
func (suite *BuilderUnitSuite) TeardownSuite() {
clues.SetHasher(clues.NoHash())
}
func (suite *BuilderUnitSuite) TestAppend() {
table := append(append([]testData{}, genericCases...), basicUnescapedInputs...)
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
p := Builder{}.Append(test.input...)
assert.Equal(t, test.expectedString, p.String())
})
}
}
func (suite *BuilderUnitSuite) TestAppendItem() {
t := suite.T()
p, err := Build("t", "ro", ExchangeService, EmailCategory, false, "foo", "bar")
require.NoError(t, err, clues.ToCore(err))
pb := p.ToBuilder()
assert.Equal(t, pb.String(), p.String())
pb = pb.Append("qux")
p, err = p.AppendItem("qux")
require.NoError(t, err, clues.ToCore(err))
assert.Equal(t, pb.String(), p.String())
_, err = p.AppendItem("fnords")
require.Error(t, err, clues.ToCore(err))
}
func (suite *BuilderUnitSuite) TestUnescapeAndAppend() {
table := append(append([]testData{}, genericCases...), basicEscapedInputs...)
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
p, err := Builder{}.UnescapeAndAppend(test.input...)
require.NoError(t, err, clues.ToCore(err))
assert.Equal(t, test.expectedString, p.String())
})
}
}
func (suite *BuilderUnitSuite) TestEscapedFailure() {
target := "i_s"
for c := range charactersToEscape {
suite.Run(fmt.Sprintf("Unescaped-%c", c), func() {
tmp := strings.ReplaceAll(target, "_", string(c))
_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
assert.Errorf(suite.T(), err, "path with unescaped %s did not error", string(c))
})
}
}
func (suite *BuilderUnitSuite) TestBadEscapeSequenceErrors() {
target := `i\_s/a`
notEscapes := []rune{'a', 'b', '#', '%'}
for _, c := range notEscapes {
suite.Run(fmt.Sprintf("Escaped-%c", c), func() {
tmp := strings.ReplaceAll(target, "_", string(c))
_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
assert.Errorf(
suite.T(),
err,
"path with bad escape sequence %c%c did not error",
escapeCharacter,
c)
})
}
}
func (suite *BuilderUnitSuite) TestTrailingEscapeChar() {
base := []string{"this", "is", "a", "path"}
for i := 0; i < len(base); i++ {
suite.Run(fmt.Sprintf("Element%v", i), func() {
path := make([]string, len(base))
copy(path, base)
path[i] = path[i] + string(escapeCharacter)
_, err := Builder{}.UnescapeAndAppend(path...)
assert.Error(
suite.T(),
err,
"path with trailing escape character did not error")
})
}
}
func (suite *BuilderUnitSuite) TestElements() {
table := []struct {
name string
input []string
output []string
pathFunc func(elements []string) (*Builder, error)
}{
{
name: "SimpleEscapedPath",
input: []string{"this", "is", "a", "path"},
output: []string{"this", "is", "a", "path"},
pathFunc: func(elements []string) (*Builder, error) {
return Builder{}.UnescapeAndAppend(elements...)
},
},
{
name: "SimpleUnescapedPath",
input: []string{"this", "is", "a", "path"},
output: []string{"this", "is", "a", "path"},
pathFunc: func(elements []string) (*Builder, error) {
return Builder{}.Append(elements...), nil
},
},
{
name: "EscapedPath",
input: []string{"this", `is\/`, "a", "path"},
output: []string{"this", "is/", "a", "path"},
pathFunc: func(elements []string) (*Builder, error) {
return Builder{}.UnescapeAndAppend(elements...)
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
p, err := test.pathFunc(test.input)
require.NoError(t, err, clues.ToCore(err))
assert.Equal(t, Elements(test.output), p.Elements())
})
}
}
func (suite *BuilderUnitSuite) TestPopFront() {
table := []struct {
name string
base *Builder
expectedString string
}{
{
name: "Empty",
base: &Builder{},
expectedString: "",
},
{
name: "OneElement",
base: Builder{}.Append("something"),
expectedString: "",
},
{
name: "TwoElements",
base: Builder{}.Append("something", "else"),
expectedString: "else",
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
assert.Equal(t, test.expectedString, test.base.PopFront().String())
})
}
}
func (suite *BuilderUnitSuite) TestShortRef() {
table := []struct {
name string
inputElements []string
expectedLen int
}{
{
name: "PopulatedPath",
inputElements: []string{"this", "is", "a", "path"},
expectedLen: shortRefCharacters,
},
{
name: "EmptyPath",
inputElements: nil,
expectedLen: 0,
},
}
for _, test := range table {
suite.Run(test.name, func() {
pb := Builder{}.Append(test.inputElements...)
ref := pb.ShortRef()
assert.Len(suite.T(), ref, test.expectedLen)
})
}
}
func (suite *BuilderUnitSuite) TestShortRefIsStable() {
t := suite.T()
pb := Builder{}.Append("this", "is", "a", "path")
prevRef := pb.ShortRef()
assert.Len(t, prevRef, shortRefCharacters)
for i := 0; i < 5; i++ {
ref := pb.ShortRef()
assert.Len(t, ref, shortRefCharacters)
assert.Equal(t, prevRef, ref, "ShortRef changed between calls")
prevRef = ref
}
}
func (suite *BuilderUnitSuite) TestShortRefIsUnique() {
pb1 := Builder{}.Append("this", "is", "a", "path")
pb2 := pb1.Append("also")
require.NotEqual(suite.T(), pb1, pb2)
assert.NotEqual(suite.T(), pb1.ShortRef(), pb2.ShortRef())
}
// TestShortRefUniqueWithEscaping tests that two paths that output the same
// unescaped string but different escaped strings have different shortrefs. This
// situation can occur when one path has embedded path separators while the
// other does not but contains the same characters.
func (suite *BuilderUnitSuite) TestShortRefUniqueWithEscaping() {
pb1 := Builder{}.Append(`this`, `is`, `a`, `path`)
pb2 := Builder{}.Append(`this`, `is/a`, `path`)
require.NotEqual(suite.T(), pb1, pb2)
assert.NotEqual(suite.T(), pb1.ShortRef(), pb2.ShortRef())
}
func (suite *BuilderUnitSuite) TestFolder() {
table := []struct {
name string
p func(t *testing.T) Path
escape bool
expectFolder string
expectSplit []string
}{
{
name: "clean path",
p: func(t *testing.T) Path {
p, err := Builder{}.
Append("a", "b", "c").
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
require.NoError(t, err, clues.ToCore(err))
return p
},
expectFolder: "a/b/c",
expectSplit: []string{"a", "b", "c"},
},
{
name: "clean path escaped",
p: func(t *testing.T) Path {
p, err := Builder{}.
Append("a", "b", "c").
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
require.NoError(t, err, clues.ToCore(err))
return p
},
escape: true,
expectFolder: "a/b/c",
expectSplit: []string{"a", "b", "c"},
},
{
name: "escapable path",
p: func(t *testing.T) Path {
p, err := Builder{}.
Append("a/", "b", "c").
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
require.NoError(t, err, clues.ToCore(err))
return p
},
expectFolder: "a//b/c",
expectSplit: []string{"a", "b", "c"},
},
{
name: "escapable path escaped",
p: func(t *testing.T) Path {
p, err := Builder{}.
Append("a/", "b", "c").
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
require.NoError(t, err, clues.ToCore(err))
return p
},
escape: true,
expectFolder: "a\\//b/c",
expectSplit: []string{"a\\/", "b", "c"},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
p := test.p(t)
result := p.Folder(test.escape)
assert.Equal(t, test.expectFolder, result)
assert.Equal(t, test.expectSplit, Split(result))
})
}
}
func (suite *BuilderUnitSuite) TestPIIHandling() {
p, err := Build("t", "ro", ExchangeService, EventsCategory, true, "dir", "item")
require.NoError(suite.T(), err)
table := []struct {
name string
p Path
expect string
expectPlain string
}{
{
name: "standard path",
p: p,
expect: "***/exchange/***/events/***/***",
expectPlain: "t/exchange/ro/events/dir/item",
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
assert.Equal(t, test.expect, test.p.Conceal(), "conceal")
assert.Equal(t, test.expectPlain, test.p.String(), "string")
assert.Equal(t, test.expect, fmt.Sprintf("%s", test.p), "fmt %%s")
assert.Equal(t, test.expect, fmt.Sprintf("%+v", test.p), "fmt %%+v")
assert.Equal(t, test.expectPlain, test.p.PlainString(), "plain")
})
}
}

View File

@ -0,0 +1,108 @@
package path
import (
"fmt"
"strings"
"github.com/alcionai/clues"
)
var ErrorUnknownCategory = clues.New("unknown category string")
// CategoryType denotes what category of data the path corresponds to. The order
// of the enums below can be changed, but the string representation of each enum
// must remain the same or migration code needs to be added to handle changes to
// the string format.
type CategoryType int
//go:generate stringer -type=CategoryType -linecomment
const (
UnknownCategory CategoryType = iota
EmailCategory // email
ContactsCategory // contacts
EventsCategory // events
FilesCategory // files
ListsCategory // lists
LibrariesCategory // libraries
PagesCategory // pages
DetailsCategory // details
)
func ToCategoryType(category string) CategoryType {
cat := strings.ToLower(category)
switch cat {
case strings.ToLower(EmailCategory.String()):
return EmailCategory
case strings.ToLower(ContactsCategory.String()):
return ContactsCategory
case strings.ToLower(EventsCategory.String()):
return EventsCategory
case strings.ToLower(FilesCategory.String()):
return FilesCategory
case strings.ToLower(LibrariesCategory.String()):
return LibrariesCategory
case strings.ToLower(ListsCategory.String()):
return ListsCategory
case strings.ToLower(PagesCategory.String()):
return PagesCategory
case strings.ToLower(DetailsCategory.String()):
return DetailsCategory
default:
return UnknownCategory
}
}
// ---------------------------------------------------------------------------
// Service-Category pairings
// ---------------------------------------------------------------------------
// serviceCategories is a mapping of all valid service/category pairs for
// non-metadata paths.
var serviceCategories = map[ServiceType]map[CategoryType]struct{}{
ExchangeService: {
EmailCategory: {},
ContactsCategory: {},
EventsCategory: {},
},
OneDriveService: {
FilesCategory: {},
},
SharePointService: {
LibrariesCategory: {},
ListsCategory: {},
PagesCategory: {},
},
}
func validateServiceAndCategoryStrings(s, c string) (ServiceType, CategoryType, error) {
service := toServiceType(s)
if service == UnknownService {
return UnknownService, UnknownCategory, clues.Stack(ErrorUnknownService).With("service", fmt.Sprintf("%q", s))
}
category := ToCategoryType(c)
if category == UnknownCategory {
return UnknownService, UnknownCategory, clues.Stack(ErrorUnknownService).With("category", fmt.Sprintf("%q", c))
}
if err := ValidateServiceAndCategory(service, category); err != nil {
return UnknownService, UnknownCategory, err
}
return service, category, nil
}
func ValidateServiceAndCategory(service ServiceType, category CategoryType) error {
cats, ok := serviceCategories[service]
if !ok {
return clues.New("unsupported service").With("service", fmt.Sprintf("%q", service))
}
if _, ok := cats[category]; !ok {
return clues.New("unknown service/category combination").
With("service", fmt.Sprintf("%q", service), "category", fmt.Sprintf("%q", category))
}
return nil
}

View File

@ -2,6 +2,7 @@ package path
import "github.com/alcionai/clues" import "github.com/alcionai/clues"
// TODO: Move this into m365/collection/drive
// drivePath is used to represent path components // drivePath is used to represent path components
// of an item within the drive i.e. // of an item within the drive i.e.
// Given `drives/b!X_8Z2zuXpkKkXZsr7gThk9oJpuj0yXVGnK5_VjRRPK-q725SX_8ZQJgFDK8PlFxA/root:/Folder1/Folder2/file` // Given `drives/b!X_8Z2zuXpkKkXZsr7gThk9oJpuj0yXVGnK5_VjRRPK-q725SX_8ZQJgFDK8PlFxA/root:/Folder1/Folder2/file`

View File

@ -51,8 +51,6 @@
package path package path
import ( import (
"bytes"
"crypto/sha256"
"fmt" "fmt"
"strings" "strings"
@ -84,7 +82,7 @@ type Path interface {
Service() ServiceType Service() ServiceType
Category() CategoryType Category() CategoryType
Tenant() string Tenant() string
ResourceOwner() string ProtectedResource() string
Folder(escaped bool) string Folder(escaped bool) string
Folders() Elements Folders() Elements
Item() string Item() string
@ -122,12 +120,6 @@ type Path interface {
fmt.Stringer fmt.Stringer
} }
// interface compliance required for handling PII
var (
_ clues.Concealer = &Builder{}
_ fmt.Stringer = &Builder{}
)
// RestorePaths denotes the location to find an item in kopia and the path of // RestorePaths denotes the location to find an item in kopia and the path of
// the collection to place the item in for restore. // the collection to place the item in for restore.
type RestorePaths struct { type RestorePaths struct {
@ -135,186 +127,27 @@ type RestorePaths struct {
RestorePath Path RestorePath Path
} }
// Builder is a simple path representation that only tracks path elements. It // ---------------------------------------------------------------------------
// can join, escape, and unescape elements. Higher-level packages are expected // Exported Helpers
// to wrap this struct to build resource-specific contexts (e.x. an // ---------------------------------------------------------------------------
// ExchangeMailPath).
// Resource-specific paths allow access to more information like segments in the func Build(
// path. Builders that are turned into resource paths later on do not need to tenant, protectedResource string,
// manually add prefixes for items that normally appear in the data layer (ex. service ServiceType,
// tenant ID, service, user ID, etc). category CategoryType,
type Builder struct { hasItem bool,
// Unescaped version of elements. elements ...string,
elements Elements ) (Path, error) {
b := Builder{}.Append(elements...)
return b.ToDataLayerPath(
tenant, protectedResource,
service, category,
hasItem)
} }
// Append creates a copy of this Builder and adds the given elements them to the func BuildPrefix(
// end of the new Builder. Elements are added in the order they are passed. tenant, protectedResource string,
func (pb Builder) Append(elements ...string) *Builder {
res := &Builder{elements: make([]string, len(pb.elements))}
copy(res.elements, pb.elements)
// Unescaped elements can't fail validation.
//nolint:errcheck
res.appendElements(false, elements)
return res
}
func (pb *Builder) appendElements(escaped bool, elements []string) error {
for _, e := range elements {
if len(e) == 0 {
continue
}
tmp := e
if escaped {
tmp = TrimTrailingSlash(tmp)
// If tmp was just the path separator then it will be empty now.
if len(tmp) == 0 {
continue
}
if err := validateEscapedElement(tmp); err != nil {
return err
}
tmp = unescape(tmp)
}
pb.elements = append(pb.elements, tmp)
}
return nil
}
// UnescapeAndAppend creates a copy of this Builder and adds one or more already
// escaped path elements to the end of the new Builder. Elements are added in
// the order they are passed.
func (pb Builder) UnescapeAndAppend(elements ...string) (*Builder, error) {
res := &Builder{elements: make([]string, 0, len(pb.elements))}
copy(res.elements, pb.elements)
if err := res.appendElements(true, elements); err != nil {
return nil, err
}
return res, nil
}
// SplitUnescapeAppend takes in an escaped string representing a directory
// path, splits the string, and appends it to the current builder.
func (pb Builder) SplitUnescapeAppend(s string) (*Builder, error) {
elems := Split(TrimTrailingSlash(s))
return pb.UnescapeAndAppend(elems...)
}
func (pb Builder) PopFront() *Builder {
if len(pb.elements) <= 1 {
return &Builder{}
}
elements := make([]string, len(pb.elements)-1)
copy(elements, pb.elements[1:])
return &Builder{
elements: elements,
}
}
// Dir removes the last element from the builder.
func (pb Builder) Dir() *Builder {
if len(pb.elements) <= 1 {
return &Builder{}
}
return &Builder{
// Safe to use the same elements because Builders are immutable.
elements: pb.elements[:len(pb.elements)-1],
}
}
// HeadElem returns the first element in the Builder.
func (pb Builder) HeadElem() string {
if len(pb.elements) == 0 {
return ""
}
return pb.elements[0]
}
// LastElem returns the last element in the Builder.
func (pb Builder) LastElem() string {
if len(pb.elements) == 0 {
return ""
}
return pb.elements[len(pb.elements)-1]
}
// UpdateParent updates leading elements matching prev to be cur and returns
// true if it was updated. If prev is not a prefix of this Builder changes
// nothing and returns false. If either prev or cur is nil does nothing and
// returns false.
func (pb *Builder) UpdateParent(prev, cur *Builder) bool {
if prev == cur || prev == nil || cur == nil || len(prev.Elements()) > len(pb.Elements()) {
return false
}
parent := true
for i, e := range prev.Elements() {
if pb.elements[i] != e {
parent = false
break
}
}
if !parent {
return false
}
pb.elements = append(cur.Elements(), pb.elements[len(prev.Elements()):]...)
return true
}
// ShortRef produces a truncated hash of the builder that
// acts as a unique identifier.
func (pb Builder) ShortRef() string {
if len(pb.elements) == 0 {
return ""
}
data := bytes.Buffer{}
for _, element := range pb.elements {
data.WriteString(element)
}
sum := sha256.Sum256(data.Bytes())
// Some conversions to get the right number of characters in the output. This
// outputs hex, so we need to take the target number of characters and do the
// equivalent of (shortRefCharacters * 4) / 8. This is
// <number of bits represented> / <bits per byte> which gets us how many bytes
// to give to our format command.
numBytes := shortRefCharacters / 2
return fmt.Sprintf("%x", sum[:numBytes])
}
// Elements returns all the elements in the path. This is a temporary function
// and will likely be updated to handle encoded elements instead of clear-text
// elements in the future.
func (pb Builder) Elements() Elements {
return append(Elements{}, pb.elements...)
}
func ServicePrefix(
tenant, resourceOwner string,
s ServiceType, s ServiceType,
c CategoryType, c CategoryType,
) (Path, error) { ) (Path, error) {
@ -324,209 +157,18 @@ func ServicePrefix(
return nil, err return nil, err
} }
if err := verifyInputValues(tenant, resourceOwner); err != nil { if err := verifyInputValues(tenant, protectedResource); err != nil {
return nil, err return nil, err
} }
return &dataLayerResourcePath{ return &dataLayerResourcePath{
Builder: *pb.withPrefix(tenant, s.String(), resourceOwner, c.String()), Builder: *pb.withPrefix(tenant, s.String(), protectedResource, c.String()),
service: s, service: s,
category: c, category: c,
hasItem: false, hasItem: false,
}, nil }, nil
} }
// withPrefix creates a Builder prefixed with the parameter values, and
// concatenated with the current builder elements.
func (pb Builder) withPrefix(elements ...string) *Builder {
res := Builder{}.Append(elements...)
res.elements = append(res.elements, pb.elements...)
return res
}
// ---------------------------------------------------------------------------
// Data Layer Path Transformers
// ---------------------------------------------------------------------------
func (pb Builder) ToStreamStorePath(
tenant, purpose string,
service ServiceType,
isItem bool,
) (Path, error) {
if err := verifyInputValues(tenant, purpose); err != nil {
return nil, err
}
if isItem && len(pb.elements) == 0 {
return nil, clues.New("missing path beyond prefix")
}
metadataService := UnknownService
switch service {
case ExchangeService:
metadataService = ExchangeMetadataService
case OneDriveService:
metadataService = OneDriveMetadataService
case SharePointService:
metadataService = SharePointMetadataService
}
return &dataLayerResourcePath{
Builder: *pb.withPrefix(
tenant,
metadataService.String(),
purpose,
DetailsCategory.String()),
service: metadataService,
category: DetailsCategory,
hasItem: isItem,
}, nil
}
func (pb Builder) ToServiceCategoryMetadataPath(
tenant, user string,
service ServiceType,
category CategoryType,
isItem bool,
) (Path, error) {
if err := ValidateServiceAndCategory(service, category); err != nil {
return nil, err
}
if err := verifyInputValues(tenant, user); err != nil {
return nil, err
}
if isItem && len(pb.elements) == 0 {
return nil, clues.New("missing path beyond prefix")
}
metadataService := UnknownService
switch service {
case ExchangeService:
metadataService = ExchangeMetadataService
case OneDriveService:
metadataService = OneDriveMetadataService
case SharePointService:
metadataService = SharePointMetadataService
}
return &dataLayerResourcePath{
Builder: *pb.withPrefix(
tenant,
metadataService.String(),
user,
category.String(),
),
service: metadataService,
category: category,
hasItem: isItem,
}, nil
}
func (pb Builder) ToDataLayerPath(
tenant, user string,
service ServiceType,
category CategoryType,
isItem bool,
) (Path, error) {
if err := ValidateServiceAndCategory(service, category); err != nil {
return nil, err
}
if err := pb.verifyPrefix(tenant, user); err != nil {
return nil, err
}
return &dataLayerResourcePath{
Builder: *pb.withPrefix(
tenant,
service.String(),
user,
category.String()),
service: service,
category: category,
hasItem: isItem,
}, 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(
tenant, user string,
isItem bool,
) (Path, error) {
return pb.ToDataLayerPath(tenant, user, OneDriveService, FilesCategory, isItem)
}
func (pb Builder) ToDataLayerSharePointPath(
tenant, site string,
category CategoryType,
isItem bool,
) (Path, error) {
return pb.ToDataLayerPath(tenant, site, SharePointService, category, isItem)
}
// ---------------------------------------------------------------------------
// Stringers and PII Concealer Compliance
// ---------------------------------------------------------------------------
// Conceal produces a concealed representation of the builder, suitable for
// logging, storing in errors, and other output.
func (pb Builder) Conceal() string {
return pb.elements.Conceal()
}
// Format produces a concealed representation of the builder, even when
// used within a PrintF, suitable for logging, storing in errors,
// and other output.
func (pb Builder) Format(fs fmt.State, _ rune) {
fmt.Fprint(fs, pb.Conceal())
}
// String returns a string that contains all path elements joined together.
// Elements of the path that need escaping are escaped.
// The result is not concealed, and is not suitable for logging or structured
// errors.
func (pb Builder) String() string {
return pb.elements.String()
}
// PlainString returns an unescaped, unmodified string of the builder.
// The result is not concealed, and is not suitable for logging or structured
// errors.
func (pb Builder) PlainString() string {
return pb.elements.PlainString()
}
// ---------------------------------------------------------------------------
// Exported Helpers
// ---------------------------------------------------------------------------
func Build(
tenant, resourceOwner string,
service ServiceType,
category CategoryType,
hasItem bool,
elements ...string,
) (Path, error) {
b := Builder{}.Append(elements...)
return b.ToDataLayerPath(
tenant, resourceOwner,
service, category,
hasItem)
}
// FromDataLayerPath parses the escaped path p, validates the elements in p // FromDataLayerPath parses the escaped path p, validates the elements in p
// match a resource-specific path format, and returns a Path struct for that // match a resource-specific path format, and returns a Path struct for that
// resource-specific type. If p does not match any resource-specific paths or // resource-specific type. If p does not match any resource-specific paths or
@ -648,13 +290,13 @@ func Split(segment string) []string {
// Unexported Helpers // Unexported Helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
func verifyInputValues(tenant, resourceOwner string) error { func verifyInputValues(tenant, protectedResource string) error {
if len(tenant) == 0 { if len(tenant) == 0 {
return clues.Stack(errMissingSegment, clues.New("tenant")) return clues.Stack(errMissingSegment, clues.New("tenant"))
} }
if len(resourceOwner) == 0 { if len(protectedResource) == 0 {
return clues.Stack(errMissingSegment, clues.New("resourceOwner")) return clues.Stack(errMissingSegment, clues.New("protected resource"))
} }
return nil return nil
@ -762,17 +404,3 @@ func join(elements []string) string {
// '\' according to the escaping rules. // '\' according to the escaping rules.
return strings.Join(elements, string(PathSeparator)) return strings.Join(elements, string(PathSeparator))
} }
// verifyPrefix ensures that the tenant and resourceOwner are valid
// values, and that the builder has some directory structure.
func (pb Builder) verifyPrefix(tenant, resourceOwner string) error {
if err := verifyInputValues(tenant, resourceOwner); err != nil {
return err
}
if len(pb.elements) == 0 {
return clues.New("missing path beyond prefix")
}
return nil
}

View File

@ -2,7 +2,6 @@ package path
import ( import (
"fmt" "fmt"
"strings"
"testing" "testing"
"github.com/alcionai/clues" "github.com/alcionai/clues"
@ -233,239 +232,7 @@ func (suite *PathUnitSuite) TeardownSuite() {
clues.SetHasher(clues.NoHash()) clues.SetHasher(clues.NoHash())
} }
func (suite *PathUnitSuite) TestAppend() { func (suite *PathUnitSuite) TestFromDataLayerPathErrors() {
table := append(append([]testData{}, genericCases...), basicUnescapedInputs...)
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
p := Builder{}.Append(test.input...)
assert.Equal(t, test.expectedString, p.String())
})
}
}
func (suite *PathUnitSuite) TestAppendItem() {
t := suite.T()
p, err := Build("t", "ro", ExchangeService, EmailCategory, false, "foo", "bar")
require.NoError(t, err, clues.ToCore(err))
pb := p.ToBuilder()
assert.Equal(t, pb.String(), p.String())
pb = pb.Append("qux")
p, err = p.AppendItem("qux")
require.NoError(t, err, clues.ToCore(err))
assert.Equal(t, pb.String(), p.String())
_, err = p.AppendItem("fnords")
require.Error(t, err, clues.ToCore(err))
}
func (suite *PathUnitSuite) TestUnescapeAndAppend() {
table := append(append([]testData{}, genericCases...), basicEscapedInputs...)
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
p, err := Builder{}.UnescapeAndAppend(test.input...)
require.NoError(t, err, clues.ToCore(err))
assert.Equal(t, test.expectedString, p.String())
})
}
}
func (suite *PathUnitSuite) TestEscapedFailure() {
target := "i_s"
for c := range charactersToEscape {
suite.Run(fmt.Sprintf("Unescaped-%c", c), func() {
tmp := strings.ReplaceAll(target, "_", string(c))
_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
assert.Errorf(suite.T(), err, "path with unescaped %s did not error", string(c))
})
}
}
func (suite *PathUnitSuite) TestBadEscapeSequenceErrors() {
target := `i\_s/a`
notEscapes := []rune{'a', 'b', '#', '%'}
for _, c := range notEscapes {
suite.Run(fmt.Sprintf("Escaped-%c", c), func() {
tmp := strings.ReplaceAll(target, "_", string(c))
_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
assert.Errorf(
suite.T(),
err,
"path with bad escape sequence %c%c did not error",
escapeCharacter,
c)
})
}
}
func (suite *PathUnitSuite) TestTrailingEscapeChar() {
base := []string{"this", "is", "a", "path"}
for i := 0; i < len(base); i++ {
suite.Run(fmt.Sprintf("Element%v", i), func() {
path := make([]string, len(base))
copy(path, base)
path[i] = path[i] + string(escapeCharacter)
_, err := Builder{}.UnescapeAndAppend(path...)
assert.Error(
suite.T(),
err,
"path with trailing escape character did not error")
})
}
}
func (suite *PathUnitSuite) TestElements() {
table := []struct {
name string
input []string
output []string
pathFunc func(elements []string) (*Builder, error)
}{
{
name: "SimpleEscapedPath",
input: []string{"this", "is", "a", "path"},
output: []string{"this", "is", "a", "path"},
pathFunc: func(elements []string) (*Builder, error) {
return Builder{}.UnescapeAndAppend(elements...)
},
},
{
name: "SimpleUnescapedPath",
input: []string{"this", "is", "a", "path"},
output: []string{"this", "is", "a", "path"},
pathFunc: func(elements []string) (*Builder, error) {
return Builder{}.Append(elements...), nil
},
},
{
name: "EscapedPath",
input: []string{"this", `is\/`, "a", "path"},
output: []string{"this", "is/", "a", "path"},
pathFunc: func(elements []string) (*Builder, error) {
return Builder{}.UnescapeAndAppend(elements...)
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
p, err := test.pathFunc(test.input)
require.NoError(t, err, clues.ToCore(err))
assert.Equal(t, Elements(test.output), p.Elements())
})
}
}
func (suite *PathUnitSuite) TestPopFront() {
table := []struct {
name string
base *Builder
expectedString string
}{
{
name: "Empty",
base: &Builder{},
expectedString: "",
},
{
name: "OneElement",
base: Builder{}.Append("something"),
expectedString: "",
},
{
name: "TwoElements",
base: Builder{}.Append("something", "else"),
expectedString: "else",
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
assert.Equal(t, test.expectedString, test.base.PopFront().String())
})
}
}
func (suite *PathUnitSuite) TestShortRef() {
table := []struct {
name string
inputElements []string
expectedLen int
}{
{
name: "PopulatedPath",
inputElements: []string{"this", "is", "a", "path"},
expectedLen: shortRefCharacters,
},
{
name: "EmptyPath",
inputElements: nil,
expectedLen: 0,
},
}
for _, test := range table {
suite.Run(test.name, func() {
pb := Builder{}.Append(test.inputElements...)
ref := pb.ShortRef()
assert.Len(suite.T(), ref, test.expectedLen)
})
}
}
func (suite *PathUnitSuite) TestShortRefIsStable() {
t := suite.T()
pb := Builder{}.Append("this", "is", "a", "path")
prevRef := pb.ShortRef()
assert.Len(t, prevRef, shortRefCharacters)
for i := 0; i < 5; i++ {
ref := pb.ShortRef()
assert.Len(t, ref, shortRefCharacters)
assert.Equal(t, prevRef, ref, "ShortRef changed between calls")
prevRef = ref
}
}
func (suite *PathUnitSuite) TestShortRefIsUnique() {
pb1 := Builder{}.Append("this", "is", "a", "path")
pb2 := pb1.Append("also")
require.NotEqual(suite.T(), pb1, pb2)
assert.NotEqual(suite.T(), pb1.ShortRef(), pb2.ShortRef())
}
// TestShortRefUniqueWithEscaping tests that two paths that output the same
// unescaped string but different escaped strings have different shortrefs. This
// situation can occur when one path has embedded path separators while the
// other does not but contains the same characters.
func (suite *PathUnitSuite) TestShortRefUniqueWithEscaping() {
pb1 := Builder{}.Append(`this`, `is`, `a`, `path`)
pb2 := Builder{}.Append(`this`, `is/a`, `path`)
require.NotEqual(suite.T(), pb1, pb2)
assert.NotEqual(suite.T(), pb1.ShortRef(), pb2.ShortRef())
}
func (suite *PathUnitSuite) TestFromStringErrors() {
table := []struct { table := []struct {
name string name string
escapedPath string escapedPath string
@ -521,82 +288,7 @@ func (suite *PathUnitSuite) TestFromStringErrors() {
} }
} }
func (suite *PathUnitSuite) TestFolder() { func (suite *PathUnitSuite) TestFromDataLayerPath() {
table := []struct {
name string
p func(t *testing.T) Path
escape bool
expectFolder string
expectSplit []string
}{
{
name: "clean path",
p: func(t *testing.T) Path {
p, err := Builder{}.
Append("a", "b", "c").
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
require.NoError(t, err, clues.ToCore(err))
return p
},
expectFolder: "a/b/c",
expectSplit: []string{"a", "b", "c"},
},
{
name: "clean path escaped",
p: func(t *testing.T) Path {
p, err := Builder{}.
Append("a", "b", "c").
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
require.NoError(t, err, clues.ToCore(err))
return p
},
escape: true,
expectFolder: "a/b/c",
expectSplit: []string{"a", "b", "c"},
},
{
name: "escapable path",
p: func(t *testing.T) Path {
p, err := Builder{}.
Append("a/", "b", "c").
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
require.NoError(t, err, clues.ToCore(err))
return p
},
expectFolder: "a//b/c",
expectSplit: []string{"a", "b", "c"},
},
{
name: "escapable path escaped",
p: func(t *testing.T) Path {
p, err := Builder{}.
Append("a/", "b", "c").
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
require.NoError(t, err, clues.ToCore(err))
return p
},
escape: true,
expectFolder: "a\\//b/c",
expectSplit: []string{"a\\/", "b", "c"},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
p := test.p(t)
result := p.Folder(test.escape)
assert.Equal(t, test.expectFolder, result)
assert.Equal(t, test.expectSplit, Split(result))
})
}
}
func (suite *PathUnitSuite) TestFromString() {
const ( const (
testTenant = "tenant" testTenant = "tenant"
testUser = "user" testUser = "user"
@ -715,7 +407,7 @@ func (suite *PathUnitSuite) TestFromString() {
assert.Equal(t, service, p.Service(), "service") assert.Equal(t, service, p.Service(), "service")
assert.Equal(t, cat, p.Category(), "category") assert.Equal(t, cat, p.Category(), "category")
assert.Equal(t, testTenant, p.Tenant(), "tenant") assert.Equal(t, testTenant, p.Tenant(), "tenant")
assert.Equal(t, testUser, p.ResourceOwner(), "resource owner") assert.Equal(t, testUser, p.ProtectedResource(), "protected resource")
fld := p.Folder(false) fld := p.Folder(false)
escfld := p.Folder(true) escfld := p.Folder(true)
@ -740,43 +432,13 @@ func (suite *PathUnitSuite) TestFromString() {
} }
} }
func (suite *PathUnitSuite) TestPath_piiHandling() { func (suite *PathUnitSuite) TestBuildPrefix() {
p, err := Build("t", "ro", ExchangeService, EventsCategory, true, "dir", "item")
require.NoError(suite.T(), err)
table := []struct {
name string
p Path
expect string
expectPlain string
}{
{
name: "standard path",
p: p,
expect: "***/exchange/***/events/***/***",
expectPlain: "t/exchange/ro/events/dir/item",
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
assert.Equal(t, test.expect, test.p.Conceal(), "conceal")
assert.Equal(t, test.expectPlain, test.p.String(), "string")
assert.Equal(t, test.expect, fmt.Sprintf("%s", test.p), "fmt %%s")
assert.Equal(t, test.expect, fmt.Sprintf("%+v", test.p), "fmt %%+v")
assert.Equal(t, test.expectPlain, test.p.PlainString(), "plain")
})
}
}
func (suite *PathUnitSuite) TestToServicePrefix() {
table := []struct { table := []struct {
name string name string
service ServiceType service ServiceType
category CategoryType category CategoryType
tenant string tenant string
owner string resource string
expect string expect string
expectErr require.ErrorAssertionFunc expectErr require.ErrorAssertionFunc
}{ }{
@ -785,7 +447,7 @@ func (suite *PathUnitSuite) TestToServicePrefix() {
service: ExchangeService, service: ExchangeService,
category: ContactsCategory, category: ContactsCategory,
tenant: "t", tenant: "t",
owner: "ro", resource: "ro",
expect: join([]string{"t", ExchangeService.String(), "ro", ContactsCategory.String()}), expect: join([]string{"t", ExchangeService.String(), "ro", ContactsCategory.String()}),
expectErr: require.NoError, expectErr: require.NoError,
}, },
@ -794,7 +456,7 @@ func (suite *PathUnitSuite) TestToServicePrefix() {
service: ExchangeService, service: ExchangeService,
category: FilesCategory, category: FilesCategory,
tenant: "t", tenant: "t",
owner: "ro", resource: "ro",
expectErr: require.Error, expectErr: require.Error,
}, },
{ {
@ -802,15 +464,15 @@ func (suite *PathUnitSuite) TestToServicePrefix() {
service: ExchangeService, service: ExchangeService,
category: ContactsCategory, category: ContactsCategory,
tenant: "", tenant: "",
owner: "ro", resource: "ro",
expectErr: require.Error, expectErr: require.Error,
}, },
{ {
name: "bad owner", name: "bad resource",
service: ExchangeService, service: ExchangeService,
category: ContactsCategory, category: ContactsCategory,
tenant: "t", tenant: "t",
owner: "", resource: "",
expectErr: require.Error, expectErr: require.Error,
}, },
} }
@ -818,7 +480,7 @@ func (suite *PathUnitSuite) TestToServicePrefix() {
suite.Run(test.name, func() { suite.Run(test.name, func() {
t := suite.T() t := suite.T()
r, err := ServicePrefix(test.tenant, test.owner, test.service, test.category) r, err := BuildPrefix(test.tenant, test.resource, test.service, test.category)
test.expectErr(t, err, clues.ToCore(err)) test.expectErr(t, err, clues.ToCore(err))
if r == nil { if r == nil {

View File

@ -1,161 +1,16 @@
package path package path
import ( import (
"fmt"
"strings"
"github.com/alcionai/clues" "github.com/alcionai/clues"
) )
var ErrorUnknownService = clues.New("unknown service string")
// ServiceType denotes what service the path corresponds to. Metadata services
// are also included though they are only used for paths that house metadata for
// Corso backups.
//
// Metadata services are not considered valid service types for resource paths
// though they can be used for metadata paths.
//
// The order of the enums below can be changed, but the string representation of
// each enum must remain the same or migration code needs to be added to handle
// changes to the string format.
type ServiceType int
//go:generate stringer -type=ServiceType -linecomment
const (
UnknownService ServiceType = iota
ExchangeService // exchange
OneDriveService // onedrive
SharePointService // sharepoint
ExchangeMetadataService // exchangeMetadata
OneDriveMetadataService // onedriveMetadata
SharePointMetadataService // sharepointMetadata
)
func toServiceType(service string) ServiceType {
s := strings.ToLower(service)
switch s {
case strings.ToLower(ExchangeService.String()):
return ExchangeService
case strings.ToLower(OneDriveService.String()):
return OneDriveService
case strings.ToLower(SharePointService.String()):
return SharePointService
case strings.ToLower(ExchangeMetadataService.String()):
return ExchangeMetadataService
case strings.ToLower(OneDriveMetadataService.String()):
return OneDriveMetadataService
case strings.ToLower(SharePointMetadataService.String()):
return SharePointMetadataService
default:
return UnknownService
}
}
var ErrorUnknownCategory = clues.New("unknown category string")
// CategoryType denotes what category of data the path corresponds to. The order
// of the enums below can be changed, but the string representation of each enum
// must remain the same or migration code needs to be added to handle changes to
// the string format.
type CategoryType int
//go:generate stringer -type=CategoryType -linecomment
const (
UnknownCategory CategoryType = iota
EmailCategory // email
ContactsCategory // contacts
EventsCategory // events
FilesCategory // files
ListsCategory // lists
LibrariesCategory // libraries
PagesCategory // pages
DetailsCategory // details
)
func ToCategoryType(category string) CategoryType {
cat := strings.ToLower(category)
switch cat {
case strings.ToLower(EmailCategory.String()):
return EmailCategory
case strings.ToLower(ContactsCategory.String()):
return ContactsCategory
case strings.ToLower(EventsCategory.String()):
return EventsCategory
case strings.ToLower(FilesCategory.String()):
return FilesCategory
case strings.ToLower(LibrariesCategory.String()):
return LibrariesCategory
case strings.ToLower(ListsCategory.String()):
return ListsCategory
case strings.ToLower(PagesCategory.String()):
return PagesCategory
case strings.ToLower(DetailsCategory.String()):
return DetailsCategory
default:
return UnknownCategory
}
}
// serviceCategories is a mapping of all valid service/category pairs for
// non-metadata paths.
var serviceCategories = map[ServiceType]map[CategoryType]struct{}{
ExchangeService: {
EmailCategory: {},
ContactsCategory: {},
EventsCategory: {},
},
OneDriveService: {
FilesCategory: {},
},
SharePointService: {
LibrariesCategory: {},
ListsCategory: {},
PagesCategory: {},
},
}
func validateServiceAndCategoryStrings(s, c string) (ServiceType, CategoryType, error) {
service := toServiceType(s)
if service == UnknownService {
return UnknownService, UnknownCategory, clues.Stack(ErrorUnknownService).With("service", fmt.Sprintf("%q", s))
}
category := ToCategoryType(c)
if category == UnknownCategory {
return UnknownService, UnknownCategory, clues.Stack(ErrorUnknownService).With("category", fmt.Sprintf("%q", c))
}
if err := ValidateServiceAndCategory(service, category); err != nil {
return UnknownService, UnknownCategory, err
}
return service, category, nil
}
func ValidateServiceAndCategory(service ServiceType, category CategoryType) error {
cats, ok := serviceCategories[service]
if !ok {
return clues.New("unsupported service").With("service", fmt.Sprintf("%q", service))
}
if _, ok := cats[category]; !ok {
return clues.New("unknown service/category combination").
With("service", fmt.Sprintf("%q", service), "category", fmt.Sprintf("%q", category))
}
return nil
}
// dataLayerResourcePath allows callers to extract information from a // dataLayerResourcePath allows callers to extract information from a
// resource-specific path. This struct is unexported so that callers are // resource-specific path. This struct is unexported so that callers are
// forced to use the pre-defined constructors, making it impossible to create a // forced to use the pre-defined constructors, making it impossible to create a
// dataLayerResourcePath with invalid service/category combinations. // dataLayerResourcePath with invalid service/category combinations.
// //
// All dataLayerResourcePaths start with the same prefix: // All dataLayerResourcePaths start with the same prefix:
// <tenant ID>/<service>/<resource owner ID>/<category> // <tenant ID>/<service>/<protected resource ID>[/<subService>/<protected resource ID>]/<category>
// which allows extracting high-level information from the path. The path // which allows extracting high-level information from the path. The path
// elements after this prefix represent zero or more folders and, if the path // elements after this prefix represent zero or more folders and, if the path
// refers to a file or item, an item ID. A valid dataLayerResourcePath must have // refers to a file or item, an item ID. A valid dataLayerResourcePath must have
@ -183,9 +38,9 @@ func (rp dataLayerResourcePath) Category() CategoryType {
return rp.category return rp.category
} }
// ResourceOwner returns the user ID or group ID embedded in the // ProtectedResource returns the resource ID embedded in the
// dataLayerResourcePath. // dataLayerResourcePath.
func (rp dataLayerResourcePath) ResourceOwner() string { func (rp dataLayerResourcePath) ProtectedResource() string {
return rp.Builder.elements[2] return rp.Builder.elements[2]
} }

View File

@ -37,7 +37,7 @@ var (
rest: rest, rest: rest,
}, },
{ {
name: "NoResourceOwner", name: "NoProtectedResource",
tenant: testTenant, tenant: testTenant,
user: "", user: "",
rest: rest, rest: rest,
@ -404,7 +404,7 @@ func (suite *DataLayerResourcePath) TestToExchangePathForCategory() {
assert.Equal(t, testTenant, p.Tenant()) assert.Equal(t, testTenant, p.Tenant())
assert.Equal(t, path.ExchangeService, p.Service()) assert.Equal(t, path.ExchangeService, p.Service())
assert.Equal(t, test.category, p.Category()) assert.Equal(t, test.category, p.Category())
assert.Equal(t, testUser, p.ResourceOwner()) assert.Equal(t, testUser, p.ProtectedResource())
assert.Equal(t, strings.Join(m.expectedFolders, "/"), p.Folder(false)) assert.Equal(t, strings.Join(m.expectedFolders, "/"), p.Folder(false))
assert.Equal(t, path.Elements(m.expectedFolders), p.Folders()) assert.Equal(t, path.Elements(m.expectedFolders), p.Folders())
assert.Equal(t, m.expectedItem, p.Item()) assert.Equal(t, m.expectedItem, p.Item())
@ -471,12 +471,12 @@ func (suite *PopulatedDataLayerResourcePath) TestCategory() {
} }
} }
func (suite *PopulatedDataLayerResourcePath) TestResourceOwner() { func (suite *PopulatedDataLayerResourcePath) TestProtectedResource() {
for _, m := range modes { for _, m := range modes {
suite.Run(m.name, func() { suite.Run(m.name, func() {
t := suite.T() t := suite.T()
assert.Equal(t, testUser, suite.paths[m.isItem].ResourceOwner()) assert.Equal(t, testUser, suite.paths[m.isItem].ProtectedResource())
}) })
} }
} }

View File

@ -0,0 +1,53 @@
package path
import (
"strings"
"github.com/alcionai/clues"
)
var ErrorUnknownService = clues.New("unknown service string")
// ServiceType denotes what service the path corresponds to. Metadata services
// are also included though they are only used for paths that house metadata for
// Corso backups.
//
// Metadata services are not considered valid service types for resource paths
// though they can be used for metadata paths.
//
// The order of the enums below can be changed, but the string representation of
// each enum must remain the same or migration code needs to be added to handle
// changes to the string format.
type ServiceType int
//go:generate stringer -type=ServiceType -linecomment
const (
UnknownService ServiceType = iota
ExchangeService // exchange
OneDriveService // onedrive
SharePointService // sharepoint
ExchangeMetadataService // exchangeMetadata
OneDriveMetadataService // onedriveMetadata
SharePointMetadataService // sharepointMetadata
)
func toServiceType(service string) ServiceType {
s := strings.ToLower(service)
switch s {
case strings.ToLower(ExchangeService.String()):
return ExchangeService
case strings.ToLower(OneDriveService.String()):
return OneDriveService
case strings.ToLower(SharePointService.String()):
return SharePointService
case strings.ToLower(ExchangeMetadataService.String()):
return ExchangeMetadataService
case strings.ToLower(OneDriveMetadataService.String()):
return OneDriveMetadataService
case strings.ToLower(SharePointMetadataService.String()):
return SharePointMetadataService
default:
return UnknownService
}
}

View File

@ -348,7 +348,7 @@ func ensureAllUsersInDetails(
continue continue
} }
ro := p.ResourceOwner() ro := p.ProtectedResource()
if !assert.NotEmpty(t, ro, "resource owner in path: "+rr) { if !assert.NotEmpty(t, ro, "resource owner in path: "+rr) {
continue continue
} }

View File

@ -9,6 +9,7 @@ import (
"github.com/alcionai/corso/src/internal/common/dttm" "github.com/alcionai/corso/src/internal/common/dttm"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/filters" "github.com/alcionai/corso/src/pkg/filters"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
@ -43,6 +44,7 @@ type (
var ( var (
_ Reducer = &ExchangeRestore{} _ Reducer = &ExchangeRestore{}
_ pathCategorier = &ExchangeRestore{} _ pathCategorier = &ExchangeRestore{}
_ reasoner = &ExchangeRestore{}
) )
// NewExchange produces a new Selector with the service set to ServiceExchange. // NewExchange produces a new Selector with the service set to ServiceExchange.
@ -69,7 +71,7 @@ func (s Selector) ToExchangeBackup() (*ExchangeBackup, error) {
} }
func (s ExchangeBackup) SplitByResourceOwner(users []string) []ExchangeBackup { func (s ExchangeBackup) SplitByResourceOwner(users []string) []ExchangeBackup {
sels := splitByResourceOwner[ExchangeScope](s.Selector, users, ExchangeUser) sels := splitByProtectedResource[ExchangeScope](s.Selector, users, ExchangeUser)
ss := make([]ExchangeBackup, 0, len(sels)) ss := make([]ExchangeBackup, 0, len(sels))
for _, sel := range sels { for _, sel := range sels {
@ -103,7 +105,7 @@ func (s Selector) ToExchangeRestore() (*ExchangeRestore, error) {
} }
func (sr ExchangeRestore) SplitByResourceOwner(users []string) []ExchangeRestore { func (sr ExchangeRestore) SplitByResourceOwner(users []string) []ExchangeRestore {
sels := splitByResourceOwner[ExchangeScope](sr.Selector, users, ExchangeUser) sels := splitByProtectedResource[ExchangeScope](sr.Selector, users, ExchangeUser)
ss := make([]ExchangeRestore, 0, len(sels)) ss := make([]ExchangeRestore, 0, len(sels))
for _, sel := range sels { for _, sel := range sels {
@ -122,6 +124,13 @@ func (s exchange) PathCategories() selectorPathCategories {
} }
} }
// Reasons returns a deduplicated set of the backup reasons produced
// using the selector's discrete owner and each scopes' service and
// category types.
func (s exchange) Reasons(tenantID string, useOwnerNameForID bool) []identity.Reasoner {
return reasonsFor(s, tenantID, useOwnerNameForID)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Stringers and Concealers // Stringers and Concealers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -816,7 +816,7 @@ func (suite *ExchangeSelectorSuite) TestExchangeRestore_Reduce() {
joinedFldrs := strings.Join(newElems, "/") joinedFldrs := strings.Join(newElems, "/")
return stubRepoRef(p.Service(), p.Category(), p.ResourceOwner(), joinedFldrs, p.Item()) return stubRepoRef(p.Service(), p.Category(), p.ProtectedResource(), joinedFldrs, p.Item())
} }
makeDeets := func(refs ...path.Path) *details.Details { makeDeets := func(refs ...path.Path) *details.Details {

View File

@ -7,6 +7,7 @@ import (
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
@ -40,6 +41,7 @@ type (
var ( var (
_ Reducer = &GroupsRestore{} _ Reducer = &GroupsRestore{}
_ pathCategorier = &GroupsRestore{} _ pathCategorier = &GroupsRestore{}
_ reasoner = &GroupsRestore{}
) )
// NewGroupsBackup produces a new Selector with the service set to ServiceGroups. // NewGroupsBackup produces a new Selector with the service set to ServiceGroups.
@ -66,7 +68,7 @@ func (s Selector) ToGroupsBackup() (*GroupsBackup, error) {
} }
func (s GroupsBackup) SplitByResourceOwner(resources []string) []GroupsBackup { func (s GroupsBackup) SplitByResourceOwner(resources []string) []GroupsBackup {
sels := splitByResourceOwner[GroupsScope](s.Selector, resources, GroupsGroup) sels := splitByProtectedResource[GroupsScope](s.Selector, resources, GroupsGroup)
ss := make([]GroupsBackup, 0, len(sels)) ss := make([]GroupsBackup, 0, len(sels))
for _, sel := range sels { for _, sel := range sels {
@ -100,7 +102,7 @@ func (s Selector) ToGroupsRestore() (*GroupsRestore, error) {
} }
func (s GroupsRestore) SplitByResourceOwner(resources []string) []GroupsRestore { func (s GroupsRestore) SplitByResourceOwner(resources []string) []GroupsRestore {
sels := splitByResourceOwner[GroupsScope](s.Selector, resources, GroupsGroup) sels := splitByProtectedResource[GroupsScope](s.Selector, resources, GroupsGroup)
ss := make([]GroupsRestore, 0, len(sels)) ss := make([]GroupsRestore, 0, len(sels))
for _, sel := range sels { for _, sel := range sels {
@ -119,6 +121,13 @@ func (s groups) PathCategories() selectorPathCategories {
} }
} }
// Reasons returns a deduplicated set of the backup reasons produced
// using the selector's discrete owner and each scopes' service and
// category types.
func (s groups) Reasons(tenantID string, useOwnerNameForID bool) []identity.Reasoner {
return reasonsFor(s, tenantID, useOwnerNameForID)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Stringers and Concealers // Stringers and Concealers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -167,6 +167,8 @@ func (s mockScope) PlainString() string { return plainString(s) }
// selectors // selectors
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
var _ servicerCategorizerProvider = &mockSel{}
type mockSel struct { type mockSel struct {
Selector Selector
} }
@ -183,6 +185,10 @@ func stubSelector(resourceOwners []string) mockSel {
} }
} }
func (m mockSel) PathCategories() selectorPathCategories {
return m.PathCategories()
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// helper funcs // helper funcs
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -8,6 +8,7 @@ import (
"github.com/alcionai/corso/src/internal/common/dttm" "github.com/alcionai/corso/src/internal/common/dttm"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/filters" "github.com/alcionai/corso/src/pkg/filters"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
@ -42,6 +43,7 @@ type (
var ( var (
_ Reducer = &OneDriveRestore{} _ Reducer = &OneDriveRestore{}
_ pathCategorier = &OneDriveRestore{} _ pathCategorier = &OneDriveRestore{}
_ reasoner = &OneDriveRestore{}
) )
// NewOneDriveBackup produces a new Selector with the service set to ServiceOneDrive. // NewOneDriveBackup produces a new Selector with the service set to ServiceOneDrive.
@ -68,7 +70,7 @@ func (s Selector) ToOneDriveBackup() (*OneDriveBackup, error) {
} }
func (s OneDriveBackup) SplitByResourceOwner(users []string) []OneDriveBackup { func (s OneDriveBackup) SplitByResourceOwner(users []string) []OneDriveBackup {
sels := splitByResourceOwner[OneDriveScope](s.Selector, users, OneDriveUser) sels := splitByProtectedResource[OneDriveScope](s.Selector, users, OneDriveUser)
ss := make([]OneDriveBackup, 0, len(sels)) ss := make([]OneDriveBackup, 0, len(sels))
for _, sel := range sels { for _, sel := range sels {
@ -102,7 +104,7 @@ func (s Selector) ToOneDriveRestore() (*OneDriveRestore, error) {
} }
func (s OneDriveRestore) SplitByResourceOwner(users []string) []OneDriveRestore { func (s OneDriveRestore) SplitByResourceOwner(users []string) []OneDriveRestore {
sels := splitByResourceOwner[OneDriveScope](s.Selector, users, OneDriveUser) sels := splitByProtectedResource[OneDriveScope](s.Selector, users, OneDriveUser)
ss := make([]OneDriveRestore, 0, len(sels)) ss := make([]OneDriveRestore, 0, len(sels))
for _, sel := range sels { for _, sel := range sels {
@ -121,6 +123,13 @@ func (s oneDrive) PathCategories() selectorPathCategories {
} }
} }
// Reasons returns a deduplicated set of the backup reasons produced
// using the selector's discrete owner and each scopes' service and
// category types.
func (s oneDrive) Reasons(tenantID string, useOwnerNameForID bool) []identity.Reasoner {
return reasonsFor(s, tenantID, useOwnerNameForID)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Stringers and Concealers // Stringers and Concealers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -43,16 +43,12 @@ func (suite *OneDriveSelectorSuite) TestToOneDriveBackup() {
} }
func (suite *OneDriveSelectorSuite) TestOneDriveSelector_AllData() { func (suite *OneDriveSelectorSuite) TestOneDriveSelector_AllData() {
t := suite.T()
var ( var (
users = []string{"u1", "u2"} users = []string{"u1", "u2"}
sel = NewOneDriveBackup(users) sel = NewOneDriveBackup(users)
allScopes = sel.AllData() allScopes = sel.AllData()
) )
assert.ElementsMatch(t, users, sel.DiscreteResourceOwners())
// Initialize the selector Include, Exclude, Filter // Initialize the selector Include, Exclude, Filter
sel.Exclude(allScopes) sel.Exclude(allScopes)
sel.Include(allScopes) sel.Include(allScopes)

View File

@ -0,0 +1,91 @@
package selectors
import (
"golang.org/x/exp/maps"
"github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/path"
)
// ---------------------------------------------------------------------------
// reasoner interface compliance
// ---------------------------------------------------------------------------
var _ identity.Reasoner = &backupReason{}
type backupReason struct {
category path.CategoryType
resource string
service path.ServiceType
tenant string
}
func (br backupReason) Tenant() string {
return br.tenant
}
func (br backupReason) ProtectedResource() string {
return br.resource
}
func (br backupReason) Service() path.ServiceType {
return br.service
}
func (br backupReason) Category() path.CategoryType {
return br.category
}
func (br backupReason) SubtreePath() (path.Path, error) {
return path.BuildPrefix(
br.tenant,
br.resource,
br.service,
br.category)
}
func (br backupReason) key() string {
return br.category.String() + br.resource + br.service.String() + br.tenant
}
// ---------------------------------------------------------------------------
// common transformer
// ---------------------------------------------------------------------------
type servicerCategorizerProvider interface {
pathServicer
pathCategorier
idname.Provider
}
func reasonsFor(
sel servicerCategorizerProvider,
tenantID string,
useOwnerNameForID bool,
) []identity.Reasoner {
service := sel.PathService()
reasons := map[string]identity.Reasoner{}
resource := sel.ID()
if useOwnerNameForID {
resource = sel.Name()
}
pc := sel.PathCategories()
for _, sl := range [][]path.CategoryType{pc.Includes, pc.Filters} {
for _, cat := range sl {
br := backupReason{
category: cat,
resource: resource,
service: service,
tenant: tenantID,
}
reasons[br.key()] = br
}
}
return maps.Values(reasons)
}

View File

@ -0,0 +1,406 @@
package selectors
import (
"testing"
"github.com/alcionai/clues"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/path"
)
type ReasonsUnitSuite struct {
tester.Suite
}
func TestReasonsUnitSuite(t *testing.T) {
suite.Run(t, &ReasonsUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *ReasonsUnitSuite) TestReasonsFor_thorough() {
var (
tenantID = "tid"
exchange = path.ExchangeService.String()
email = path.EmailCategory.String()
contacts = path.ContactsCategory.String()
)
type expect struct {
tenant string
resource string
category string
service string
subtreePath string
subtreePathHadErr bool
}
stpFor := func(resource, category, service string) string {
return path.Builder{}.Append(tenantID, service, resource, category).String()
}
table := []struct {
name string
sel func() ExchangeRestore
useName bool
expect []expect
}{
{
name: "no scopes",
sel: func() ExchangeRestore {
return *NewExchangeRestore([]string{"timbo"})
},
expect: []expect{},
},
{
name: "use name",
sel: func() ExchangeRestore {
sel := NewExchangeRestore([]string{"timbo"})
sel.Include(sel.MailFolders(Any()))
plainSel := sel.SetDiscreteOwnerIDName("timbo", "timbubba")
sel, err := plainSel.ToExchangeRestore()
require.NoError(suite.T(), err, clues.ToCore(err))
return *sel
},
useName: true,
expect: []expect{
{
tenant: tenantID,
resource: "timbubba",
category: email,
service: exchange,
subtreePath: stpFor("timbubba", email, exchange),
},
},
},
{
name: "only includes",
sel: func() ExchangeRestore {
sel := *NewExchangeRestore([]string{"bubba"})
sel.Include(sel.MailFolders(Any()))
return sel
},
expect: []expect{
{
tenant: tenantID,
resource: "bubba",
category: email,
service: exchange,
subtreePath: stpFor("bubba", email, exchange),
},
},
},
{
name: "only filters",
sel: func() ExchangeRestore {
sel := *NewExchangeRestore([]string{"tachoma dhaume"})
sel.Filter(sel.MailFolders(Any()))
return sel
},
expect: []expect{
{
tenant: tenantID,
resource: "tachoma dhaume",
category: email,
service: exchange,
subtreePath: stpFor("tachoma dhaume", email, exchange),
},
},
},
{
name: "duplicate includes and filters",
sel: func() ExchangeRestore {
sel := *NewExchangeRestore([]string{"vyng vang zoombah"})
sel.Include(sel.MailFolders(Any()))
sel.Filter(sel.MailFolders(Any()))
return sel
},
expect: []expect{
{
tenant: tenantID,
resource: "vyng vang zoombah",
category: email,
service: exchange,
subtreePath: stpFor("vyng vang zoombah", email, exchange),
},
},
},
{
name: "duplicate includes",
sel: func() ExchangeRestore {
sel := *NewExchangeRestore([]string{"fat billie"})
sel.Include(sel.MailFolders(Any()), sel.MailFolders(Any()))
return sel
},
expect: []expect{
{
tenant: tenantID,
resource: "fat billie",
category: email,
service: exchange,
subtreePath: stpFor("fat billie", email, exchange),
},
},
},
{
name: "duplicate filters",
sel: func() ExchangeRestore {
sel := *NewExchangeRestore([]string{"seathane"})
sel.Filter(sel.MailFolders(Any()), sel.MailFolders(Any()))
return sel
},
expect: []expect{
{
tenant: tenantID,
resource: "seathane",
category: email,
service: exchange,
subtreePath: stpFor("seathane", email, exchange),
},
},
},
{
name: "no duplicates",
sel: func() ExchangeRestore {
sel := *NewExchangeRestore([]string{"perell"})
sel.Include(sel.MailFolders(Any()), sel.ContactFolders(Any()))
return sel
},
expect: []expect{
{
tenant: tenantID,
resource: "perell",
category: email,
service: exchange,
subtreePath: stpFor("perell", email, exchange),
},
{
tenant: tenantID,
resource: "perell",
category: contacts,
service: exchange,
subtreePath: stpFor("perell", contacts, exchange),
},
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
results := []expect{}
rs := reasonsFor(test.sel(), tenantID, test.useName)
for _, r := range rs {
stp, err := r.SubtreePath()
t.Log("stp err", err)
stpStr := ""
if stp != nil {
stpStr = stp.String()
}
results = append(results, expect{
tenant: r.Tenant(),
resource: r.ProtectedResource(),
service: r.Service().String(),
category: r.Category().String(),
subtreePath: stpStr,
subtreePathHadErr: err != nil,
})
}
assert.ElementsMatch(t, test.expect, results)
})
}
}
func (suite *ReasonsUnitSuite) TestReasonsFor_serviceChecks() {
var (
tenantID = "tid"
exchange = path.ExchangeService.String()
email = path.EmailCategory.String()
contacts = path.ContactsCategory.String()
)
type expect struct {
tenant string
resource string
category string
service string
subtreePath string
subtreePathHadErr bool
}
stpFor := func(resource, category, service string) string {
return path.Builder{}.Append(tenantID, service, resource, category).String()
}
table := []struct {
name string
sel func() ExchangeRestore
useName bool
expect []expect
}{
{
name: "no scopes",
sel: func() ExchangeRestore {
return *NewExchangeRestore([]string{"timbo"})
},
expect: []expect{},
},
{
name: "only includes",
sel: func() ExchangeRestore {
sel := *NewExchangeRestore([]string{"bubba"})
sel.Include(sel.MailFolders(Any()))
return sel
},
expect: []expect{
{
tenant: tenantID,
resource: "bubba",
category: email,
service: exchange,
subtreePath: stpFor("bubba", email, exchange),
},
},
},
{
name: "only filters",
sel: func() ExchangeRestore {
sel := *NewExchangeRestore([]string{"tachoma dhaume"})
sel.Filter(sel.MailFolders(Any()))
return sel
},
expect: []expect{
{
tenant: tenantID,
resource: "tachoma dhaume",
category: email,
service: exchange,
subtreePath: stpFor("tachoma dhaume", email, exchange),
},
},
},
{
name: "duplicate includes and filters",
sel: func() ExchangeRestore {
sel := *NewExchangeRestore([]string{"vyng vang zoombah"})
sel.Include(sel.MailFolders(Any()))
sel.Filter(sel.MailFolders(Any()))
return sel
},
expect: []expect{
{
tenant: tenantID,
resource: "vyng vang zoombah",
category: email,
service: exchange,
subtreePath: stpFor("vyng vang zoombah", email, exchange),
},
},
},
{
name: "duplicate includes",
sel: func() ExchangeRestore {
sel := *NewExchangeRestore([]string{"fat billie"})
sel.Include(sel.MailFolders(Any()), sel.MailFolders(Any()))
return sel
},
expect: []expect{
{
tenant: tenantID,
resource: "fat billie",
category: email,
service: exchange,
subtreePath: stpFor("fat billie", email, exchange),
},
},
},
{
name: "duplicate filters",
sel: func() ExchangeRestore {
sel := *NewExchangeRestore([]string{"seathane"})
sel.Filter(sel.MailFolders(Any()), sel.MailFolders(Any()))
return sel
},
expect: []expect{
{
tenant: tenantID,
resource: "seathane",
category: email,
service: exchange,
subtreePath: stpFor("seathane", email, exchange),
},
},
},
{
name: "no duplicates",
sel: func() ExchangeRestore {
sel := *NewExchangeRestore([]string{"perell"})
sel.Include(sel.MailFolders(Any()), sel.ContactFolders(Any()))
return sel
},
expect: []expect{
{
tenant: tenantID,
resource: "perell",
category: email,
service: exchange,
subtreePath: stpFor("perell", email, exchange),
},
{
tenant: tenantID,
resource: "perell",
category: contacts,
service: exchange,
subtreePath: stpFor("perell", contacts, exchange),
},
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
results := []expect{}
rs := reasonsFor(test.sel(), tenantID, test.useName)
for _, r := range rs {
stp, err := r.SubtreePath()
t.Log("stp err", err)
stpStr := ""
if stp != nil {
stpStr = stp.String()
}
results = append(results, expect{
tenant: r.Tenant(),
resource: r.ProtectedResource(),
service: r.Service().String(),
category: r.Category().String(),
subtreePath: stpStr,
subtreePathHadErr: err != nil,
})
}
assert.ElementsMatch(t, test.expect, results)
})
}
}

View File

@ -161,6 +161,267 @@ type (
} }
) )
// appendScopes iterates through each scope in the list of scope slices,
// calling setDefaults() to ensure it is completely populated, and appends
// those scopes to the `to` slice.
func appendScopes[T scopeT](to []scope, scopes ...[]T) []scope {
if len(to) == 0 {
to = []scope{}
}
for _, scopeSl := range scopes {
for _, s := range scopeSl {
s.setDefaults()
to = append(to, scope(s))
}
}
return to
}
// scopes retrieves the list of scopes in the selector.
func scopes[T scopeT](s Selector) []T {
scopes := []T{}
for _, v := range s.Includes {
scopes = append(scopes, T(v))
}
return scopes
}
// ---------------------------------------------------------------------------
// scope config & constructors
// ---------------------------------------------------------------------------
// constructs the default item-scope comparator options according
// to the selector configuration.
// - if cfg.OnlyMatchItemNames == false, then comparison assumes item IDs,
// which are case sensitive, resulting in StrictEqualsMatch
func defaultItemOptions(cfg Config) []option {
opts := []option{}
if !cfg.OnlyMatchItemNames {
opts = append(opts, StrictEqualMatch())
}
return opts
}
type scopeConfig struct {
usePathFilter bool
usePrefixFilter bool
useSuffixFilter bool
useEqualsFilter bool
useStrictEqualsFilter bool
}
type option func(*scopeConfig)
func (sc *scopeConfig) populate(opts ...option) {
for _, opt := range opts {
opt(sc)
}
}
// PrefixMatch ensures the selector uses a Prefix comparator, instead
// of contains or equals. Will not override a default Any() or None()
// comparator.
func PrefixMatch() option {
return func(sc *scopeConfig) {
sc.usePrefixFilter = true
}
}
// SuffixMatch ensures the selector uses a Suffix comparator, instead
// of contains or equals. Will not override a default Any() or None()
// comparator.
func SuffixMatch() option {
return func(sc *scopeConfig) {
sc.useSuffixFilter = true
}
}
// StrictEqualsMatch ensures the selector uses a StrictEquals comparator, instead
// of contains. Will not override a default Any() or None() comparator.
func StrictEqualMatch() option {
return func(sc *scopeConfig) {
sc.useStrictEqualsFilter = true
}
}
// ExactMatch ensures the selector uses an Equals comparator, instead
// of contains. Will not override a default Any() or None() comparator.
func ExactMatch() option {
return func(sc *scopeConfig) {
sc.useEqualsFilter = true
}
}
// pathComparator is an internal-facing option. It is assumed that scope
// constructors will provide the pathComparator option whenever a folder-
// level scope (ie, a scope that compares path hierarchies) is created.
func pathComparator() option {
return func(sc *scopeConfig) {
sc.usePathFilter = true
}
}
func badCastErr(cast, is service) error {
return clues.Stack(ErrorBadSelectorCast, clues.New(fmt.Sprintf("%s is not %s", cast, is)))
}
// if the provided slice contains Any, returns [Any]
// if the slice contains None, returns [None]
// if the slice contains Any and None, returns the first
// if the slice is empty, returns [None]
// otherwise returns the input
func clean(s []string) []string {
if len(s) == 0 {
return None()
}
for _, e := range s {
if e == AnyTgt {
return Any()
}
if e == NoneTgt {
return None()
}
}
return s
}
type filterFunc func([]string) filters.Filter
// filterize turns the slice into a filter.
// if the input is Any(), returns a passAny filter.
// if the input is None(), returns a failAny filter.
// if the scopeConfig specifies a filter, use that filter.
// if the input is len(1), returns an Equals filter.
// otherwise returns a Contains filter.
func filterFor(sc scopeConfig, targets ...string) filters.Filter {
return filterize(sc, nil, targets...)
}
// filterize turns the slice into a filter.
// if the input is Any(), returns a passAny filter.
// if the input is None(), returns a failAny filter.
// if the scopeConfig specifies a filter, use that filter.
// if defaultFilter is non-nil, returns that filter.
// if the input is len(1), returns an Equals filter.
// otherwise returns a Contains filter.
func filterize(
sc scopeConfig,
defaultFilter filterFunc,
targets ...string,
) filters.Filter {
targets = clean(targets)
if len(targets) == 0 || targets[0] == NoneTgt {
return failAny
}
if targets[0] == AnyTgt {
return passAny
}
if sc.usePathFilter {
if sc.useEqualsFilter {
return filters.PathEquals(targets)
}
if sc.usePrefixFilter {
return filters.PathPrefix(targets)
}
if sc.useSuffixFilter {
return filters.PathSuffix(targets)
}
return filters.PathContains(targets)
}
if sc.usePrefixFilter {
return filters.Prefix(targets)
}
if sc.useSuffixFilter {
return filters.Suffix(targets)
}
if sc.useStrictEqualsFilter {
return filters.StrictEqual(targets)
}
if defaultFilter != nil {
return defaultFilter(targets)
}
return filters.Equal(targets)
}
// pathFilterFactory returns the appropriate path filter
// (contains, prefix, or suffix) for the provided options.
// If multiple options are flagged, Prefix takes priority.
// If no options are provided, returns PathContains.
func pathFilterFactory(opts ...option) filterFunc {
sc := &scopeConfig{}
sc.populate(opts...)
var ff filterFunc
switch true {
case sc.usePrefixFilter:
ff = filters.PathPrefix
case sc.useSuffixFilter:
ff = filters.PathSuffix
case sc.useEqualsFilter:
ff = filters.PathEquals
default:
ff = filters.PathContains
}
return wrapSliceFilter(ff)
}
func wrapSliceFilter(ff filterFunc) filterFunc {
return func(s []string) filters.Filter {
s = clean(s)
if f, ok := isAnyOrNone(s); ok {
return f
}
return ff(s)
}
}
// returns (<filter>, true) if s is len==1 and s[0] is
// anyTgt or noneTgt, implying that the caller should use
// the returned filter. On (<filter>, false), the caller
// can ignore the returned filter.
// a special case exists for len(s)==0, interpreted as
// "noneTgt"
func isAnyOrNone(s []string) (filters.Filter, bool) {
switch len(s) {
case 0:
return failAny, true
case 1:
switch s[0] {
case AnyTgt:
return passAny, true
case NoneTgt:
return failAny, true
}
}
return failAny, false
}
// makeScope produces a well formatted, typed scope that ensures all base values are populated. // makeScope produces a well formatted, typed scope that ensures all base values are populated.
func makeScope[T scopeT]( func makeScope[T scopeT](
cat categorizer, cat categorizer,
@ -239,95 +500,9 @@ func marshalScope(mss map[string]string) string {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// scope funcs // reducer & filtering
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// matches returns true if the category is included in the scope's
// data type, and the input string passes the scope's filter for
// that category.
func matches[T scopeT, C categoryT](s T, cat C, inpt string) bool {
if !typeAndCategoryMatches(cat, s.categorizer()) {
return false
}
if len(inpt) == 0 {
return false
}
return s[cat.String()].Compare(inpt)
}
// matchesAny returns true if the category is included in the scope's
// data type, and any one of the input strings passes the scope's filter.
func matchesAny[T scopeT, C categoryT](s T, cat C, inpts []string) bool {
if !typeAndCategoryMatches(cat, s.categorizer()) {
return false
}
if len(inpts) == 0 {
return false
}
return s[cat.String()].CompareAny(inpts...)
}
// getCategory returns the scope's category value.
// if s is an info-type scope, returns the info category.
func getCategory[T scopeT](s T) string {
return s[scopeKeyCategory].Identity
}
// getInfoCategory returns the scope's infoFilter category value.
func getInfoCategory[T scopeT](s T) string {
return s[scopeKeyInfoCategory].Identity
}
// getCatValue takes the value of s[cat] and returns the slice.
// If s[cat] is nil, returns None().
func getCatValue[T scopeT](s T, cat categorizer) []string {
filt, ok := s[cat.String()]
if !ok {
return None()
}
if len(filt.Targets) > 0 {
return filt.Targets
}
return filt.Targets
}
// set sets a value by category to the scope. Only intended for internal
// use, not for exporting to callers.
func set[T scopeT](s T, cat categorizer, v []string, opts ...option) T {
sc := &scopeConfig{}
sc.populate(opts...)
s[cat.String()] = filterFor(*sc, v...)
return s
}
// returns true if the category is included in the scope's category type,
// and the value is set to None().
func isNoneTarget[T scopeT, C categoryT](s T, cat C) bool {
if !typeAndCategoryMatches(cat, s.categorizer()) {
return false
}
return s[cat.String()].Comparator == filters.Fails
}
// returns true if the category is included in the scope's category type,
// and the value is set to Any().
func isAnyTarget[T scopeT, C categoryT](s T, cat C) bool {
if !typeAndCategoryMatches(cat, s.categorizer()) {
return false
}
return s[cat.String()].Comparator == filters.Passes
}
// reduce filters the entries in the details to only those that match the // reduce filters the entries in the details to only those that match the
// inclusions, filters, and exclusions in the selector. // inclusions, filters, and exclusions in the selector.
func reduce[T scopeT, C categoryT]( func reduce[T scopeT, C categoryT](
@ -370,7 +545,7 @@ func reduce[T scopeT, C categoryT](
} }
// first check, every entry needs to match the selector's resource owners. // first check, every entry needs to match the selector's resource owners.
if !matchesResourceOwner.Compare(repoPath.ResourceOwner()) { if !matchesResourceOwner.Compare(repoPath.ProtectedResource()) {
continue continue
} }
@ -542,6 +717,92 @@ func matchesPathValues[T scopeT, C categoryT](
// helper funcs // helper funcs
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// matches returns true if the category is included in the scope's
// data type, and the input string passes the scope's filter for
// that category.
func matches[T scopeT, C categoryT](s T, cat C, inpt string) bool {
if !typeAndCategoryMatches(cat, s.categorizer()) {
return false
}
if len(inpt) == 0 {
return false
}
return s[cat.String()].Compare(inpt)
}
// matchesAny returns true if the category is included in the scope's
// data type, and any one of the input strings passes the scope's filter.
func matchesAny[T scopeT, C categoryT](s T, cat C, inpts []string) bool {
if !typeAndCategoryMatches(cat, s.categorizer()) {
return false
}
if len(inpts) == 0 {
return false
}
return s[cat.String()].CompareAny(inpts...)
}
// getCategory returns the scope's category value.
// if s is an info-type scope, returns the info category.
func getCategory[T scopeT](s T) string {
return s[scopeKeyCategory].Identity
}
// getInfoCategory returns the scope's infoFilter category value.
func getInfoCategory[T scopeT](s T) string {
return s[scopeKeyInfoCategory].Identity
}
// getCatValue takes the value of s[cat] and returns the slice.
// If s[cat] is nil, returns None().
func getCatValue[T scopeT](s T, cat categorizer) []string {
filt, ok := s[cat.String()]
if !ok {
return None()
}
if len(filt.Targets) > 0 {
return filt.Targets
}
return filt.Targets
}
// set sets a value by category to the scope. Only intended for internal
// use, not for exporting to callers.
func set[T scopeT](s T, cat categorizer, v []string, opts ...option) T {
sc := &scopeConfig{}
sc.populate(opts...)
s[cat.String()] = filterFor(*sc, v...)
return s
}
// returns true if the category is included in the scope's category type,
// and the value is set to None().
func isNoneTarget[T scopeT, C categoryT](s T, cat C) bool {
if !typeAndCategoryMatches(cat, s.categorizer()) {
return false
}
return s[cat.String()].Comparator == filters.Fails
}
// returns true if the category is included in the scope's category type,
// and the value is set to Any().
func isAnyTarget[T scopeT, C categoryT](s T, cat C) bool {
if !typeAndCategoryMatches(cat, s.categorizer()) {
return false
}
return s[cat.String()].Comparator == filters.Passes
}
// categoryMatches returns true if: // categoryMatches returns true if:
// - neither type is 'unknown' // - neither type is 'unknown'
// - either type is the root type // - either type is the root type

View File

@ -10,6 +10,7 @@ import (
"github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/filters" "github.com/alcionai/corso/src/pkg/filters"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
@ -87,6 +88,14 @@ type pathCategorier interface {
PathCategories() selectorPathCategories PathCategories() selectorPathCategories
} }
type pathServicer interface {
PathService() path.ServiceType
}
type reasoner interface {
Reasons(tenantID string, useOwnerNameForID bool) []identity.Reasoner
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Selector // Selector
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -159,11 +168,25 @@ func (s *Selector) Configure(cfg Config) {
s.Cfg = cfg s.Cfg = cfg
} }
// DiscreteResourceOwners returns the list of individual resourceOwners used // ---------------------------------------------------------------------------
// in the selector. // protected resources & idname provider compliance
// TODO(rkeepers): remove in favor of split and s.DiscreteOwner // ---------------------------------------------------------------------------
func (s Selector) DiscreteResourceOwners() []string {
return s.ResourceOwners.Targets var _ idname.Provider = &Selector{}
// ID returns s.discreteOwner, which is assumed to be a stable ID.
func (s Selector) ID() string {
return s.DiscreteOwner
}
// Name returns s.discreteOwnerName. If that value is empty, it returns
// s.DiscreteOwner instead.
func (s Selector) Name() string {
if len(s.DiscreteOwnerName) == 0 {
return s.DiscreteOwner
}
return s.DiscreteOwnerName
} }
// SetDiscreteOwnerIDName ensures the selector has the correct discrete owner // SetDiscreteOwnerIDName ensures the selector has the correct discrete owner
@ -193,32 +216,17 @@ func (s Selector) SetDiscreteOwnerIDName(id, name string) Selector {
return r return r
} }
// ID returns s.discreteOwner, which is assumed to be a stable ID. // isAnyProtectedResource returns true if the selector includes all resource owners.
func (s Selector) ID() string { func isAnyProtectedResource(s Selector) bool {
return s.DiscreteOwner
}
// Name returns s.discreteOwnerName. If that value is empty, it returns
// s.DiscreteOwner instead.
func (s Selector) Name() string {
if len(s.DiscreteOwnerName) == 0 {
return s.DiscreteOwner
}
return s.DiscreteOwnerName
}
// isAnyResourceOwner returns true if the selector includes all resource owners.
func isAnyResourceOwner(s Selector) bool {
return s.ResourceOwners.Comparator == filters.Passes return s.ResourceOwners.Comparator == filters.Passes
} }
// isNoneResourceOwner returns true if the selector includes no resource owners. // isNoneProtectedResource returns true if the selector includes no resource owners.
func isNoneResourceOwner(s Selector) bool { func isNoneProtectedResource(s Selector) bool {
return s.ResourceOwners.Comparator == filters.Fails return s.ResourceOwners.Comparator == filters.Fails
} }
// SplitByResourceOwner makes one shallow clone for each resourceOwner in the // splitByProtectedResource makes one shallow clone for each resourceOwner in the
// selector, specifying a new DiscreteOwner for each one. // selector, specifying a new DiscreteOwner for each one.
// If the original selector already specified a discrete slice of resource owners, // If the original selector already specified a discrete slice of resource owners,
// only those owners are used in the result. // only those owners are used in the result.
@ -230,14 +238,14 @@ func isNoneResourceOwner(s Selector) bool {
// //
// temporarily, clones all scopes in each selector and replaces the owners with // temporarily, clones all scopes in each selector and replaces the owners with
// the discrete owner. // the discrete owner.
func splitByResourceOwner[T scopeT, C categoryT](s Selector, allOwners []string, rootCat C) []Selector { func splitByProtectedResource[T scopeT, C categoryT](s Selector, allOwners []string, rootCat C) []Selector {
if isNoneResourceOwner(s) { if isNoneProtectedResource(s) {
return []Selector{} return []Selector{}
} }
targets := allOwners targets := allOwners
if !isAnyResourceOwner(s) { if !isAnyProtectedResource(s) {
targets = s.ResourceOwners.Targets targets = s.ResourceOwners.Targets
} }
@ -252,35 +260,6 @@ func splitByResourceOwner[T scopeT, C categoryT](s Selector, allOwners []string,
return ss return ss
} }
// appendScopes iterates through each scope in the list of scope slices,
// calling setDefaults() to ensure it is completely populated, and appends
// those scopes to the `to` slice.
func appendScopes[T scopeT](to []scope, scopes ...[]T) []scope {
if len(to) == 0 {
to = []scope{}
}
for _, scopeSl := range scopes {
for _, s := range scopeSl {
s.setDefaults()
to = append(to, scope(s))
}
}
return to
}
// scopes retrieves the list of scopes in the selector.
func scopes[T scopeT](s Selector) []T {
scopes := []T{}
for _, v := range s.Includes {
scopes = append(scopes, T(v))
}
return scopes
}
// Returns the path.ServiceType matching the selector service. // Returns the path.ServiceType matching the selector service.
func (s Selector) PathService() path.ServiceType { func (s Selector) PathService() path.ServiceType {
return serviceToPathType[s.Service] return serviceToPathType[s.Service]
@ -303,7 +282,7 @@ func (s Selector) Reduce(
return r.Reduce(ctx, deets, errs), nil return r.Reduce(ctx, deets, errs), nil
} }
// returns the sets of path categories identified in each scope set. // PathCategories returns the sets of path categories identified in each scope set.
func (s Selector) PathCategories() (selectorPathCategories, error) { func (s Selector) PathCategories() (selectorPathCategories, error) {
ro, err := selectorAsIface[pathCategorier](s) ro, err := selectorAsIface[pathCategorier](s)
if err != nil { if err != nil {
@ -313,6 +292,18 @@ func (s Selector) PathCategories() (selectorPathCategories, error) {
return ro.PathCategories(), nil return ro.PathCategories(), nil
} }
// Reasons returns a deduplicated set of the backup reasons produced
// using the selector's discrete owner and each scopes' service and
// category types.
func (s Selector) Reasons(tenantID string, useOwnerNameForID bool) ([]identity.Reasoner, error) {
ro, err := selectorAsIface[reasoner](s)
if err != nil {
return nil, err
}
return ro.Reasons(tenantID, useOwnerNameForID), nil
}
// transformer for arbitrary selector interfaces // transformer for arbitrary selector interfaces
func selectorAsIface[T any](s Selector) (T, error) { func selectorAsIface[T any](s Selector) (T, error) {
var ( var (
@ -331,6 +322,9 @@ func selectorAsIface[T any](s Selector) (T, error) {
case ServiceSharePoint: case ServiceSharePoint:
a, err = func() (any, error) { return s.ToSharePointRestore() }() a, err = func() (any, error) { return s.ToSharePointRestore() }()
t = a.(T) t = a.(T)
case ServiceGroups:
a, err = func() (any, error) { return s.ToGroupsRestore() }()
t = a.(T)
default: default:
err = clues.Stack(ErrorUnrecognizedService, clues.New(s.Service.String())) err = clues.Stack(ErrorUnrecognizedService, clues.New(s.Service.String()))
} }
@ -420,28 +414,6 @@ func (ls loggableSelector) marshal() string {
// helpers // helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// produces the discrete set of resource owners in the slice of scopes.
// Any and None values are discarded.
func resourceOwnersIn(s []scope, rootCat string) []string {
rm := map[string]struct{}{}
for _, sc := range s {
for _, v := range sc[rootCat].Targets {
rm[v] = struct{}{}
}
}
rs := []string{}
for k := range rm {
if k != AnyTgt && k != NoneTgt {
rs = append(rs, k)
}
}
return rs
}
// produces the discrete set of path categories in the slice of scopes. // produces the discrete set of path categories in the slice of scopes.
func pathCategoriesIn[T scopeT, C categoryT](ss []scope) []path.CategoryType { func pathCategoriesIn[T scopeT, C categoryT](ss []scope) []path.CategoryType {
m := map[path.CategoryType]struct{}{} m := map[path.CategoryType]struct{}{}
@ -459,235 +431,3 @@ func pathCategoriesIn[T scopeT, C categoryT](ss []scope) []path.CategoryType {
return maps.Keys(m) return maps.Keys(m)
} }
// ---------------------------------------------------------------------------
// scope constructors
// ---------------------------------------------------------------------------
// constructs the default item-scope comparator options according
// to the selector configuration.
// - if cfg.OnlyMatchItemNames == false, then comparison assumes item IDs,
// which are case sensitive, resulting in StrictEqualsMatch
func defaultItemOptions(cfg Config) []option {
opts := []option{}
if !cfg.OnlyMatchItemNames {
opts = append(opts, StrictEqualMatch())
}
return opts
}
type scopeConfig struct {
usePathFilter bool
usePrefixFilter bool
useSuffixFilter bool
useEqualsFilter bool
useStrictEqualsFilter bool
}
type option func(*scopeConfig)
func (sc *scopeConfig) populate(opts ...option) {
for _, opt := range opts {
opt(sc)
}
}
// PrefixMatch ensures the selector uses a Prefix comparator, instead
// of contains or equals. Will not override a default Any() or None()
// comparator.
func PrefixMatch() option {
return func(sc *scopeConfig) {
sc.usePrefixFilter = true
}
}
// SuffixMatch ensures the selector uses a Suffix comparator, instead
// of contains or equals. Will not override a default Any() or None()
// comparator.
func SuffixMatch() option {
return func(sc *scopeConfig) {
sc.useSuffixFilter = true
}
}
// StrictEqualsMatch ensures the selector uses a StrictEquals comparator, instead
// of contains. Will not override a default Any() or None() comparator.
func StrictEqualMatch() option {
return func(sc *scopeConfig) {
sc.useStrictEqualsFilter = true
}
}
// ExactMatch ensures the selector uses an Equals comparator, instead
// of contains. Will not override a default Any() or None() comparator.
func ExactMatch() option {
return func(sc *scopeConfig) {
sc.useEqualsFilter = true
}
}
// pathComparator is an internal-facing option. It is assumed that scope
// constructors will provide the pathComparator option whenever a folder-
// level scope (ie, a scope that compares path hierarchies) is created.
func pathComparator() option {
return func(sc *scopeConfig) {
sc.usePathFilter = true
}
}
func badCastErr(cast, is service) error {
return clues.Stack(ErrorBadSelectorCast, clues.New(fmt.Sprintf("%s is not %s", cast, is)))
}
// if the provided slice contains Any, returns [Any]
// if the slice contains None, returns [None]
// if the slice contains Any and None, returns the first
// if the slice is empty, returns [None]
// otherwise returns the input
func clean(s []string) []string {
if len(s) == 0 {
return None()
}
for _, e := range s {
if e == AnyTgt {
return Any()
}
if e == NoneTgt {
return None()
}
}
return s
}
type filterFunc func([]string) filters.Filter
// filterize turns the slice into a filter.
// if the input is Any(), returns a passAny filter.
// if the input is None(), returns a failAny filter.
// if the scopeConfig specifies a filter, use that filter.
// if the input is len(1), returns an Equals filter.
// otherwise returns a Contains filter.
func filterFor(sc scopeConfig, targets ...string) filters.Filter {
return filterize(sc, nil, targets...)
}
// filterize turns the slice into a filter.
// if the input is Any(), returns a passAny filter.
// if the input is None(), returns a failAny filter.
// if the scopeConfig specifies a filter, use that filter.
// if defaultFilter is non-nil, returns that filter.
// if the input is len(1), returns an Equals filter.
// otherwise returns a Contains filter.
func filterize(
sc scopeConfig,
defaultFilter filterFunc,
targets ...string,
) filters.Filter {
targets = clean(targets)
if len(targets) == 0 || targets[0] == NoneTgt {
return failAny
}
if targets[0] == AnyTgt {
return passAny
}
if sc.usePathFilter {
if sc.useEqualsFilter {
return filters.PathEquals(targets)
}
if sc.usePrefixFilter {
return filters.PathPrefix(targets)
}
if sc.useSuffixFilter {
return filters.PathSuffix(targets)
}
return filters.PathContains(targets)
}
if sc.usePrefixFilter {
return filters.Prefix(targets)
}
if sc.useSuffixFilter {
return filters.Suffix(targets)
}
if sc.useStrictEqualsFilter {
return filters.StrictEqual(targets)
}
if defaultFilter != nil {
return defaultFilter(targets)
}
return filters.Equal(targets)
}
// pathFilterFactory returns the appropriate path filter
// (contains, prefix, or suffix) for the provided options.
// If multiple options are flagged, Prefix takes priority.
// If no options are provided, returns PathContains.
func pathFilterFactory(opts ...option) filterFunc {
sc := &scopeConfig{}
sc.populate(opts...)
var ff filterFunc
switch true {
case sc.usePrefixFilter:
ff = filters.PathPrefix
case sc.useSuffixFilter:
ff = filters.PathSuffix
case sc.useEqualsFilter:
ff = filters.PathEquals
default:
ff = filters.PathContains
}
return wrapSliceFilter(ff)
}
func wrapSliceFilter(ff filterFunc) filterFunc {
return func(s []string) filters.Filter {
s = clean(s)
if f, ok := isAnyOrNone(s); ok {
return f
}
return ff(s)
}
}
// returns (<filter>, true) if s is len==1 and s[0] is
// anyTgt or noneTgt, implying that the caller should use
// the returned filter. On (<filter>, false), the caller
// can ignore the returned filter.
// a special case exists for len(s)==0, interpreted as
// "noneTgt"
func isAnyOrNone(s []string) (filters.Filter, bool) {
switch len(s) {
case 0:
return failAny, true
case 1:
switch s[0] {
case AnyTgt:
return passAny, true
case NoneTgt:
return failAny, true
}
}
return failAny, false
}

View File

@ -44,56 +44,6 @@ func (suite *SelectorSuite) TestBadCastErr() {
assert.Error(suite.T(), err, clues.ToCore(err)) assert.Error(suite.T(), err, clues.ToCore(err))
} }
func (suite *SelectorSuite) TestResourceOwnersIn() {
rootCat := rootCatStub.String()
table := []struct {
name string
input []scope
expect []string
}{
{
name: "nil",
input: nil,
expect: []string{},
},
{
name: "empty",
input: []scope{},
expect: []string{},
},
{
name: "single",
input: []scope{{rootCat: filters.Identity("foo")}},
expect: []string{"foo"},
},
{
name: "multiple scopes",
input: []scope{
{rootCat: filters.Identity("foo,bar")},
{rootCat: filters.Identity("baz")},
},
expect: []string{"foo,bar", "baz"},
},
{
name: "multiple scopes with duplicates",
input: []scope{
{rootCat: filters.Identity("foo")},
{rootCat: filters.Identity("foo")},
},
expect: []string{"foo"},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
result := resourceOwnersIn(test.input, rootCat)
assert.ElementsMatch(t, test.expect, result)
})
}
}
func (suite *SelectorSuite) TestPathCategoriesIn() { func (suite *SelectorSuite) TestPathCategoriesIn() {
leafCat := leafCatStub.String() leafCat := leafCatStub.String()
f := filters.Identity(leafCat) f := filters.Identity(leafCat)
@ -144,20 +94,20 @@ func (suite *SelectorSuite) TestContains() {
func (suite *SelectorSuite) TestIsAnyResourceOwner() { func (suite *SelectorSuite) TestIsAnyResourceOwner() {
t := suite.T() t := suite.T()
assert.False(t, isAnyResourceOwner(newSelector(ServiceUnknown, []string{"foo"}))) assert.False(t, isAnyProtectedResource(newSelector(ServiceUnknown, []string{"foo"})))
assert.False(t, isAnyResourceOwner(newSelector(ServiceUnknown, []string{}))) assert.False(t, isAnyProtectedResource(newSelector(ServiceUnknown, []string{})))
assert.False(t, isAnyResourceOwner(newSelector(ServiceUnknown, nil))) assert.False(t, isAnyProtectedResource(newSelector(ServiceUnknown, nil)))
assert.True(t, isAnyResourceOwner(newSelector(ServiceUnknown, []string{AnyTgt}))) assert.True(t, isAnyProtectedResource(newSelector(ServiceUnknown, []string{AnyTgt})))
assert.True(t, isAnyResourceOwner(newSelector(ServiceUnknown, Any()))) assert.True(t, isAnyProtectedResource(newSelector(ServiceUnknown, Any())))
} }
func (suite *SelectorSuite) TestIsNoneResourceOwner() { func (suite *SelectorSuite) TestIsNoneResourceOwner() {
t := suite.T() t := suite.T()
assert.False(t, isNoneResourceOwner(newSelector(ServiceUnknown, []string{"foo"}))) assert.False(t, isNoneProtectedResource(newSelector(ServiceUnknown, []string{"foo"})))
assert.True(t, isNoneResourceOwner(newSelector(ServiceUnknown, []string{}))) assert.True(t, isNoneProtectedResource(newSelector(ServiceUnknown, []string{})))
assert.True(t, isNoneResourceOwner(newSelector(ServiceUnknown, nil))) assert.True(t, isNoneProtectedResource(newSelector(ServiceUnknown, nil)))
assert.True(t, isNoneResourceOwner(newSelector(ServiceUnknown, []string{NoneTgt}))) assert.True(t, isNoneProtectedResource(newSelector(ServiceUnknown, []string{NoneTgt})))
assert.True(t, isNoneResourceOwner(newSelector(ServiceUnknown, None()))) assert.True(t, isNoneProtectedResource(newSelector(ServiceUnknown, None())))
} }
func (suite *SelectorSuite) TestSplitByResourceOnwer() { func (suite *SelectorSuite) TestSplitByResourceOnwer() {
@ -224,7 +174,7 @@ func (suite *SelectorSuite) TestSplitByResourceOnwer() {
t := suite.T() t := suite.T()
s := newSelector(ServiceUnknown, test.input) s := newSelector(ServiceUnknown, test.input)
result := splitByResourceOwner[mockScope](s, allOwners, rootCatStub) result := splitByProtectedResource[mockScope](s, allOwners, rootCatStub)
assert.Len(t, result, test.expectLen) assert.Len(t, result, test.expectLen)

View File

@ -8,6 +8,7 @@ import (
"github.com/alcionai/corso/src/internal/common/dttm" "github.com/alcionai/corso/src/internal/common/dttm"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/filters" "github.com/alcionai/corso/src/pkg/filters"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
@ -42,6 +43,7 @@ type (
var ( var (
_ Reducer = &SharePointRestore{} _ Reducer = &SharePointRestore{}
_ pathCategorier = &SharePointRestore{} _ pathCategorier = &SharePointRestore{}
_ reasoner = &SharePointRestore{}
) )
// NewSharePointBackup produces a new Selector with the service set to ServiceSharePoint. // NewSharePointBackup produces a new Selector with the service set to ServiceSharePoint.
@ -68,7 +70,7 @@ func (s Selector) ToSharePointBackup() (*SharePointBackup, error) {
} }
func (s SharePointBackup) SplitByResourceOwner(sites []string) []SharePointBackup { func (s SharePointBackup) SplitByResourceOwner(sites []string) []SharePointBackup {
sels := splitByResourceOwner[SharePointScope](s.Selector, sites, SharePointSite) sels := splitByProtectedResource[SharePointScope](s.Selector, sites, SharePointSite)
ss := make([]SharePointBackup, 0, len(sels)) ss := make([]SharePointBackup, 0, len(sels))
for _, sel := range sels { for _, sel := range sels {
@ -102,7 +104,7 @@ func (s Selector) ToSharePointRestore() (*SharePointRestore, error) {
} }
func (s SharePointRestore) SplitByResourceOwner(sites []string) []SharePointRestore { func (s SharePointRestore) SplitByResourceOwner(sites []string) []SharePointRestore {
sels := splitByResourceOwner[SharePointScope](s.Selector, sites, SharePointSite) sels := splitByProtectedResource[SharePointScope](s.Selector, sites, SharePointSite)
ss := make([]SharePointRestore, 0, len(sels)) ss := make([]SharePointRestore, 0, len(sels))
for _, sel := range sels { for _, sel := range sels {
@ -121,6 +123,13 @@ func (s sharePoint) PathCategories() selectorPathCategories {
} }
} }
// Reasons returns a deduplicated set of the backup reasons produced
// using the selector's discrete owner and each scopes' service and
// category types.
func (s sharePoint) Reasons(tenantID string, useOwnerNameForID bool) []identity.Reasoner {
return reasonsFor(s, tenantID, useOwnerNameForID)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Stringers and Concealers // Stringers and Concealers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -44,66 +44,6 @@ func (suite *SharePointSelectorSuite) TestToSharePointBackup() {
assert.NotZero(t, ob.Scopes()) assert.NotZero(t, ob.Scopes())
} }
func (suite *SharePointSelectorSuite) TestSharePointSelector_AllData() {
t := suite.T()
sites := []string{"s1", "s2"}
sel := NewSharePointBackup(sites)
siteScopes := sel.AllData()
assert.ElementsMatch(t, sites, sel.DiscreteResourceOwners())
// Initialize the selector Include, Exclude, Filter
sel.Exclude(siteScopes)
sel.Include(siteScopes)
sel.Filter(siteScopes)
table := []struct {
name string
scopesToCheck []scope
}{
{"Include Scopes", sel.Includes},
{"Exclude Scopes", sel.Excludes},
{"info scopes", sel.Filters},
}
for _, test := range table {
require.Len(t, test.scopesToCheck, 3)
for _, scope := range test.scopesToCheck {
var (
spsc = SharePointScope(scope)
cat = spsc.Category()
)
suite.Run(test.name+"-"+cat.String(), func() {
t := suite.T()
switch cat {
case SharePointLibraryItem:
scopeMustHave(
t,
spsc,
map[categorizer][]string{
SharePointLibraryItem: Any(),
SharePointLibraryFolder: Any(),
},
)
case SharePointListItem:
scopeMustHave(
t,
spsc,
map[categorizer][]string{
SharePointListItem: Any(),
SharePointList: Any(),
},
)
}
})
}
}
}
func (suite *SharePointSelectorSuite) TestSharePointSelector_Include_WebURLs() { func (suite *SharePointSelectorSuite) TestSharePointSelector_Include_WebURLs() {
t := suite.T() t := suite.T()