Compare commits
5 Commits
main
...
3993-path-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0ea40984a | ||
|
|
be60a6d1e4 | ||
|
|
cbbc6c61dd | ||
|
|
e06e6b7bb4 | ||
|
|
e3e90e397c |
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -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),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
4
src/pkg/backup/details/testdata/testdata.go
vendored
4
src/pkg/backup/details/testdata/testdata.go
vendored
@ -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)
|
||||||
|
|||||||
16
src/pkg/backup/identity/identity.go
Normal file
16
src/pkg/backup/identity/identity.go
Normal 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
379
src/pkg/path/builder.go
Normal 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()
|
||||||
|
}
|
||||||
369
src/pkg/path/builder_test.go
Normal file
369
src/pkg/path/builder_test.go
Normal 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")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/pkg/path/category_type.go
Normal file
108
src/pkg/path/category_type.go
Normal 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
|
||||||
|
}
|
||||||
@ -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`
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
53
src/pkg/path/service_type.go
Normal file
53
src/pkg/path/service_type.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -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
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -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
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
91
src/pkg/selectors/reasons.go
Normal file
91
src/pkg/selectors/reasons.go
Normal 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)
|
||||||
|
}
|
||||||
406
src/pkg/selectors/reasons_test.go
Normal file
406
src/pkg/selectors/reasons_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user