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:
|
// 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.
|
// More generic (ex: --user) and more frequently used flags take precedence.
|
||||||
flags.AddBackupIDFlag(c, true)
|
flags.AddBackupIDFlag(c, true)
|
||||||
flags.AddExchangeDetailsAndRestoreFlags(c)
|
flags.AddExchangeDetailsAndRestoreFlags(c, false)
|
||||||
|
|
||||||
case deleteCommand:
|
case deleteCommand:
|
||||||
c, fs = utils.AddCommand(cmd, exchangeDeleteCmd())
|
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,
|
addOneDriveCommands,
|
||||||
addSharePointCommands,
|
addSharePointCommands,
|
||||||
addGroupsCommands,
|
addGroupsCommands,
|
||||||
|
addExchangeCommands,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var defaultAcceptedFormatTypes = []string{string(control.DefaultFormat)}
|
||||||
|
|
||||||
// AddCommands attaches all `corso export * *` commands to the parent.
|
// AddCommands attaches all `corso export * *` commands to the parent.
|
||||||
func AddCommands(cmd *cobra.Command) {
|
func AddCommands(cmd *cobra.Command) {
|
||||||
subCommand := exportCmd()
|
subCommand := exportCmd()
|
||||||
@ -63,8 +66,9 @@ func runExport(
|
|||||||
ueco utils.ExportCfgOpts,
|
ueco utils.ExportCfgOpts,
|
||||||
sel selectors.Selector,
|
sel selectors.Selector,
|
||||||
backupID, serviceName string,
|
backupID, serviceName string,
|
||||||
|
acceptedFormatTypes []string,
|
||||||
) error {
|
) error {
|
||||||
if err := utils.ValidateExportConfigFlags(&ueco); err != nil {
|
if err := utils.ValidateExportConfigFlags(&ueco, acceptedFormatTypes); err != nil {
|
||||||
return Only(ctx, err)
|
return Only(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/alcionai/corso/src/cli/flags"
|
"github.com/alcionai/corso/src/cli/flags"
|
||||||
"github.com/alcionai/corso/src/cli/utils"
|
"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.
|
// 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)
|
sel := utils.IncludeGroupsRestoreDataSelectors(ctx, opts)
|
||||||
utils.FilterGroupsRestoreInfoSelectors(sel, 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)
|
sel := utils.IncludeOneDriveRestoreDataSelectors(opts)
|
||||||
utils.FilterOneDriveRestoreInfoSelectors(sel, 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)
|
sel := utils.IncludeSharePointRestoreDataSelectors(ctx, opts)
|
||||||
utils.FilterSharePointRestoreInfoSelectors(sel, 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
|
// AddExchangeDetailsAndRestoreFlags adds flags that are common to both the
|
||||||
// details and restore commands.
|
// details and restore commands.
|
||||||
func AddExchangeDetailsAndRestoreFlags(cmd *cobra.Command) {
|
func AddExchangeDetailsAndRestoreFlags(cmd *cobra.Command, emailOnly bool) {
|
||||||
fs := cmd.Flags()
|
fs := cmd.Flags()
|
||||||
|
|
||||||
// email flags
|
// email flags
|
||||||
@ -78,6 +78,12 @@ func AddExchangeDetailsAndRestoreFlags(cmd *cobra.Command) {
|
|||||||
EmailReceivedBeforeFN, "",
|
EmailReceivedBeforeFN, "",
|
||||||
"Select emails received before this datetime.")
|
"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
|
// event flags
|
||||||
fs.StringSliceVar(
|
fs.StringSliceVar(
|
||||||
&EventFV,
|
&EventFV,
|
||||||
|
|||||||
@ -27,7 +27,7 @@ func addExchangeCommands(cmd *cobra.Command) *cobra.Command {
|
|||||||
fs.SortFlags = false
|
fs.SortFlags = false
|
||||||
|
|
||||||
flags.AddBackupIDFlag(c, true)
|
flags.AddBackupIDFlag(c, true)
|
||||||
flags.AddExchangeDetailsAndRestoreFlags(c)
|
flags.AddExchangeDetailsAndRestoreFlags(c, false)
|
||||||
flags.AddRestoreConfigFlags(c, true)
|
flags.AddRestoreConfigFlags(c, true)
|
||||||
flags.AddFailFastFlag(c)
|
flags.AddFailFastFlag(c)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,7 @@ type ExchangeOpts struct {
|
|||||||
EventSubject string
|
EventSubject string
|
||||||
|
|
||||||
RestoreCfg RestoreCfgOpts
|
RestoreCfg RestoreCfgOpts
|
||||||
|
ExportCfg ExportCfgOpts
|
||||||
|
|
||||||
Populated flags.PopulatedFlags
|
Populated flags.PopulatedFlags
|
||||||
}
|
}
|
||||||
@ -60,6 +61,7 @@ func MakeExchangeOpts(cmd *cobra.Command) ExchangeOpts {
|
|||||||
EventSubject: flags.EventSubjectFV,
|
EventSubject: flags.EventSubjectFV,
|
||||||
|
|
||||||
RestoreCfg: makeRestoreCfgOpts(cmd),
|
RestoreCfg: makeRestoreCfgOpts(cmd),
|
||||||
|
ExportCfg: makeExportCfgOpts(cmd),
|
||||||
|
|
||||||
// populated contains the list of flags that appear in the
|
// populated contains the list of flags that appear in the
|
||||||
// command, according to pflags. Use this to differentiate
|
// command, according to pflags. Use this to differentiate
|
||||||
|
|||||||
@ -45,12 +45,7 @@ func MakeExportConfig(
|
|||||||
|
|
||||||
// ValidateExportConfigFlags ensures all export config flags that utilize
|
// ValidateExportConfigFlags ensures all export config flags that utilize
|
||||||
// enumerated values match a well-known value.
|
// enumerated values match a well-known value.
|
||||||
func ValidateExportConfigFlags(opts *ExportCfgOpts) error {
|
func ValidateExportConfigFlags(opts *ExportCfgOpts, acceptedFormatTypes []string) error {
|
||||||
acceptedFormatTypes := []string{
|
|
||||||
string(control.DefaultFormat),
|
|
||||||
string(control.JSONFormat),
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, populated := opts.Populated[flags.FormatFN]; !populated {
|
if _, populated := opts.Populated[flags.FormatFN]; !populated {
|
||||||
opts.Format = string(control.DefaultFormat)
|
opts.Format = string(control.DefaultFormat)
|
||||||
} else if !filters.Equal(acceptedFormatTypes).Compare(opts.Format) {
|
} else if !filters.Equal(acceptedFormatTypes).Compare(opts.Format) {
|
||||||
|
|||||||
@ -55,6 +55,11 @@ func (suite *ExportCfgUnitSuite) TestMakeExportConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ExportCfgUnitSuite) TestValidateExportConfigFlags() {
|
func (suite *ExportCfgUnitSuite) TestValidateExportConfigFlags() {
|
||||||
|
acceptedFormatTypes := []string{
|
||||||
|
string(control.DefaultFormat),
|
||||||
|
string(control.JSONFormat),
|
||||||
|
}
|
||||||
|
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
input ExportCfgOpts
|
input ExportCfgOpts
|
||||||
@ -100,7 +105,8 @@ func (suite *ExportCfgUnitSuite) TestValidateExportConfigFlags() {
|
|||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
suite.Run(test.name, func() {
|
suite.Run(test.name, func() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
err := ValidateExportConfigFlags(&test.input)
|
|
||||||
|
err := ValidateExportConfigFlags(&test.input, acceptedFormatTypes)
|
||||||
|
|
||||||
test.expectErr(t, err, clues.ToCore(err))
|
test.expectErr(t, err, clues.ToCore(err))
|
||||||
assert.Equal(t, test.expectFormat, control.FormatType(test.input.Format))
|
assert.Equal(t, test.expectFormat, control.FormatType(test.input.Format))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user