From 6224a92e7a510b09e4e4683fd996cd5c0efb61be Mon Sep 17 00:00:00 2001 From: Keepers <104464746+ryanfkeepers@users.noreply.github.com> Date: Wed, 20 Jul 2022 16:25:28 -0600 Subject: [PATCH] filter backup details by flags (#371) * filter backup details by flags `backup details` should have its output filtered by the flags provided by the user. In addition, the selector's FilterDetails should maintain information (esp service info) about the entries, rather than slicing them down to only the path reference. --- src/cli/backup/exchange.go | 30 +++++++++- src/cli/backup/exchange_test.go | 2 +- src/internal/operations/restore.go | 9 ++- src/pkg/backup/backup.go | 10 ++++ src/pkg/backup/backup_test.go | 39 +++++++++++++ src/pkg/selectors/exchange.go | 46 +++++++++++---- src/pkg/selectors/exchange_test.go | 91 +++++++++++++++++++++++------- 7 files changed, 191 insertions(+), 36 deletions(-) diff --git a/src/cli/backup/exchange.go b/src/cli/backup/exchange.go index ac1534c3a..8efba7feb 100644 --- a/src/cli/backup/exchange.go +++ b/src/cli/backup/exchange.go @@ -109,7 +109,8 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error { if utils.HasNoFlagsAndShownHelp(cmd) { return nil } - if err := validateBackupCreateFlags(exchangeAll, user, exchangeData); err != nil { + + if err := validateExchangeBackupCreateFlags(exchangeAll, user, exchangeData); err != nil { return err } @@ -176,7 +177,7 @@ func exchangeBackupCreateSelectors(all bool, users, data []string) selectors.Sel return sel.Selector } -func validateBackupCreateFlags(all bool, users, data []string) error { +func validateExchangeBackupCreateFlags(all bool, users, data []string) error { if len(users) == 0 && !all { return errors.New("requries one or more --user ids, the wildcard --user *, or the --all flag.") } @@ -253,6 +254,22 @@ var exchangeDetailsCmd = &cobra.Command{ func detailsExchangeCmd(cmd *cobra.Command, args []string) error { ctx := cmd.Context() + if utils.HasNoFlagsAndShownHelp(cmd) { + return nil + } + + if err := validateExchangeBackupDetailFlags( + contact, + contactFolder, + email, + emailFolder, + event, + user, + backupID, + ); err != nil { + return err + } + s, acct, err := config.GetStorageAndAccount(true, nil) if err != nil { return err @@ -278,7 +295,14 @@ func detailsExchangeCmd(cmd *cobra.Command, args []string) error { return errors.Wrap(err, "Failed to get backup details in the repository") } - print.Entries(d.Entries) + sel := exchangeBackupDetailSelectors(contact, contactFolder, email, emailFolder, event, user) + erSel, err := sel.ToExchangeRestore() + if err != nil { + return err + } + + ds := erSel.FilterDetails(d) + print.Entries(ds.Entries) return nil } diff --git a/src/cli/backup/exchange_test.go b/src/cli/backup/exchange_test.go index 644effda0..c951b5b45 100644 --- a/src/cli/backup/exchange_test.go +++ b/src/cli/backup/exchange_test.go @@ -93,7 +93,7 @@ func (suite *ExchangeSuite) TestValidateBackupCreateFlags() { } for _, test := range table { suite.T().Run(test.name, func(t *testing.T) { - test.expect(t, validateBackupCreateFlags(test.all, test.user, test.data)) + test.expect(t, validateExchangeBackupCreateFlags(test.all, test.user, test.data)) }) } } diff --git a/src/internal/operations/restore.go b/src/internal/operations/restore.go index 20a053944..9aa568022 100644 --- a/src/internal/operations/restore.go +++ b/src/internal/operations/restore.go @@ -2,6 +2,7 @@ package operations import ( "context" + "strings" "time" "github.com/pkg/errors" @@ -95,7 +96,13 @@ func (op *RestoreOperation) Run(ctx context.Context) error { // format the details and retrieve the items from kopia fds := er.FilterDetails(d) - dcs, err := op.kopia.RestoreMultipleItems(ctx, b.SnapshotID, fds) + // todo: use path pkg for this + fdsPaths := fds.Paths() + paths := make([][]string, len(fdsPaths)) + for i := range fdsPaths { + paths[i] = strings.Split(fdsPaths[i], "/") + } + dcs, err := op.kopia.RestoreMultipleItems(ctx, b.SnapshotID, paths) if err != nil { stats.readErr = errors.Wrap(err, "retrieving service data") return stats.readErr diff --git a/src/pkg/backup/backup.go b/src/pkg/backup/backup.go index d85ae9c23..0bff0e077 100644 --- a/src/pkg/backup/backup.go +++ b/src/pkg/backup/backup.go @@ -77,6 +77,16 @@ type DetailsEntry struct { ItemInfo } +// Paths returns the list of Paths extracted from the Entriess slice. +func (dm DetailsModel) Paths() []string { + ents := dm.Entries + r := make([]string, len(ents)) + for i := range ents { + r[i] = ents[i].RepoRef + } + return r +} + // Headers returns the human-readable names of properties in a DetailsEntry // for printing out to a terminal in a columnar display. func (de DetailsEntry) Headers() []string { diff --git a/src/pkg/backup/backup_test.go b/src/pkg/backup/backup_test.go index 4cdeb32b1..69b5fb0b7 100644 --- a/src/pkg/backup/backup_test.go +++ b/src/pkg/backup/backup_test.go @@ -106,3 +106,42 @@ func (suite *BackupSuite) TestDetailsEntry_HeadersValues() { }) } } + +func (suite *BackupSuite) TestDetailsModel_Path() { + table := []struct { + name string + ents []backup.DetailsEntry + expect []string + }{ + { + name: "nil entries", + ents: nil, + expect: []string{}, + }, + { + name: "single entry", + ents: []backup.DetailsEntry{ + {RepoRef: "abcde"}, + }, + expect: []string{"abcde"}, + }, + { + name: "multiple entries", + ents: []backup.DetailsEntry{ + {RepoRef: "abcde"}, + {RepoRef: "12345"}, + }, + expect: []string{"abcde", "12345"}, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + d := backup.Details{ + DetailsModel: backup.DetailsModel{ + Entries: test.ents, + }, + } + assert.DeepEqual(t, test.expect, d.Paths()) + }) + } +} diff --git a/src/pkg/selectors/exchange.go b/src/pkg/selectors/exchange.go index 8d628ea72..eae1fe28c 100644 --- a/src/pkg/selectors/exchange.go +++ b/src/pkg/selectors/exchange.go @@ -391,7 +391,7 @@ var categoryPathSet = map[exchangeCategory][]exchangeCategory{ // includesPath returns true if all filters in the scope match the path. func (s exchangeScope) includesPath(cat exchangeCategory, path []string) bool { - ids := idPath(cat, path) + ids := exchangeIDPath(cat, path) for _, c := range categoryPathSet[cat] { target := s.Get(c) if len(target) == 0 { @@ -408,9 +408,18 @@ func (s exchangeScope) includesPath(cat exchangeCategory, path []string) bool { return true } +// includesInfo returns true if all filters in the scope match the info. +func (s exchangeScope) includesInfo(cat exchangeCategory, info *backup.ExchangeInfo) bool { + // todo: implement once filters used in scopes + if info == nil { + return false + } + return false +} + // excludesPath returns true if all filters in the scope match the path. func (s exchangeScope) excludesPath(cat exchangeCategory, path []string) bool { - ids := idPath(cat, path) + ids := exchangeIDPath(cat, path) for _, c := range categoryPathSet[cat] { target := s.Get(c) if len(target) == 0 { @@ -427,6 +436,15 @@ func (s exchangeScope) excludesPath(cat exchangeCategory, path []string) bool { return false } +// excludesInfo returns true if all filters in the scope matche the info. +func (s exchangeScope) excludesInfo(cat exchangeCategory, info *backup.ExchangeInfo) bool { + // todo: implement once filters used in scopes + if info == nil { + return false + } + return false +} + // temporary helper until filters replace string values for scopes. func contains(super []string, sub string) bool { for _, s := range super { @@ -446,7 +464,7 @@ func contains(super []string, sub string) bool { // Example: // [tenantID, userID, "mail", mailFolder, mailID] // => {exchUser: userID, exchMailFolder: mailFolder, exchMail: mailID} -func idPath(cat exchangeCategory, path []string) map[exchangeCategory]string { +func exchangeIDPath(cat exchangeCategory, path []string) map[exchangeCategory]string { m := map[exchangeCategory]string{} if len(path) == 0 { return m @@ -484,7 +502,7 @@ func idPath(cat exchangeCategory, path []string) map[exchangeCategory]string { // FilterDetails reduces the entries in a backupDetails struct to only // those that match the inclusions and exclusions in the selector. -func (s *ExchangeRestore) FilterDetails(deets *backup.Details) [][]string { +func (s *ExchangeRestore) FilterDetails(deets *backup.Details) *backup.Details { if deets == nil { return nil } @@ -492,9 +510,10 @@ func (s *ExchangeRestore) FilterDetails(deets *backup.Details) [][]string { entIncs := exchangeScopesByCategory(s.Includes) entExcs := exchangeScopesByCategory(s.Excludes) - refs := [][]string{} + ents := []backup.DetailsEntry{} for _, ent := range deets.Entries { + // todo: use Path pkg for this path := strings.Split(ent.RepoRef, "/") // not all paths will be len=3. Most should be longer. // This just protects us from panicing four lines later. @@ -513,14 +532,16 @@ func (s *ExchangeRestore) FilterDetails(deets *backup.Details) [][]string { matched := matchExchangeEntry( cat, path, + ent.Exchange, entIncs[cat.String()], entExcs[cat.String()]) if matched { - refs = append(refs, path) + ents = append(ents, ent) } } - return refs + deets.Entries = ents + return deets } // groups each scope by its category of data (contact, event, or mail). @@ -548,10 +569,15 @@ func exchangeScopesByCategory(scopes []map[string]string) map[string][]exchangeS // compare each path to the included and excluded exchange scopes. Returns true // if the path is included, and not excluded. -func matchExchangeEntry(cat exchangeCategory, path []string, incs, excs []exchangeScope) bool { +func matchExchangeEntry( + cat exchangeCategory, + path []string, + info *backup.ExchangeInfo, + incs, excs []exchangeScope, +) bool { var included bool for _, inc := range incs { - if inc.includesPath(cat, path) { + if inc.includesPath(cat, path) || inc.includesInfo(cat, info) { included = true break } @@ -562,7 +588,7 @@ func matchExchangeEntry(cat exchangeCategory, path []string, incs, excs []exchan var excluded bool for _, exc := range excs { - if exc.excludesPath(cat, path) { + if exc.excludesPath(cat, path) || exc.excludesInfo(cat, info) { excluded = true break } diff --git a/src/pkg/selectors/exchange_test.go b/src/pkg/selectors/exchange_test.go index d5b838459..d673552f9 100644 --- a/src/pkg/selectors/exchange_test.go +++ b/src/pkg/selectors/exchange_test.go @@ -1,7 +1,6 @@ package selectors import ( - "strings" "testing" "github.com/alcionai/corso/pkg/backup" @@ -475,6 +474,32 @@ func (suite *ExchangeSourceSuite) TestExchangeScope_Get() { } } +func (suite *ExchangeSourceSuite) TestExchangeScope_IncludesInfo() { + const ( + TODO = "this is a placeholder, awaiting implemenation of filters" + ) + var ( + es = NewExchangeRestore() + ) + + table := []struct { + name string + scope []exchangeScope + info *backup.ExchangeInfo + expect assert.BoolAssertionFunc + }{ + {"all user's items", es.Users(All()), nil, assert.False}, // false while a todo + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + scopes := extendExchangeScopeValues(All(), test.scope) + for _, scope := range scopes { + test.expect(t, scope.includesInfo(ExchangeMail, test.info)) + } + }) + } +} + func (suite *ExchangeSourceSuite) TestExchangeScope_IncludesPath() { const ( usr = "userID" @@ -517,6 +542,32 @@ func (suite *ExchangeSourceSuite) TestExchangeScope_IncludesPath() { } } +func (suite *ExchangeSourceSuite) TestExchangeScope_ExcludesInfo() { + const ( + TODO = "this is a placeholder, awaiting implemenation of filters" + ) + var ( + es = NewExchangeRestore() + ) + + table := []struct { + name string + scope []exchangeScope + info *backup.ExchangeInfo + expect assert.BoolAssertionFunc + }{ + {"all user's items", es.Users(All()), nil, assert.False}, // false while a todo + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + scopes := extendExchangeScopeValues(None(), test.scope) + for _, scope := range scopes { + test.expect(t, scope.excludesInfo(ExchangeMail, test.info)) + } + }) + } +} + func (suite *ExchangeSourceSuite) TestExchangeScope_ExcludesPath() { const ( usr = "userID" @@ -623,18 +674,14 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() { event = "tid/uid/event/eid" mail = "tid/uid/mail/mfld/mid" ) - split := func(s ...string) [][]string { - r := [][]string{} - for _, ss := range s { - r = append(r, strings.Split(ss, "/")) - } - return r + arr := func(s ...string) []string { + return s } table := []struct { name string deets *backup.Details makeSelector func() *ExchangeRestore - expect [][]string + expect []string }{ { "no refs", @@ -644,7 +691,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() { er.Include(er.Users(All())) return er }, - [][]string{}, + []string{}, }, { "contact only", @@ -654,7 +701,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() { er.Include(er.Users(All())) return er }, - split(contact), + arr(contact), }, { "event only", @@ -664,7 +711,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() { er.Include(er.Users(All())) return er }, - split(event), + arr(event), }, { "mail only", @@ -674,7 +721,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() { er.Include(er.Users(All())) return er }, - split(mail), + arr(mail), }, { "all", @@ -684,7 +731,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() { er.Include(er.Users(All())) return er }, - split(contact, event, mail), + arr(contact, event, mail), }, { "only match contact", @@ -694,7 +741,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() { er.Include(er.Contacts([]string{"uid"}, []string{"cfld"}, []string{"cid"})) return er }, - split(contact), + arr(contact), }, { "only match event", @@ -704,7 +751,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() { er.Include(er.Events([]string{"uid"}, []string{"eid"})) return er }, - split(event), + arr(event), }, { "only match mail", @@ -714,7 +761,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() { er.Include(er.Mails([]string{"uid"}, []string{"mfld"}, []string{"mid"})) return er }, - split(mail), + arr(mail), }, { "exclude contact", @@ -725,7 +772,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() { er.Exclude(er.Contacts([]string{"uid"}, []string{"cfld"}, []string{"cid"})) return er }, - split(event, mail), + arr(event, mail), }, { "exclude event", @@ -736,7 +783,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() { er.Exclude(er.Events([]string{"uid"}, []string{"eid"})) return er }, - split(contact, mail), + arr(contact, mail), }, { "exclude mail", @@ -747,14 +794,15 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_FilterDetails() { er.Exclude(er.Mails([]string{"uid"}, []string{"mfld"}, []string{"mid"})) return er }, - split(contact, event), + arr(contact, event), }, } for _, test := range table { suite.T().Run(test.name, func(t *testing.T) { sel := test.makeSelector() results := sel.FilterDetails(test.deets) - assert.Equal(t, test.expect, results) + paths := results.Paths() + assert.Equal(t, test.expect, paths) }) } } @@ -804,6 +852,7 @@ func (suite *ExchangeSourceSuite) TestExchangeScopesByCategory() { } func (suite *ExchangeSourceSuite) TestMatchExchangeEntry() { + var TODO_EXCHANGE_INFO *backup.ExchangeInfo const ( mail = "mailID" cat = ExchangeMail @@ -846,7 +895,7 @@ func (suite *ExchangeSourceSuite) TestMatchExchangeEntry() { } for _, test := range table { suite.T().Run(test.name, func(t *testing.T) { - test.expect(t, matchExchangeEntry(cat, path, test.includes, test.excludes)) + test.expect(t, matchExchangeEntry(cat, path, TODO_EXCHANGE_INFO, test.includes, test.excludes)) }) } }