diff --git a/src/cli/backup/exchange.go b/src/cli/backup/exchange.go index d944ea23f..ed9e4b20d 100644 --- a/src/cli/backup/exchange.go +++ b/src/cli/backup/exchange.go @@ -15,8 +15,16 @@ import ( // exchange bucket info from flags var ( - user string backupDetailsID string + exchangeAll bool + exchangeData []string + user []string +) + +const ( + dataContacts = "contacts" + dataEmail = "email" + dataEvents = "events" ) // called by backup.go to map parent subcommands to provider-specific handling. @@ -28,12 +36,18 @@ func addExchangeCommands(parent *cobra.Command) *cobra.Command { switch parent.Use { case createCommand: c, fs = utils.AddCommand(parent, exchangeCreateCmd) - fs.StringVar(&user, "user", "", "ID of the user whose Exchange data is to be backed up.") + fs.StringArrayVar(&user, "user", nil, "Back up Exchange data by user ID; accepts "+utils.Wildcard+" to select all users") + fs.BoolVar(&exchangeAll, "all", false, "Back up all Exchange data for all users") + fs.StringArrayVar( + &exchangeData, + "data", + nil, + "Select one or more types of data to backup: "+dataEmail+", "+dataContacts+", or "+dataEvents) case listCommand: c, _ = utils.AddCommand(parent, exchangeListCmd) case detailsCommand: c, fs = utils.AddCommand(parent, exchangeDetailsCmd) - fs.StringVar(&backupDetailsID, "backup-details", "", "ID of the backup details to be shown.") + fs.StringVar(&backupDetailsID, "backup-details", "", "ID of the backup details to be shown") cobra.CheckErr(c.MarkFlagRequired("backup-details")) } return c @@ -56,6 +70,9 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error { if utils.HasNoFlagsAndShownHelp(cmd) { return nil } + if err := validateBackupCreateFlags(exchangeAll, user, exchangeData); err != nil { + return err + } s, acct, err := config.GetStorageAndAccount(true, nil) if err != nil { @@ -79,10 +96,9 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error { } defer utils.CloseRepo(ctx, r) - sel := selectors.NewExchangeBackup() - sel.Include(sel.Users(user)) + sel := exchangeBackupCreateSelectors(exchangeAll, user, exchangeData) - bo, err := r.NewBackup(ctx, sel.Selector) + bo, err := r.NewBackup(ctx, sel) if err != nil { return errors.Wrap(err, "Failed to initialize Exchange backup") } @@ -97,6 +113,65 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error { return nil } +func exchangeBackupCreateSelectors(all bool, users, data []string) selectors.Selector { + sel := selectors.NewExchangeBackup() + if all { + sel.Include(sel.Users(selectors.All)) + return sel.Selector + } + if len(data) == 0 { + for _, user := range users { + if user == utils.Wildcard { + user = selectors.All + } + sel.Include(sel.ContactFolders(user, selectors.All)) + sel.Include(sel.MailFolders(user, selectors.All)) + sel.Include(sel.Events(user, selectors.All)) + } + } + for _, d := range data { + switch d { + case dataContacts: + for _, user := range users { + if user == utils.Wildcard { + user = selectors.All + } + sel.Include(sel.ContactFolders(user, selectors.All)) + } + case dataEmail: + for _, user := range users { + if user == utils.Wildcard { + user = selectors.All + } + sel.Include(sel.MailFolders(user, selectors.All)) + } + case dataEvents: + for _, user := range users { + if user == utils.Wildcard { + user = selectors.All + } + sel.Include(sel.Events(user, selectors.All)) + } + } + } + return sel.Selector +} + +func validateBackupCreateFlags(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.") + } + if len(data) > 0 && all { + return errors.New("--all backs up all data, and cannot be reduced with --data") + } + for _, d := range data { + if d != dataContacts && d != dataEmail && d != dataEvents { + return errors.New(d + " is an unrecognized data type; must be one of " + dataContacts + ", " + dataEmail + ", or " + dataEvents) + } + } + return nil +} + // `corso backup list exchange [...]` var exchangeListCmd = &cobra.Command{ Use: exchangeServiceCommand, diff --git a/src/cli/backup/exchange_test.go b/src/cli/backup/exchange_test.go index 8cacad8ee..cbbc9b5e6 100644 --- a/src/cli/backup/exchange_test.go +++ b/src/cli/backup/exchange_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/alcionai/corso/cli/utils" ctesting "github.com/alcionai/corso/internal/testing" ) @@ -49,3 +50,165 @@ func (suite *ExchangeSuite) TestAddExchangeCommands() { }) } } + +func (suite *ExchangeSuite) TestValidateBackupCreateFlags() { + table := []struct { + name string + all bool + user, data []string + expect assert.ErrorAssertionFunc + }{ + { + name: "no users, not all", + expect: assert.Error, + }, + { + name: "all and data", + all: true, + data: []string{dataEmail}, + expect: assert.Error, + }, + { + name: "unrecognized data", + user: []string{"fnord"}, + data: []string{"smurfs"}, + expect: assert.Error, + }, + { + name: "users, not all", + user: []string{"fnord"}, + expect: assert.NoError, + }, + { + name: "no users, all", + all: true, + expect: assert.NoError, + }, + { + name: "users, all", + all: true, + user: []string{"fnord"}, + expect: assert.NoError, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + test.expect(t, validateBackupCreateFlags(test.all, test.user, test.data)) + }) + } +} + +func (suite *ExchangeSuite) TestExchangeBackupCreateSelectors() { + table := []struct { + name string + all bool + user, data []string + expectIncludeLen int + }{ + { + name: "all", + all: true, + expectIncludeLen: 1, + }, + { + name: "all users, no data", + user: []string{utils.Wildcard}, + expectIncludeLen: 3, + }, + { + name: "single user, no data", + user: []string{"u1"}, + expectIncludeLen: 3, + }, + { + name: "all users, contacts", + user: []string{utils.Wildcard}, + data: []string{dataContacts}, + expectIncludeLen: 1, + }, + { + name: "single user, contacts", + user: []string{"u1"}, + data: []string{dataContacts}, + expectIncludeLen: 1, + }, + { + name: "all users, email", + user: []string{utils.Wildcard}, + data: []string{dataEmail}, + expectIncludeLen: 1, + }, + { + name: "single user, email", + user: []string{"u1"}, + data: []string{dataEmail}, + expectIncludeLen: 1, + }, + { + name: "all users, events", + user: []string{utils.Wildcard}, + data: []string{dataEvents}, + expectIncludeLen: 1, + }, + { + name: "single user, events", + user: []string{"u1"}, + data: []string{dataEvents}, + expectIncludeLen: 1, + }, + { + name: "all users, contacts + email", + user: []string{utils.Wildcard}, + data: []string{dataContacts, dataEmail}, + expectIncludeLen: 2, + }, + { + name: "single user, contacts + email", + user: []string{"u1"}, + data: []string{dataContacts, dataEmail}, + expectIncludeLen: 2, + }, + { + name: "all users, email + events", + user: []string{utils.Wildcard}, + data: []string{dataEmail, dataEvents}, + expectIncludeLen: 2, + }, + { + name: "single user, email + events", + user: []string{"u1"}, + data: []string{dataEmail, dataEvents}, + expectIncludeLen: 2, + }, + { + name: "all users, events + contacts", + user: []string{utils.Wildcard}, + data: []string{dataEvents, dataContacts}, + expectIncludeLen: 2, + }, + { + name: "single user, events + contacts", + user: []string{"u1"}, + data: []string{dataEvents, dataContacts}, + expectIncludeLen: 2, + }, + { + name: "many users, events", + user: []string{"fnord", "smarf"}, + data: []string{dataEvents}, + expectIncludeLen: 2, + }, + { + name: "many users, events + contacts", + user: []string{"fnord", "smarf"}, + data: []string{dataEvents, dataContacts}, + expectIncludeLen: 4, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + sel := exchangeBackupCreateSelectors(test.all, test.user, test.data) + assert.Equal(t, test.expectIncludeLen, len(sel.Includes)) + }) + } +} diff --git a/src/cli/restore/exchange.go b/src/cli/restore/exchange.go index e3658e455..2948edf4c 100644 --- a/src/cli/restore/exchange.go +++ b/src/cli/restore/exchange.go @@ -127,10 +127,10 @@ func validateRestoreFlags(u, f, m, rpid string) error { return errors.New("a restore point ID is requried") } lu, lf, lm := len(u), len(f), len(m) - if (lu == 0 || u == "*") && (lf+lm > 0) { + if (lu == 0 || u == utils.Wildcard) && (lf+lm > 0) { return errors.New("a specific --user must be provided if --folder or --mail is specified") } - if (lf == 0 || f == "*") && lm > 0 { + if (lf == 0 || f == utils.Wildcard) && lm > 0 { return errors.New("a specific --folder must be provided if a --mail is specified") } return nil diff --git a/src/cli/restore/exchange_test.go b/src/cli/restore/exchange_test.go index fdbc5aa4a..31bb29b70 100644 --- a/src/cli/restore/exchange_test.go +++ b/src/cli/restore/exchange_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/alcionai/corso/cli/utils" ctesting "github.com/alcionai/corso/internal/testing" ) @@ -27,10 +28,10 @@ func (suite *ExchangeSuite) TestValidateRestoreFlags() { }{ {"all populated", "u", "f", "m", "rpid", assert.NoError}, {"folder missing user", "", "f", "m", "rpid", assert.Error}, - {"folder with wildcard 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", "*", "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}, } diff --git a/src/cli/utils/utils.go b/src/cli/utils/utils.go index 9a07f891d..185ef42f2 100644 --- a/src/cli/utils/utils.go +++ b/src/cli/utils/utils.go @@ -10,6 +10,10 @@ import ( "github.com/spf13/pflag" ) +const ( + Wildcard = "*" +) + // RequireProps validates the existence of the properties // in the map. Expects the format map[propName]propVal. func RequireProps(props map[string]string) error {