diff --git a/src/cli/backup/exchange.go b/src/cli/backup/exchange.go index f16934718..32436f980 100644 --- a/src/cli/backup/exchange.go +++ b/src/cli/backup/exchange.go @@ -440,7 +440,6 @@ func validateExchangeBackupDetailFlags( lc, lcf := len(contacts), len(contactFolders) le, lef := len(emails), len(emailFolders) lev := len(events) - // if only the backupID is populated, that's the same as --all if lu+lc+lcf+le+lef+lev == 0 { return nil } diff --git a/src/cli/restore/exchange.go b/src/cli/restore/exchange.go index 6df9f7e0f..01d959ff7 100644 --- a/src/cli/restore/exchange.go +++ b/src/cli/restore/exchange.go @@ -17,10 +17,17 @@ import ( // exchange bucket info from flags var ( - emailFolder string - email string - backupID string - user string + backupID string + contact []string + contactFolder []string + email []string + emailFolder []string + emailReceivedAfter []string + emailReceivedBefore []string + emailSender []string + emailSubject []string + event []string + user []string ) // called by restore.go to map parent subcommands to provider-specific handling. @@ -33,11 +40,37 @@ func addExchangeCommands(parent *cobra.Command) *cobra.Command { switch parent.Use { case restoreCommand: c, fs = utils.AddCommand(parent, exchangeRestoreCmd) - fs.StringVar(&emailFolder, "email-folder", "", "Name of the email folder being restored") - fs.StringVar(&email, "email", "", "ID of the email being restored") fs.StringVar(&backupID, "backup", "", "ID of the backup to restore") cobra.CheckErr(c.MarkFlagRequired("backup")) - fs.StringVar(&user, "user", "", "ID of the user whose exchange data will get restored") + + // per-data-type flags + fs.StringArrayVar(&contact, "contact", nil, "Restore contacts by ID; accepts "+utils.Wildcard+" to select all contacts") + fs.StringArrayVar( + &contactFolder, + "contact-folder", + nil, + "Restore all contacts within the folder ID; accepts "+utils.Wildcard+" to select all contact folders") + fs.StringArrayVar(&email, "email", nil, "Restore emails by ID; accepts "+utils.Wildcard+" to select all emails") + fs.StringArrayVar( + &emailFolder, + "email-folder", + nil, + "Restore all emails by folder ID; accepts "+utils.Wildcard+" to select all email folders") + fs.StringArrayVar(&event, "event", nil, "Restore events by ID; accepts "+utils.Wildcard+" to select all events") + fs.StringArrayVar(&user, "user", nil, "Restore all data by user ID; accepts "+utils.Wildcard+" to select all users") + + // TODO: reveal these flags when their production is supported in GC + cobra.CheckErr(fs.MarkHidden("contact")) + cobra.CheckErr(fs.MarkHidden("contact-folder")) + cobra.CheckErr(fs.MarkHidden("event")) + + // exchange-info flags + fs.StringArrayVar(&emailReceivedAfter, "email-received-after", nil, "Restore mail where the email was received after this datetime") + fs.StringArrayVar(&emailReceivedBefore, "email-received-before", nil, "Restore mail where the email was received before this datetime") + fs.StringArrayVar(&emailSender, "email-sender", nil, "Restore mail where the email sender matches this user id") + fs.StringArrayVar(&emailSubject, "email-subject", nil, "Restore mail where the email subject lines contain this value") + + // others options.AddOperationFlags(c) } return c @@ -61,8 +94,16 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error { return nil } - if err := validateRestoreFlags(user, emailFolder, email, backupID); err != nil { - return errors.Wrap(err, "Missing required flags") + if err := validateExchangeRestoreFlags( + contact, + contactFolder, + email, + emailFolder, + event, + user, + backupID, + ); err != nil { + return err } s, a, err := config.GetStorageAndAccount(true, nil) @@ -88,7 +129,28 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error { } defer utils.CloseRepo(ctx, r) - ro, err := r.NewRestore(ctx, backupID, exchangeRestoreSelectors(user, emailFolder, email), options.OperationOptions()) + sel := selectors.NewExchangeRestore() + includeExchangeRestoreDataSelectors( + sel, + contact, + contactFolder, + email, + emailFolder, + event, + user) + includeExchangeRestoreInfoSelectors( + sel, + emailReceivedAfter, + emailReceivedBefore, + emailSender, + emailSubject) + + // if no selector flags were specified, get all data in the service. + if len(sel.Scopes()) == 0 { + sel.Include(sel.Users(selectors.Any())) + } + + ro, err := r.NewRestore(ctx, backupID, sel.Selector, options.OperationOptions()) if err != nil { return errors.Wrap(err, "Failed to initialize Exchange restore") } @@ -101,34 +163,124 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error { return nil } -func exchangeRestoreSelectors(u, f, m string) selectors.Selector { - sel := selectors.NewExchangeRestore() - if len(m) > 0 { - sel.Include(sel.Mails( - []string{u}, []string{f}, []string{m}, - )) +// builds the data-selector inclusions for `restore exchange` +func includeExchangeRestoreDataSelectors( + sel *selectors.ExchangeRestore, + contacts, contactFolders, emails, emailFolders, events, users []string, +) { + lc, lcf := len(contacts), len(contactFolders) + le, lef := len(emails), len(emailFolders) + lev := len(events) + lu := len(users) + + if lc+lcf+le+lef+lev+lu == 0 { + return } - if len(f) > 0 && len(m) == 0 { - sel.Include(sel.MailFolders( - []string{u}, []string{f}, - )) + + // if only users are provided, we only get one selector + if lu > 0 && lc+lcf+le+lef+lev == 0 { + sel.Include(sel.Users(users)) + return } - if len(f) == 0 && len(m) == 0 { - sel.Include(sel.Users([]string{u})) - } - return sel.Selector + + // otherwise, add selectors for each type of data + includeExchangeContacts(sel, users, contactFolders, contacts) + includeExchangeEmails(sel, users, emailFolders, email) + includeExchangeEvents(sel, users, events) } -func validateRestoreFlags(u, f, m, rpid string) error { - if len(rpid) == 0 { - return errors.New("a restore point ID is required") +func includeExchangeContacts(sel *selectors.ExchangeRestore, users, contactFolders, contacts []string) { + if len(contactFolders) == 0 { + return } - lu, lf, lm := len(u), len(f), len(m) - if (lu == 0 || u == "*") && (lf+lm > 0) { - return errors.New("a specific --user must be provided if --email-folder or --email is specified") + if len(contacts) > 0 { + sel.Include(sel.Contacts(users, contactFolders, contacts)) + } else { + sel.Include(sel.ContactFolders(users, contactFolders)) } - if (lf == 0 || f == "*") && lm > 0 { - return errors.New("a specific --email-folder must be provided if a --email is specified") +} + +func includeExchangeEmails(sel *selectors.ExchangeRestore, users, emailFolders, emails []string) { + if len(emailFolders) == 0 { + return + } + if len(emails) > 0 { + sel.Include(sel.Mails(users, emailFolders, emails)) + } else { + sel.Include(sel.MailFolders(users, emailFolders)) + } +} + +func includeExchangeEvents(sel *selectors.ExchangeRestore, users, events []string) { + if len(events) == 0 { + return + } + sel.Include(sel.Events(users, events)) +} + +// builds the info-selector inclusions for `restore exchange` +func includeExchangeRestoreInfoSelectors( + sel *selectors.ExchangeRestore, + emailReceivedAfter, emailReceivedBefore, emailSender, emailSubject []string, +) { + includeExchangeInfoMailReceivedAfter(sel, emailReceivedAfter) + includeExchangeInfoMailReceivedBefore(sel, emailReceivedBefore) + includeExchangeInfoMailSender(sel, emailSender) + includeExchangeInfoMailSubject(sel, emailSubject) +} + +func includeExchangeInfoMailReceivedAfter(sel *selectors.ExchangeRestore, receivedAfter []string) { + if len(receivedAfter) == 0 { + return + } + sel.Include(sel.MailReceivedAfter(receivedAfter)) +} + +func includeExchangeInfoMailReceivedBefore(sel *selectors.ExchangeRestore, receivedBefore []string) { + if len(receivedBefore) == 0 { + return + } + sel.Include(sel.MailReceivedBefore(receivedBefore)) +} + +func includeExchangeInfoMailSender(sel *selectors.ExchangeRestore, sender []string) { + if len(sender) == 0 { + return + } + sel.Include(sel.MailSender(sender)) +} + +func includeExchangeInfoMailSubject(sel *selectors.ExchangeRestore, subject []string) { + if len(subject) == 0 { + return + } + sel.Include(sel.MailSubject(subject)) +} + +// checks all flags for correctness and interdependencies +func validateExchangeRestoreFlags( + contacts, contactFolders, emails, emailFolders, events, users []string, + backupID string, +) error { + if len(backupID) == 0 { + return errors.New("a backup ID is required") + } + lu := len(users) + lc, lcf := len(contacts), len(contactFolders) + le, lef := len(emails), len(emailFolders) + lev := len(events) + // if only the backupID is populated, that's the same as --all + if lu+lc+lcf+le+lef+lev == 0 { + return nil + } + if lu == 0 { + return errors.New("requires one or more --user ids, the wildcard --user *, or the --all flag.") + } + if lc > 0 && lcf == 0 { + return errors.New("one or more --contact-folder ids or the wildcard --contact-folder * must be included to specify a --contact") + } + if le > 0 && lef == 0 { + return errors.New("one or more --email-folder ids or the wildcard --email-folder * must be included to specify a --email") } return nil } diff --git a/src/cli/restore/exchange_test.go b/src/cli/restore/exchange_test.go index 31bb29b70..e646d202c 100644 --- a/src/cli/restore/exchange_test.go +++ b/src/cli/restore/exchange_test.go @@ -10,6 +10,7 @@ import ( "github.com/alcionai/corso/cli/utils" ctesting "github.com/alcionai/corso/internal/testing" + "github.com/alcionai/corso/pkg/selectors" ) type ExchangeSuite struct { @@ -20,31 +21,6 @@ func TestExchangeSuite(t *testing.T) { suite.Run(t, new(ExchangeSuite)) } -func (suite *ExchangeSuite) TestValidateRestoreFlags() { - table := []struct { - name string - u, f, m, rpid string - errCheck assert.ErrorAssertionFunc - }{ - {"all populated", "u", "f", "m", "rpid", assert.NoError}, - {"folder missing user", "", "f", "m", "rpid", assert.Error}, - {"folder with wildcard user", utils.Wildcard, "f", "m", "rpid", assert.Error}, - {"mail missing user", "", "", "m", "rpid", assert.Error}, - {"mail missing folder", "u", "", "m", "rpid", assert.Error}, - {"mail with wildcard folder", "u", utils.Wildcard, "m", "rpid", assert.Error}, - {"missing backup id", "u", "f", "m", "", assert.Error}, - {"all missing", "", "", "", "rpid", assert.NoError}, - } - for _, test := range table { - suite.T().Run(test.name, func(t *testing.T) { - test.errCheck( - t, - validateRestoreFlags(test.u, test.f, test.m, test.rpid), - ) - }) - } -} - func (suite *ExchangeSuite) TestAddExchangeCommands() { expectUse := exchangeServiceCommand table := []struct { @@ -73,3 +49,365 @@ func (suite *ExchangeSuite) TestAddExchangeCommands() { }) } } + +func (suite *ExchangeSuite) TestValidateExchangeRestoreFlags() { + stub := []string{"id-stub"} + table := []struct { + name string + contacts, contactFolders, emails, emailFolders, events, users []string + backupID string + expect assert.ErrorAssertionFunc + }{ + { + name: "only backupid", + backupID: "bid", + expect: assert.NoError, + }, + { + name: "any values populated", + backupID: "bid", + contacts: stub, + contactFolders: stub, + emails: stub, + emailFolders: stub, + events: stub, + users: stub, + expect: assert.NoError, + }, + { + name: "nothing populated", + expect: assert.Error, + }, + { + name: "no backup id", + contacts: stub, + contactFolders: stub, + emails: stub, + emailFolders: stub, + events: stub, + users: stub, + expect: assert.Error, + }, + { + name: "no users", + backupID: "bid", + contacts: stub, + contactFolders: stub, + emails: stub, + emailFolders: stub, + events: stub, + expect: assert.Error, + }, + { + name: "no contact folders", + backupID: "bid", + contacts: stub, + emails: stub, + emailFolders: stub, + events: stub, + users: stub, + expect: assert.Error, + }, + { + name: "no email folders", + backupID: "bid", + contacts: stub, + contactFolders: stub, + emails: stub, + events: stub, + users: stub, + expect: assert.Error, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + test.expect(t, validateExchangeRestoreFlags( + test.contacts, + test.contactFolders, + test.emails, + test.emailFolders, + test.events, + test.users, + test.backupID, + )) + }) + } +} + +func (suite *ExchangeSuite) TestIncludeExchangeRestoreDataSelectors() { + stub := []string{"id-stub"} + any := []string{utils.Wildcard} + table := []struct { + name string + contacts, contactFolders, emails, emailFolders, events, users []string + expectIncludeLen int + }{ + { + name: "no selectors", + expectIncludeLen: 0, + }, + { + name: "any users", + users: any, + expectIncludeLen: 1, + }, + { + name: "single user", + users: stub, + expectIncludeLen: 1, + }, + { + name: "multiple users", + users: []string{"fnord", "smarf"}, + expectIncludeLen: 1, + }, + { + name: "any users, any data", + contacts: any, + contactFolders: any, + emails: any, + emailFolders: any, + events: any, + users: any, + expectIncludeLen: 3, + }, + { + name: "any users, any folders", + contactFolders: any, + emailFolders: any, + users: any, + expectIncludeLen: 2, + }, + { + name: "single user, single of each data", + contacts: stub, + contactFolders: stub, + emails: stub, + emailFolders: stub, + events: stub, + users: stub, + expectIncludeLen: 3, + }, + { + name: "single user, single of each folder", + contactFolders: stub, + emailFolders: stub, + users: stub, + expectIncludeLen: 2, + }, + { + name: "any users, contacts", + contacts: any, + contactFolders: stub, + users: any, + expectIncludeLen: 1, + }, + { + name: "single user, contacts", + contacts: stub, + contactFolders: stub, + users: stub, + expectIncludeLen: 1, + }, + { + name: "any users, emails", + emails: any, + emailFolders: stub, + users: any, + expectIncludeLen: 1, + }, + { + name: "single user, emails", + emails: stub, + emailFolders: stub, + users: stub, + expectIncludeLen: 1, + }, + { + name: "any users, events", + events: any, + users: any, + expectIncludeLen: 1, + }, + { + name: "single user, events", + events: stub, + users: stub, + expectIncludeLen: 1, + }, + { + name: "any users, contacts + email", + contacts: any, + contactFolders: any, + emails: any, + emailFolders: any, + users: any, + expectIncludeLen: 2, + }, + { + name: "single users, contacts + email", + contacts: stub, + contactFolders: stub, + emails: stub, + emailFolders: stub, + users: stub, + expectIncludeLen: 2, + }, + { + name: "any users, email + event", + emails: any, + emailFolders: any, + events: any, + users: any, + expectIncludeLen: 2, + }, + { + name: "single users, email + event", + emails: stub, + emailFolders: stub, + events: stub, + users: stub, + expectIncludeLen: 2, + }, + { + name: "any users, event + contact", + contacts: any, + contactFolders: any, + events: any, + users: any, + expectIncludeLen: 2, + }, + { + name: "single users, event + contact", + contacts: stub, + contactFolders: stub, + events: stub, + users: stub, + expectIncludeLen: 2, + }, + { + name: "many users, events", + events: []string{"foo", "bar"}, + users: []string{"fnord", "smarf"}, + expectIncludeLen: 2, + }, + { + name: "many users, events + contacts", + contacts: []string{"foo", "bar"}, + contactFolders: []string{"foo", "bar"}, + events: []string{"foo", "bar"}, + users: []string{"fnord", "smarf"}, + expectIncludeLen: 6, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + sel := selectors.NewExchangeRestore() + includeExchangeRestoreDataSelectors( + sel, + test.contacts, + test.contactFolders, + test.emails, + test.emailFolders, + test.events, + test.users) + assert.Equal(t, test.expectIncludeLen, len(sel.Includes)) + }) + } +} + +func (suite *ExchangeSuite) TestIncludeExchangeRestoreInfoSelectors() { + stub := []string{"id-stub"} + twoStubs := []string{"a-stub", "b-stub"} + any := []string{utils.Wildcard} + table := []struct { + name string + after, before, sender, subject []string + expectIncludeLen int + }{ + { + name: "no selectors", + expectIncludeLen: 0, + }, + { + name: "any receivedAfter", + after: any, + expectIncludeLen: 1, + }, + { + name: "single receivedAfter", + after: stub, + expectIncludeLen: 1, + }, + { + name: "multiple receivedAfter", + after: twoStubs, + expectIncludeLen: 1, + }, + { + name: "any receivedBefore", + before: any, + expectIncludeLen: 1, + }, + { + name: "single receivedBefore", + before: stub, + expectIncludeLen: 1, + }, + { + name: "multiple receivedBefore", + before: twoStubs, + expectIncludeLen: 1, + }, + { + name: "any senders", + sender: any, + expectIncludeLen: 1, + }, + { + name: "single sender", + sender: stub, + expectIncludeLen: 1, + }, + { + name: "multiple senders", + sender: twoStubs, + expectIncludeLen: 1, + }, + { + name: "any subjects", + subject: any, + expectIncludeLen: 1, + }, + { + name: "single subject", + subject: stub, + expectIncludeLen: 1, + }, + { + name: "multiple subjects", + subject: twoStubs, + expectIncludeLen: 1, + }, + { + name: "one of each", + after: stub, + before: stub, + sender: stub, + subject: stub, + expectIncludeLen: 4, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + sel := selectors.NewExchangeRestore() + includeExchangeRestoreInfoSelectors( + sel, + test.after, + test.before, + test.sender, + test.subject) + assert.Equal(t, test.expectIncludeLen, len(sel.Includes)) + }) + } +} diff --git a/src/pkg/selectors/exchange.go b/src/pkg/selectors/exchange.go index b16651eea..76ce30666 100644 --- a/src/pkg/selectors/exchange.go +++ b/src/pkg/selectors/exchange.go @@ -571,15 +571,6 @@ func (s exchangeScope) matchesPath(cat exchangeCategory, path []string) bool { return true } -// 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 {