Add cli for exchange export (#4641)
<!-- PR description--> --- #### Does this PR need a docs update or release note? - [ ] ✅ Yes, it's included - [x] 🕐 Yes, but in a later PR - [ ] ⛔ No #### Type of change <!--- Please check the type of change your PR introduces: ---> - [x] 🌻 Feature - [ ] 🐛 Bugfix - [ ] 🗺️ Documentation - [ ] 🤖 Supportability/Tests - [ ] 💻 CI/Deployment - [ ] 🧹 Tech Debt/Cleanup #### Issue(s) <!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. --> * https://github.com/alcionai/corso/issues/3893 #### Test Plan <!-- How will this be tested prior to merging.--> - [ ] 💪 Manual - [x] ⚡ Unit test - [ ] 💚 E2E
This commit is contained in:
parent
2dfe22dd0c
commit
fd9c431bea
@ -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())
|
||||
|
||||
109
src/cli/export/exchange.go
Normal file
109
src/cli/export/exchange.go
Normal file
@ -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 = "<destination> --backup <backupId>"
|
||||
|
||||
// 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 [<flag>...] <destination>`
|
||||
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)
|
||||
}
|
||||
78
src/cli/export/exchange_test.go
Normal file
78
src/cli/export/exchange_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user