From fd9c431bea4d6f8b35280183c802abb5383141bc Mon Sep 17 00:00:00 2001 From: Abin Simon Date: Thu, 16 Nov 2023 10:23:16 +0530 Subject: [PATCH] Add cli for exchange export (#4641) --- #### Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [x] :clock1: Yes, but in a later PR - [ ] :no_entry: No #### Type of change - [x] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Supportability/Tests - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup #### Issue(s) * https://github.com/alcionai/corso/issues/3893 #### Test Plan - [ ] :muscle: Manual - [x] :zap: Unit test - [ ] :green_heart: E2E --- src/cli/backup/exchange.go | 2 +- src/cli/export/exchange.go | 109 ++++++++++++++++++++++++++++ src/cli/export/exchange_test.go | 78 ++++++++++++++++++++ src/cli/export/export.go | 6 +- src/cli/export/groups.go | 16 +++- src/cli/export/onedrive.go | 10 ++- src/cli/export/sharepoint.go | 10 ++- src/cli/flags/exchange.go | 8 +- src/cli/restore/exchange.go | 2 +- src/cli/utils/exchange.go | 2 + src/cli/utils/export_config.go | 7 +- src/cli/utils/export_config_test.go | 8 +- 12 files changed, 244 insertions(+), 14 deletions(-) create mode 100644 src/cli/export/exchange.go create mode 100644 src/cli/export/exchange_test.go diff --git a/src/cli/backup/exchange.go b/src/cli/backup/exchange.go index 7753644b4..72d6ab857 100644 --- a/src/cli/backup/exchange.go +++ b/src/cli/backup/exchange.go @@ -105,7 +105,7 @@ func addExchangeCommands(cmd *cobra.Command) *cobra.Command { // Flags addition ordering should follow the order we want them to appear in help and docs: // More generic (ex: --user) and more frequently used flags take precedence. flags.AddBackupIDFlag(c, true) - flags.AddExchangeDetailsAndRestoreFlags(c) + flags.AddExchangeDetailsAndRestoreFlags(c, false) case deleteCommand: c, fs = utils.AddCommand(cmd, exchangeDeleteCmd()) diff --git a/src/cli/export/exchange.go b/src/cli/export/exchange.go new file mode 100644 index 000000000..90e2a9852 --- /dev/null +++ b/src/cli/export/exchange.go @@ -0,0 +1,109 @@ +package export + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/alcionai/corso/src/cli/flags" + "github.com/alcionai/corso/src/cli/utils" +) + +// called by export.go to map subcommands to provider-specific handling. +func addExchangeCommands(cmd *cobra.Command) *cobra.Command { + var ( + c *cobra.Command + fs *pflag.FlagSet + ) + + switch cmd.Use { + case exportCommand: + c, fs = utils.AddCommand(cmd, exchangeExportCmd()) + + c.Use = c.Use + " " + exchangeServiceCommandUseSuffix + + // Flags addition ordering should follow the order we want them to appear in help and docs: + // More generic (ex: --user) and more frequently used flags take precedence. + fs.SortFlags = false + + flags.AddBackupIDFlag(c, true) + flags.AddExchangeDetailsAndRestoreFlags(c, true) + flags.AddExportConfigFlags(c) + flags.AddFailFastFlag(c) + } + + return c +} + +const ( + exchangeServiceCommand = "exchange" + exchangeServiceCommandUseSuffix = " --backup " + + // TODO(meain): remove message about only supporting email exports once others are added + //nolint:lll + exchangeServiceCommandExportExamples = `> Only email exports are supported as of now. + +# Export emails with ID 98765abcdef and 12345abcdef from Alice's last backup (1234abcd...) to my-folder +corso export exchange my-folder --backup 1234abcd-12ab-cd34-56de-1234abcd --email 98765abcdef,12345abcdef + +# Export emails with subject containing "Hello world" in the "Inbox" to my-folder +corso export exchange --backup 1234abcd-12ab-cd34-56de-1234abcd \ + --email-subject "Hello world" --email-folder Inbox my-folder` + +// TODO(meain): Uncomment once support for these are added +// `# Export an entire calendar to my-folder +// corso export exchange --backup 1234abcd-12ab-cd34-56de-1234abcd \ +// --event-calendar Calendar my-folder + +// # Export the contact with ID abdef0101 to my-folder +// corso export exchange --backup 1234abcd-12ab-cd34-56de-1234abcd --contact abdef0101 my-folder` +) + +// `corso export exchange [...] ` +func exchangeExportCmd() *cobra.Command { + return &cobra.Command{ + Use: exchangeServiceCommand, + Short: "Export M365 Exchange service data", + RunE: exportExchangeCmd, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return errors.New("missing export destination") + } + + return nil + }, + Example: exchangeServiceCommandExportExamples, + } +} + +// processes an exchange service export. +func exportExchangeCmd(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + if utils.HasNoFlagsAndShownHelp(cmd) { + return nil + } + + opts := utils.MakeExchangeOpts(cmd) + + if flags.RunModeFV == flags.RunModeFlagTest { + return nil + } + + if err := utils.ValidateExchangeRestoreFlags(flags.BackupIDFV, opts); err != nil { + return err + } + + sel := utils.IncludeExchangeRestoreDataSelectors(opts) + utils.FilterExchangeRestoreInfoSelectors(sel, opts) + + return runExport( + ctx, + cmd, + args, + opts.ExportCfg, + sel.Selector, + flags.BackupIDFV, + "Exchange", + defaultAcceptedFormatTypes) +} diff --git a/src/cli/export/exchange_test.go b/src/cli/export/exchange_test.go new file mode 100644 index 000000000..0f7269636 --- /dev/null +++ b/src/cli/export/exchange_test.go @@ -0,0 +1,78 @@ +package export + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/src/cli/flags" + flagsTD "github.com/alcionai/corso/src/cli/flags/testdata" + cliTD "github.com/alcionai/corso/src/cli/testdata" + "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/internal/tester" +) + +type ExchangeUnitSuite struct { + tester.Suite +} + +func TestExchangeUnitSuite(t *testing.T) { + suite.Run(t, &ExchangeUnitSuite{Suite: tester.NewUnitSuite(t)}) +} + +func (suite *ExchangeUnitSuite) TestAddExchangeCommands() { + expectUse := exchangeServiceCommand + " " + exchangeServiceCommandUseSuffix + + table := []struct { + name string + use string + expectUse string + expectShort string + expectRunE func(*cobra.Command, []string) error + }{ + {"export exchange", exportCommand, expectUse, exchangeExportCmd().Short, exportExchangeCmd}, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + parent := &cobra.Command{Use: exportCommand} + + cmd := cliTD.SetUpCmdHasFlags( + t, + parent, + addExchangeCommands, + []cliTD.UseCobraCommandFn{ + flags.AddAllProviderFlags, + flags.AddAllStorageFlags, + }, + flagsTD.WithFlags( + exchangeServiceCommand, + []string{ + flagsTD.RestoreDestination, + "--" + flags.RunModeFN, flags.RunModeFlagTest, + "--" + flags.BackupFN, flagsTD.BackupInput, + "--" + flags.FormatFN, flagsTD.FormatType, + "--" + flags.ArchiveFN, + }, + flagsTD.PreparedProviderFlags(), + flagsTD.PreparedStorageFlags())) + + cliTD.CheckCmdChild( + t, + parent, + 3, + test.expectUse, + test.expectShort, + test.expectRunE) + + opts := utils.MakeExchangeOpts(cmd) + + assert.Equal(t, flagsTD.BackupInput, flags.BackupIDFV) + assert.Equal(t, flagsTD.Archive, opts.ExportCfg.Archive) + assert.Equal(t, flagsTD.FormatType, opts.ExportCfg.Format) + flagsTD.AssertStorageFlags(t, cmd) + }) + } +} diff --git a/src/cli/export/export.go b/src/cli/export/export.go index 774d27904..774cb2f9d 100644 --- a/src/cli/export/export.go +++ b/src/cli/export/export.go @@ -23,8 +23,11 @@ var exportCommands = []func(cmd *cobra.Command) *cobra.Command{ addOneDriveCommands, addSharePointCommands, addGroupsCommands, + addExchangeCommands, } +var defaultAcceptedFormatTypes = []string{string(control.DefaultFormat)} + // AddCommands attaches all `corso export * *` commands to the parent. func AddCommands(cmd *cobra.Command) { subCommand := exportCmd() @@ -63,8 +66,9 @@ func runExport( ueco utils.ExportCfgOpts, sel selectors.Selector, backupID, serviceName string, + acceptedFormatTypes []string, ) error { - if err := utils.ValidateExportConfigFlags(&ueco); err != nil { + if err := utils.ValidateExportConfigFlags(&ueco, acceptedFormatTypes); err != nil { return Only(ctx, err) } diff --git a/src/cli/export/groups.go b/src/cli/export/groups.go index cf370f760..ab9c19406 100644 --- a/src/cli/export/groups.go +++ b/src/cli/export/groups.go @@ -7,6 +7,7 @@ import ( "github.com/alcionai/corso/src/cli/flags" "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/pkg/control" ) // called by export.go to map subcommands to provider-specific handling. @@ -99,5 +100,18 @@ func exportGroupsCmd(cmd *cobra.Command, args []string) error { sel := utils.IncludeGroupsRestoreDataSelectors(ctx, opts) utils.FilterGroupsRestoreInfoSelectors(sel, opts) - return runExport(ctx, cmd, args, opts.ExportCfg, sel.Selector, flags.BackupIDFV, "Groups") + acceptedGroupsFormatTypes := []string{ + string(control.DefaultFormat), + string(control.JSONFormat), + } + + return runExport( + ctx, + cmd, + args, + opts.ExportCfg, + sel.Selector, + flags.BackupIDFV, + "Groups", + acceptedGroupsFormatTypes) } diff --git a/src/cli/export/onedrive.go b/src/cli/export/onedrive.go index cc22ed4ce..c6fb54165 100644 --- a/src/cli/export/onedrive.go +++ b/src/cli/export/onedrive.go @@ -90,5 +90,13 @@ func exportOneDriveCmd(cmd *cobra.Command, args []string) error { sel := utils.IncludeOneDriveRestoreDataSelectors(opts) utils.FilterOneDriveRestoreInfoSelectors(sel, opts) - return runExport(ctx, cmd, args, opts.ExportCfg, sel.Selector, flags.BackupIDFV, "OneDrive") + return runExport( + ctx, + cmd, + args, + opts.ExportCfg, + sel.Selector, + flags.BackupIDFV, + "OneDrive", + defaultAcceptedFormatTypes) } diff --git a/src/cli/export/sharepoint.go b/src/cli/export/sharepoint.go index 7e4fd3ae7..e2be0b49b 100644 --- a/src/cli/export/sharepoint.go +++ b/src/cli/export/sharepoint.go @@ -94,5 +94,13 @@ func exportSharePointCmd(cmd *cobra.Command, args []string) error { sel := utils.IncludeSharePointRestoreDataSelectors(ctx, opts) utils.FilterSharePointRestoreInfoSelectors(sel, opts) - return runExport(ctx, cmd, args, opts.ExportCfg, sel.Selector, flags.BackupIDFV, "SharePoint") + return runExport( + ctx, + cmd, + args, + opts.ExportCfg, + sel.Selector, + flags.BackupIDFV, + "SharePoint", + defaultAcceptedFormatTypes) } diff --git a/src/cli/flags/exchange.go b/src/cli/flags/exchange.go index c859e84a1..ad53e82f8 100644 --- a/src/cli/flags/exchange.go +++ b/src/cli/flags/exchange.go @@ -49,7 +49,7 @@ var ( // AddExchangeDetailsAndRestoreFlags adds flags that are common to both the // details and restore commands. -func AddExchangeDetailsAndRestoreFlags(cmd *cobra.Command) { +func AddExchangeDetailsAndRestoreFlags(cmd *cobra.Command, emailOnly bool) { fs := cmd.Flags() // email flags @@ -78,6 +78,12 @@ func AddExchangeDetailsAndRestoreFlags(cmd *cobra.Command) { EmailReceivedBeforeFN, "", "Select emails received before this datetime.") + // NOTE: Only temporary until we add support for exporting the + // others as well in exchange. + if emailOnly { + return + } + // event flags fs.StringSliceVar( &EventFV, diff --git a/src/cli/restore/exchange.go b/src/cli/restore/exchange.go index a7ffdbb08..77793a31e 100644 --- a/src/cli/restore/exchange.go +++ b/src/cli/restore/exchange.go @@ -27,7 +27,7 @@ func addExchangeCommands(cmd *cobra.Command) *cobra.Command { fs.SortFlags = false flags.AddBackupIDFlag(c, true) - flags.AddExchangeDetailsAndRestoreFlags(c) + flags.AddExchangeDetailsAndRestoreFlags(c, false) flags.AddRestoreConfigFlags(c, true) flags.AddFailFastFlag(c) } diff --git a/src/cli/utils/exchange.go b/src/cli/utils/exchange.go index 031ce15b6..5772f8958 100644 --- a/src/cli/utils/exchange.go +++ b/src/cli/utils/exchange.go @@ -31,6 +31,7 @@ type ExchangeOpts struct { EventSubject string RestoreCfg RestoreCfgOpts + ExportCfg ExportCfgOpts Populated flags.PopulatedFlags } @@ -60,6 +61,7 @@ func MakeExchangeOpts(cmd *cobra.Command) ExchangeOpts { EventSubject: flags.EventSubjectFV, RestoreCfg: makeRestoreCfgOpts(cmd), + ExportCfg: makeExportCfgOpts(cmd), // populated contains the list of flags that appear in the // command, according to pflags. Use this to differentiate diff --git a/src/cli/utils/export_config.go b/src/cli/utils/export_config.go index 708ae1d7e..3e466e6c0 100644 --- a/src/cli/utils/export_config.go +++ b/src/cli/utils/export_config.go @@ -45,12 +45,7 @@ func MakeExportConfig( // ValidateExportConfigFlags ensures all export config flags that utilize // enumerated values match a well-known value. -func ValidateExportConfigFlags(opts *ExportCfgOpts) error { - acceptedFormatTypes := []string{ - string(control.DefaultFormat), - string(control.JSONFormat), - } - +func ValidateExportConfigFlags(opts *ExportCfgOpts, acceptedFormatTypes []string) error { if _, populated := opts.Populated[flags.FormatFN]; !populated { opts.Format = string(control.DefaultFormat) } else if !filters.Equal(acceptedFormatTypes).Compare(opts.Format) { diff --git a/src/cli/utils/export_config_test.go b/src/cli/utils/export_config_test.go index 7da782072..c17c35308 100644 --- a/src/cli/utils/export_config_test.go +++ b/src/cli/utils/export_config_test.go @@ -55,6 +55,11 @@ func (suite *ExportCfgUnitSuite) TestMakeExportConfig() { } func (suite *ExportCfgUnitSuite) TestValidateExportConfigFlags() { + acceptedFormatTypes := []string{ + string(control.DefaultFormat), + string(control.JSONFormat), + } + table := []struct { name string input ExportCfgOpts @@ -100,7 +105,8 @@ func (suite *ExportCfgUnitSuite) TestValidateExportConfigFlags() { for _, test := range table { suite.Run(test.name, func() { t := suite.T() - err := ValidateExportConfigFlags(&test.input) + + err := ValidateExportConfigFlags(&test.input, acceptedFormatTypes) test.expectErr(t, err, clues.ToCore(err)) assert.Equal(t, test.expectFormat, control.FormatType(test.input.Format))