Issue-271-mustfilter: add must-pass filters to selectors (#392)

* add must-pass filters to selectors

Extends the selectors scope set to include Filters.  This
allows users to define all-pass matchers (filters),
separate from any-pass matchers (includes), and global
exclusions.
This commit is contained in:
Keepers 2022-07-25 16:41:49 -06:00 committed by GitHub
parent 75e8317bf9
commit 350d27dcb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 313 additions and 228 deletions

View File

@ -316,7 +316,7 @@ func detailsExchangeCmd(cmd *cobra.Command, args []string) error {
emailFolder, emailFolder,
event, event,
user) user)
includeExchangeBackupDetailInfoSelectors( filterExchangeBackupDetailInfoSelectors(
sel, sel,
emailReceivedAfter, emailReceivedAfter,
emailReceivedBefore, emailReceivedBefore,
@ -328,7 +328,7 @@ func detailsExchangeCmd(cmd *cobra.Command, args []string) error {
sel.Include(sel.Users(selectors.Any())) sel.Include(sel.Users(selectors.Any()))
} }
ds := sel.FilterDetails(d) ds := sel.Reduce(d)
print.Entries(ds.Entries) print.Entries(ds.Entries)
return nil return nil
@ -389,43 +389,43 @@ func includeExchangeEvents(sel *selectors.ExchangeRestore, users, events []strin
sel.Include(sel.Events(users, events)) sel.Include(sel.Events(users, events))
} }
// builds the info-selector inclusions for `backup details exchange` // builds the info-selector filters for `backup details exchange`
func includeExchangeBackupDetailInfoSelectors( func filterExchangeBackupDetailInfoSelectors(
sel *selectors.ExchangeRestore, sel *selectors.ExchangeRestore,
emailReceivedAfter, emailReceivedBefore, emailSender, emailSubject []string, emailReceivedAfter, emailReceivedBefore, emailSender, emailSubject []string,
) { ) {
includeExchangeInfoMailReceivedAfter(sel, emailReceivedAfter) filterExchangeInfoMailReceivedAfter(sel, emailReceivedAfter)
includeExchangeInfoMailReceivedBefore(sel, emailReceivedBefore) filterExchangeInfoMailReceivedBefore(sel, emailReceivedBefore)
includeExchangeInfoMailSender(sel, emailSender) filterExchangeInfoMailSender(sel, emailSender)
includeExchangeInfoMailSubject(sel, emailSubject) filterExchangeInfoMailSubject(sel, emailSubject)
} }
func includeExchangeInfoMailReceivedAfter(sel *selectors.ExchangeRestore, receivedAfter []string) { func filterExchangeInfoMailReceivedAfter(sel *selectors.ExchangeRestore, receivedAfter []string) {
if len(receivedAfter) == 0 { if len(receivedAfter) == 0 {
return return
} }
sel.Include(sel.MailReceivedAfter(receivedAfter)) sel.Filter(sel.MailReceivedAfter(receivedAfter))
} }
func includeExchangeInfoMailReceivedBefore(sel *selectors.ExchangeRestore, receivedBefore []string) { func filterExchangeInfoMailReceivedBefore(sel *selectors.ExchangeRestore, receivedBefore []string) {
if len(receivedBefore) == 0 { if len(receivedBefore) == 0 {
return return
} }
sel.Include(sel.MailReceivedBefore(receivedBefore)) sel.Filter(sel.MailReceivedBefore(receivedBefore))
} }
func includeExchangeInfoMailSender(sel *selectors.ExchangeRestore, sender []string) { func filterExchangeInfoMailSender(sel *selectors.ExchangeRestore, sender []string) {
if len(sender) == 0 { if len(sender) == 0 {
return return
} }
sel.Include(sel.MailSender(sender)) sel.Filter(sel.MailSender(sender))
} }
func includeExchangeInfoMailSubject(sel *selectors.ExchangeRestore, subject []string) { func filterExchangeInfoMailSubject(sel *selectors.ExchangeRestore, subject []string) {
if len(subject) == 0 { if len(subject) == 0 {
return return
} }
sel.Include(sel.MailSubject(subject)) sel.Filter(sel.MailSubject(subject))
} }
// checks all flags for correctness and interdependencies // checks all flags for correctness and interdependencies

View File

@ -480,78 +480,78 @@ func (suite *ExchangeSuite) TestIncludeExchangeBackupDetailDataSelectors() {
} }
} }
func (suite *ExchangeSuite) TestIncludeExchangeBackupDetailInfoSelectors() { func (suite *ExchangeSuite) TestFilterExchangeBackupDetailInfoSelectors() {
stub := []string{"id-stub"} stub := []string{"id-stub"}
twoStubs := []string{"smarfs", "fnords"} twoStubs := []string{"smarfs", "fnords"}
any := []string{utils.Wildcard} any := []string{utils.Wildcard}
table := []struct { table := []struct {
name string name string
after, before, sender, subject []string after, before, sender, subject []string
expectIncludeLen int expectFilterLen int
}{ }{
{ {
name: "no selectors", name: "no selectors",
expectIncludeLen: 0, expectFilterLen: 0,
}, },
{ {
name: "any receivedAfter", name: "any receivedAfter",
after: any, after: any,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "single receivedAfter", name: "single receivedAfter",
after: stub, after: stub,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "multiple receivedAfter", name: "multiple receivedAfter",
after: twoStubs, after: twoStubs,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "any receivedBefore", name: "any receivedBefore",
before: any, before: any,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "single receivedBefore", name: "single receivedBefore",
before: stub, before: stub,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "multiple receivedBefore", name: "multiple receivedBefore",
before: twoStubs, before: twoStubs,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "any sender", name: "any sender",
sender: any, sender: any,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "single sender", name: "single sender",
sender: stub, sender: stub,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "multiple senders", name: "multiple senders",
sender: twoStubs, sender: twoStubs,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "any subject", name: "any subject",
subject: any, subject: any,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "single subject", name: "single subject",
subject: stub, subject: stub,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "multiple subjects", name: "multiple subjects",
subject: twoStubs, subject: twoStubs,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "one of each", name: "one of each",
@ -559,19 +559,19 @@ func (suite *ExchangeSuite) TestIncludeExchangeBackupDetailInfoSelectors() {
before: stub, before: stub,
sender: stub, sender: stub,
subject: stub, subject: stub,
expectIncludeLen: 4, expectFilterLen: 4,
}, },
} }
for _, test := range table { for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {
sel := selectors.NewExchangeRestore() sel := selectors.NewExchangeRestore()
includeExchangeBackupDetailInfoSelectors( filterExchangeBackupDetailInfoSelectors(
sel, sel,
test.after, test.after,
test.before, test.before,
test.sender, test.sender,
test.subject) test.subject)
assert.Equal(t, test.expectIncludeLen, len(sel.Includes)) assert.Equal(t, test.expectFilterLen, len(sel.Filters))
}) })
} }
} }

View File

@ -138,7 +138,7 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error {
emailFolder, emailFolder,
event, event,
user) user)
includeExchangeRestoreInfoSelectors( filterExchangeRestoreInfoSelectors(
sel, sel,
emailReceivedAfter, emailReceivedAfter,
emailReceivedBefore, emailReceivedBefore,
@ -218,43 +218,43 @@ func includeExchangeEvents(sel *selectors.ExchangeRestore, users, events []strin
sel.Include(sel.Events(users, events)) sel.Include(sel.Events(users, events))
} }
// builds the info-selector inclusions for `restore exchange` // builds the info-selector filters for `restore exchange`
func includeExchangeRestoreInfoSelectors( func filterExchangeRestoreInfoSelectors(
sel *selectors.ExchangeRestore, sel *selectors.ExchangeRestore,
emailReceivedAfter, emailReceivedBefore, emailSender, emailSubject []string, emailReceivedAfter, emailReceivedBefore, emailSender, emailSubject []string,
) { ) {
includeExchangeInfoMailReceivedAfter(sel, emailReceivedAfter) filterExchangeInfoMailReceivedAfter(sel, emailReceivedAfter)
includeExchangeInfoMailReceivedBefore(sel, emailReceivedBefore) filterExchangeInfoMailReceivedBefore(sel, emailReceivedBefore)
includeExchangeInfoMailSender(sel, emailSender) filterExchangeInfoMailSender(sel, emailSender)
includeExchangeInfoMailSubject(sel, emailSubject) filterExchangeInfoMailSubject(sel, emailSubject)
} }
func includeExchangeInfoMailReceivedAfter(sel *selectors.ExchangeRestore, receivedAfter []string) { func filterExchangeInfoMailReceivedAfter(sel *selectors.ExchangeRestore, receivedAfter []string) {
if len(receivedAfter) == 0 { if len(receivedAfter) == 0 {
return return
} }
sel.Include(sel.MailReceivedAfter(receivedAfter)) sel.Filter(sel.MailReceivedAfter(receivedAfter))
} }
func includeExchangeInfoMailReceivedBefore(sel *selectors.ExchangeRestore, receivedBefore []string) { func filterExchangeInfoMailReceivedBefore(sel *selectors.ExchangeRestore, receivedBefore []string) {
if len(receivedBefore) == 0 { if len(receivedBefore) == 0 {
return return
} }
sel.Include(sel.MailReceivedBefore(receivedBefore)) sel.Filter(sel.MailReceivedBefore(receivedBefore))
} }
func includeExchangeInfoMailSender(sel *selectors.ExchangeRestore, sender []string) { func filterExchangeInfoMailSender(sel *selectors.ExchangeRestore, sender []string) {
if len(sender) == 0 { if len(sender) == 0 {
return return
} }
sel.Include(sel.MailSender(sender)) sel.Filter(sel.MailSender(sender))
} }
func includeExchangeInfoMailSubject(sel *selectors.ExchangeRestore, subject []string) { func filterExchangeInfoMailSubject(sel *selectors.ExchangeRestore, subject []string) {
if len(subject) == 0 { if len(subject) == 0 {
return return
} }
sel.Include(sel.MailSubject(subject)) sel.Filter(sel.MailSubject(subject))
} }
// checks all flags for correctness and interdependencies // checks all flags for correctness and interdependencies

View File

@ -316,78 +316,78 @@ func (suite *ExchangeSuite) TestIncludeExchangeRestoreDataSelectors() {
} }
} }
func (suite *ExchangeSuite) TestIncludeExchangeRestoreInfoSelectors() { func (suite *ExchangeSuite) TestFilterExchangeRestoreInfoSelectors() {
stub := []string{"id-stub"} stub := []string{"id-stub"}
twoStubs := []string{"a-stub", "b-stub"} twoStubs := []string{"a-stub", "b-stub"}
any := []string{utils.Wildcard} any := []string{utils.Wildcard}
table := []struct { table := []struct {
name string name string
after, before, sender, subject []string after, before, sender, subject []string
expectIncludeLen int expectFilterLen int
}{ }{
{ {
name: "no selectors", name: "no selectors",
expectIncludeLen: 0, expectFilterLen: 0,
}, },
{ {
name: "any receivedAfter", name: "any receivedAfter",
after: any, after: any,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "single receivedAfter", name: "single receivedAfter",
after: stub, after: stub,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "multiple receivedAfter", name: "multiple receivedAfter",
after: twoStubs, after: twoStubs,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "any receivedBefore", name: "any receivedBefore",
before: any, before: any,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "single receivedBefore", name: "single receivedBefore",
before: stub, before: stub,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "multiple receivedBefore", name: "multiple receivedBefore",
before: twoStubs, before: twoStubs,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "any senders", name: "any senders",
sender: any, sender: any,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "single sender", name: "single sender",
sender: stub, sender: stub,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "multiple senders", name: "multiple senders",
sender: twoStubs, sender: twoStubs,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "any subjects", name: "any subjects",
subject: any, subject: any,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "single subject", name: "single subject",
subject: stub, subject: stub,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "multiple subjects", name: "multiple subjects",
subject: twoStubs, subject: twoStubs,
expectIncludeLen: 1, expectFilterLen: 1,
}, },
{ {
name: "one of each", name: "one of each",
@ -395,19 +395,19 @@ func (suite *ExchangeSuite) TestIncludeExchangeRestoreInfoSelectors() {
before: stub, before: stub,
sender: stub, sender: stub,
subject: stub, subject: stub,
expectIncludeLen: 4, expectFilterLen: 4,
}, },
} }
for _, test := range table { for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {
sel := selectors.NewExchangeRestore() sel := selectors.NewExchangeRestore()
includeExchangeRestoreInfoSelectors( filterExchangeRestoreInfoSelectors(
sel, sel,
test.after, test.after,
test.before, test.before,
test.sender, test.sender,
test.subject) test.subject)
assert.Equal(t, test.expectIncludeLen, len(sel.Includes)) assert.Equal(t, test.expectFilterLen, len(sel.Filters))
}) })
} }
} }

View File

@ -95,7 +95,7 @@ func (op *RestoreOperation) Run(ctx context.Context) error {
} }
// format the details and retrieve the items from kopia // format the details and retrieve the items from kopia
fds := er.FilterDetails(d) fds := er.Reduce(d)
// todo: use path pkg for this // todo: use path pkg for this
fdsPaths := fds.Paths() fdsPaths := fds.Paths()
paths := make([][]string, len(fdsPaths)) paths := make([][]string, len(fdsPaths))

View File

@ -76,53 +76,63 @@ func (s Selector) ToExchangeRestore() (*ExchangeRestore, error) {
// ------------------- // -------------------
// Exclude/Includes // Exclude/Includes
// Include appends the provided scopes to the selector's inclusion set.
//
// All parts of the scope must match for data to be included.
// Ex: Mail(u1, f1, m1) => only includes mail if it is owned by user u1,
// located in folder f1, and ID'd as m1. Use selectors.Any() to wildcard
// a scope value. No value will match if selectors.None() is provided.
//
// Group-level scopes will automatically apply the Any() wildcard to child
// properties.
// ex: User(u1) is the same as Mail(u1, Any(), Any()).
func (s *exchange) Include(scopes ...[]ExchangeScope) {
if s.Includes == nil {
s.Includes = []map[string]string{}
}
concat := []ExchangeScope{}
for _, scopeSl := range scopes {
concat = append(concat, extendExchangeScopeValues(scopeSl)...)
}
for _, sc := range concat {
s.Includes = append(s.Includes, map[string]string(sc))
}
}
// Exclude appends the provided scopes to the selector's exclusion set. // Exclude appends the provided scopes to the selector's exclusion set.
// Every Exclusion scope applies globally, affecting all inclusion scopes. // Every Exclusion scope applies globally, affecting all inclusion scopes.
// //
// All parts of the scope must match for data to be excluded. // All parts of the scope must match for data to be exclucded.
// Ex: Mail(u1, f1, m1) => only excludes mail that is owned by user u1, // Ex: Mail(u1, f1, m1) => only excludes mail if it is owned by user u1,
// located in folder f1, and ID'd as m1. Use selectors.Any() to wildcard // located in folder f1, and ID'd as m1. MailSender(foo) => only excludes
// a scope value. No value will match if selectors.None() is provided. // mail whose sender is foo. Use selectors.Any() to wildcard a scope value.
// No value will match if selectors.None() is provided.
// //
// Group-level scopes will automatically apply the Any() wildcard to // Group-level scopes will automatically apply the Any() wildcard to
// child properties. // child properties.
// ex: User(u1) automatically includes all mail, events, and contacts, // ex: User(u1) automatically cascades to all mail, events, and contacts,
// therefore it is the same as selecting all of the following: // therefore it is the same as selecting all of the following:
// Mail(u1, Any(), Any()), Event(u1, Any()), Contacts(u1, Any(), Any()) // Mail(u1, Any(), Any()), Event(u1, Any()), Contacts(u1, Any(), Any())
func (s *exchange) Exclude(scopes ...[]ExchangeScope) { func (s *exchange) Exclude(scopes ...[]ExchangeScope) {
if s.Excludes == nil { appendExcludes(&s.Selector, extendExchangeScopeValues, scopes...)
s.Excludes = []map[string]string{}
} }
concat := []ExchangeScope{}
for _, scopeSl := range scopes { // Filter appends the provided scopes to the selector's filters set.
concat = append(concat, extendExchangeScopeValues(scopeSl)...) // A selector with >0 filters and 0 inclusions will include any data
} // that passes all filters.
for _, sc := range concat { // A selector with >0 filters and >0 inclusions will reduce the
s.Excludes = append(s.Excludes, map[string]string(sc)) // inclusion set to only the data that passes all filters.
//
// All parts of the scope must match for data to pass the filter.
// Ex: Mail(u1, f1, m1) => only passes mail that is owned by user u1,
// located in folder f1, and ID'd as m1. MailSender(foo) => only passes
// mail whose sender is foo. Use selectors.Any() to wildcard a scope value.
// No value will match if selectors.None() is provided.
//
// Group-level scopes will automatically apply the Any() wildcard to
// child properties.
// ex: User(u1) automatically cascades to all mail, events, and contacts,
// therefore it is the same as selecting all of the following:
// Mail(u1, Any(), Any()), Event(u1, Any()), Contacts(u1, Any(), Any())
func (s *exchange) Filter(scopes ...[]ExchangeScope) {
appendFilters(&s.Selector, extendExchangeScopeValues, scopes...)
} }
// Include appends the provided scopes to the selector's inclusion set.
// Data is included if it matches ANY inclusion.
// The inclusion set is later filtered (all included data must pass ALL
// filters) and excluded (all included data must not match ANY exclusion).
//
// All parts of the scope must match for data to be included.
// Ex: Mail(u1, f1, m1) => only includes mail if it is owned by user u1,
// located in folder f1, and ID'd as m1. MailSender(foo) => only includes
// mail whose sender is foo. Use selectors.Any() to wildcard a scope value.
// No value will match if selectors.None() is provided.
//
// Group-level scopes will automatically apply the Any() wildcard to
// child properties.
// ex: User(u1) automatically cascades to all mail, events, and contacts,
// therefore it is the same as selecting all of the following:
// Mail(u1, Any(), Any()), Event(u1, Any()), Contacts(u1, Any(), Any())
func (s *exchange) Include(scopes ...[]ExchangeScope) {
appendIncludes(&s.Selector, extendExchangeScopeValues, scopes...)
} }
// completes population for certain scope properties, according to the // completes population for certain scope properties, according to the
@ -282,9 +292,8 @@ func makeExchangeFilterScope(cat, filterCat exchangeCategory, vs []string) Excha
} }
} }
// Produces one or more exchange contact info filter scopes. // Produces one or more exchange mail received-after filter scopes.
// Matches any mail which was received after the timestring. // Matches any mail which was received after the timestring.
// One scope is created per timeString entry.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
@ -294,9 +303,8 @@ func (sr *ExchangeRestore) MailReceivedAfter(timeStrings []string) []ExchangeSco
} }
} }
// Produces one or more exchange mail subject filter scopes. // Produces one or more exchange mail received-before filter scopes.
// Matches any mail whose mail subject contains one of the provided strings. // Matches any mail whose mail subject contains one of the provided strings.
// One scope is created per subject entry.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
@ -306,9 +314,8 @@ func (sr *ExchangeRestore) MailReceivedBefore(timeStrings []string) []ExchangeSc
} }
} }
// Produces one or more exchange mail received-after filter scopes. // Produces one or more exchange mail sender filter scopes.
// Matches any mail which was received after the timestring. // Matches any mail which was received after the timestring.
// One scope is created per timeString entry.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None] // If any slice is empty, it defaults to [selectors.None]
@ -318,7 +325,7 @@ func (sr *ExchangeRestore) MailSender(senderIDs []string) []ExchangeScope {
} }
} }
// Produces one or more exchange mail received-before filter scopes. // Produces one or more exchange mail subject line filter scopes.
// Matches any mail which was received before the timestring. // Matches any mail which was received before the timestring.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice contains selectors.None, that slice is reduced to [selectors.None]
@ -627,15 +634,16 @@ func exchangeIDPath(cat exchangeCategory, path []string) map[exchangeCategory]st
return m return m
} }
// FilterDetails reduces the entries in a backupDetails struct to only // Reduce reduces the entries in a backupDetails struct to only
// those that match the inclusions and exclusions in the selector. // those that match the inclusions, filters, and exclusions in the selector.
func (s *ExchangeRestore) FilterDetails(deets *backup.Details) *backup.Details { func (s *ExchangeRestore) Reduce(deets *backup.Details) *backup.Details {
if deets == nil { if deets == nil {
return nil return nil
} }
entIncs := exchangeScopesByCategory(s.Includes)
entExcs := exchangeScopesByCategory(s.Excludes) entExcs := exchangeScopesByCategory(s.Excludes)
entFilt := exchangeScopesByCategory(s.Filters)
entIncs := exchangeScopesByCategory(s.Includes)
ents := []backup.DetailsEntry{} ents := []backup.DetailsEntry{}
@ -660,8 +668,9 @@ func (s *ExchangeRestore) FilterDetails(deets *backup.Details) *backup.Details {
cat, cat,
path, path,
ent.Exchange, ent.Exchange,
entIncs[cat.String()], entExcs[cat.String()],
entExcs[cat.String()]) entFilt[cat.String()],
entIncs[cat.String()])
if matched { if matched {
ents = append(ents, ent) ents = append(ents, ent)
} }
@ -695,13 +704,22 @@ func exchangeScopesByCategory(scopes []map[string]string) map[string][]ExchangeS
} }
// compare each path to the included and excluded exchange scopes. Returns true // compare each path to the included and excluded exchange scopes. Returns true
// if the path is included, and not excluded. // if the path is included, passes filters, and not excluded.
func matchExchangeEntry( func matchExchangeEntry(
cat exchangeCategory, cat exchangeCategory,
path []string, path []string,
info *backup.ExchangeInfo, info *backup.ExchangeInfo,
incs, excs []ExchangeScope, excs, filts, incs []ExchangeScope,
) bool { ) bool {
// a passing match requires either a filter or an inclusion
if len(incs)+len(filts) == 0 {
return false
}
// skip this check if 0 inclusions were populated
// since filters act as the inclusion check in that case
if len(incs) > 0 {
// at least one inclusion must apply.
var included bool var included bool
for _, inc := range incs { for _, inc := range incs {
if inc.matches(cat, path, info) { if inc.matches(cat, path, info) {
@ -712,14 +730,21 @@ func matchExchangeEntry(
if !included { if !included {
return false return false
} }
}
var excluded bool // all filters must pass
for _, filt := range filts {
if !filt.matches(cat, path, info) {
return false
}
}
// any matching exclusion means failure
for _, exc := range excs { for _, exc := range excs {
if exc.matches(cat, path, info) { if exc.matches(cat, path, info) {
excluded = true return false
break
} }
} }
return !excluded return true
} }

View File

@ -488,7 +488,7 @@ func (suite *ExchangeSourceSuite) TestExchangeScope_Get() {
} }
} }
func (suite *ExchangeSourceSuite) TestExchangeScope_Include_MatchesInfo() { func (suite *ExchangeSourceSuite) TestExchangeScope_MatchesInfo() {
es := NewExchangeRestore() es := NewExchangeRestore()
const ( const (
sender = "smarf@2many.cooks" sender = "smarf@2many.cooks"
@ -628,7 +628,7 @@ func (suite *ExchangeSourceSuite) TestIdPath() {
} }
} }
func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() { func (suite *ExchangeSourceSuite) TestExchangeRestore_Reduce() {
makeDeets := func(refs ...string) *backup.Details { makeDeets := func(refs ...string) *backup.Details {
deets := &backup.Details{ deets := &backup.Details{
DetailsModel: backup.DetailsModel{ DetailsModel: backup.DetailsModel{
@ -773,7 +773,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() {
for _, test := range table { for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {
sel := test.makeSelector() sel := test.makeSelector()
results := sel.FilterDetails(test.deets) results := sel.Reduce(test.deets)
paths := results.Paths() paths := results.Paths()
assert.Equal(t, test.expect, paths) assert.Equal(t, test.expect, paths)
}) })
@ -825,44 +825,48 @@ func (suite *ExchangeSourceSuite) TestExchangeScopesByCategory() {
} }
func (suite *ExchangeSourceSuite) TestMatchExchangeEntry() { func (suite *ExchangeSourceSuite) TestMatchExchangeEntry() {
var TODO_EXCHANGE_INFO *backup.ExchangeInfo var exchangeInfo *backup.ExchangeInfo
const ( const (
mail = "mailID" mid = "mailID"
cat = ExchangeMail cat = ExchangeMail
) )
var ( var (
es = NewExchangeRestore() es = NewExchangeRestore()
inAny = extendExchangeScopeValues(es.Users(Any())) anyUser = extendExchangeScopeValues(es.Users(Any()))
inNone = extendExchangeScopeValues(es.Users(None())) noUser = extendExchangeScopeValues(es.Users(None()))
inMail = extendExchangeScopeValues(es.Mails(Any(), Any(), []string{mail})) mail = extendExchangeScopeValues(es.Mails(Any(), Any(), []string{mid}))
inOtherMail = extendExchangeScopeValues(es.Mails(Any(), Any(), []string{"smarf"})) otherMail = extendExchangeScopeValues(es.Mails(Any(), Any(), []string{"smarf"}))
exAny = extendExchangeScopeValues(es.Users(Any())) noMail = extendExchangeScopeValues(es.Mails(Any(), Any(), None()))
exNone = extendExchangeScopeValues(es.Users(None())) path = []string{"tid", "user", "mail", "folder", mid}
exMail = extendExchangeScopeValues(es.Mails(Any(), Any(), []string{mail}))
exOtherMail = extendExchangeScopeValues(es.Mails(Any(), Any(), []string{"smarf"}))
path = []string{"tid", "user", "mail", "folder", mail}
) )
table := []struct { table := []struct {
name string name string
includes []ExchangeScope excludes, filters, includes []ExchangeScope
excludes []ExchangeScope
expect assert.BoolAssertionFunc expect assert.BoolAssertionFunc
}{ }{
{"empty", nil, nil, assert.False}, {"empty", nil, nil, nil, assert.False},
{"in all", inAny, nil, assert.True}, {"in Any", nil, nil, anyUser, assert.True},
{"in None", inNone, nil, assert.False}, {"in None", nil, nil, noUser, assert.False},
{"in Mail", inMail, nil, assert.True}, {"in Mail", nil, nil, mail, assert.True},
{"in Other", inOtherMail, nil, assert.False}, {"in Other", nil, nil, otherMail, assert.False},
{"ex all", inAny, exAny, assert.False}, {"in no Mail", nil, nil, noMail, assert.False},
{"ex None", inAny, exNone, assert.True}, {"ex Any", anyUser, nil, anyUser, assert.False},
{"in Mail", inAny, exMail, assert.False}, {"ex Any filter", anyUser, anyUser, nil, assert.False},
{"in Other", inAny, exOtherMail, assert.True}, {"ex None", noUser, nil, anyUser, assert.True},
{"in and ex Mail", inMail, exMail, assert.False}, {"ex None filter mail", noUser, mail, nil, assert.True},
{"ex None filter any user", noUser, anyUser, nil, assert.False},
{"ex Mail", mail, nil, anyUser, assert.False},
{"ex Other", otherMail, nil, anyUser, assert.True},
{"in and ex Mail", mail, nil, mail, assert.False},
{"filter Any", nil, anyUser, nil, assert.False},
{"filter None", nil, noUser, anyUser, assert.False},
{"filter Mail", nil, mail, anyUser, assert.True},
{"filter Other", nil, otherMail, anyUser, assert.False},
} }
for _, test := range table { for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) { suite.T().Run(test.name, func(t *testing.T) {
test.expect(t, matchExchangeEntry(cat, path, TODO_EXCHANGE_INFO, test.includes, test.excludes)) test.expect(t, matchExchangeEntry(cat, path, exchangeInfo, test.excludes, test.filters, test.includes))
}) })
} }
} }

View File

@ -55,8 +55,9 @@ const (
// Is only used to pass along more specific selector instances. // Is only used to pass along more specific selector instances.
type Selector struct { type Selector struct {
Service service `json:"service,omitempty"` // The service scope of the data. Exchange, Teams, Sharepoint, etc. Service service `json:"service,omitempty"` // The service scope of the data. Exchange, Teams, Sharepoint, etc.
Excludes []map[string]string `json:"exclusions,omitempty"` // A slice of exclusions. Each exclusion applies to all inclusions. Excludes []map[string]string `json:"exclusions,omitempty"` // A slice of exclusion scopes. Exclusions apply globally to all inclusions/filters, with any-match behavior.
Includes []map[string]string `json:"scopes,omitempty"` // A slice of inclusions. Expected to get cast to a service wrapper within each service handler. Filters []map[string]string `json:"filters,omitempty"` // A slice of filter scopes. All inclusions must also match ALL filters.
Includes []map[string]string `json:"scopes,omitempty"` // A slice of inclusion scopes. Comparators must match either one of these, or all filters, to be included.
} }
// helper for specific selector instance constructors. // helper for specific selector instance constructors.
@ -81,6 +82,61 @@ func None() []string {
return []string{NoneTgt} return []string{NoneTgt}
} }
type baseScope interface {
~map[string]string
}
func appendExcludes[T baseScope](
s *Selector,
tform func([]T) []T,
scopes ...[]T,
) {
if s.Excludes == nil {
s.Excludes = []map[string]string{}
}
concat := []T{}
for _, scopeSl := range scopes {
concat = append(concat, tform(scopeSl)...)
}
for _, sc := range concat {
s.Excludes = append(s.Excludes, map[string]string(sc))
}
}
func appendFilters[T baseScope](
s *Selector,
tform func([]T) []T,
scopes ...[]T,
) {
if s.Filters == nil {
s.Filters = []map[string]string{}
}
concat := []T{}
for _, scopeSl := range scopes {
concat = append(concat, tform(scopeSl)...)
}
for _, sc := range concat {
s.Filters = append(s.Filters, map[string]string(sc))
}
}
func appendIncludes[T baseScope](
s *Selector,
tform func([]T) []T,
scopes ...[]T,
) {
if s.Includes == nil {
s.Includes = []map[string]string{}
}
concat := []T{}
for _, scopeSl := range scopes {
concat = append(concat, tform(scopeSl)...)
}
for _, sc := range concat {
s.Includes = append(s.Includes, map[string]string(sc))
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Destination // Destination
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------