OneDrive CLI commands (#739)

## Description

Wires up the OneDrive Backup(create,list,details,delete) and Restore commands

Unit tests added but integration tests will be added after the underlying operation
PRs are merged.

## Type of change

Please check the type of change your PR introduces:
- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Test
- [ ] 🐹 Trivial/Minor

## Issue(s)
#658 
#668 

## Test Plan

<!-- How will this be tested prior to merging.-->

- [x] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
Vaibhav Kamra 2022-09-16 13:32:36 -07:00 committed by GitHub
parent 31e4a68355
commit a9b0e2e7ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 560 additions and 9 deletions

View File

@ -13,6 +13,7 @@ var subCommands = []*cobra.Command{
var serviceCommands = []func(parent *cobra.Command) *cobra.Command{
addExchangeCommands,
addOneDriveCommands,
}
// AddCommands attaches all `corso backup * *` commands to the parent.

252
src/cli/backup/onedrive.go Normal file
View File

@ -0,0 +1,252 @@
package backup
import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/alcionai/corso/src/cli/config"
"github.com/alcionai/corso/src/cli/options"
. "github.com/alcionai/corso/src/cli/print"
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/internal/model"
"github.com/alcionai/corso/src/pkg/backup"
"github.com/alcionai/corso/src/pkg/repository"
"github.com/alcionai/corso/src/pkg/selectors"
)
// ------------------------------------------------------------------------------------------------
// setup and globals
// ------------------------------------------------------------------------------------------------
const oneDriveServiceCommand = "onedrive"
// called by backup.go to map parent subcommands to provider-specific handling.
func addOneDriveCommands(parent *cobra.Command) *cobra.Command {
var (
c *cobra.Command
fs *pflag.FlagSet
)
switch parent.Use {
case createCommand:
c, fs = utils.AddCommand(parent, oneDriveCreateCmd())
fs.StringArrayVar(&user, "user", nil,
"Backup OneDrive data by user ID; accepts "+utils.Wildcard+" to select all users")
options.AddOperationFlags(c)
case listCommand:
c, _ = utils.AddCommand(parent, oneDriveListCmd())
case detailsCommand:
c, fs = utils.AddCommand(parent, oneDriveDetailsCmd())
fs.StringVar(&backupID, "backup", "", "ID of the backup containing the details to be shown")
cobra.CheckErr(c.MarkFlagRequired("backup"))
case deleteCommand:
c, fs = utils.AddCommand(parent, oneDriveDeleteCmd())
fs.StringVar(&backupID, "backup", "", "ID of the backup containing the details to be shown")
cobra.CheckErr(c.MarkFlagRequired("backup"))
}
return c
}
// ------------------------------------------------------------------------------------------------
// backup create
// ------------------------------------------------------------------------------------------------
// `corso backup create onedrive [<flag>...]`
func oneDriveCreateCmd() *cobra.Command {
return &cobra.Command{
Use: oneDriveServiceCommand,
Short: "Backup M365 OneDrive service data",
RunE: createOneDriveCmd,
Args: cobra.NoArgs,
}
}
// processes an onedrive service backup.
func createOneDriveCmd(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if utils.HasNoFlagsAndShownHelp(cmd) {
return nil
}
if err := validateOneDriveBackupCreateFlags(user); err != nil {
return err
}
s, acct, err := config.GetStorageAndAccount(ctx, true, nil)
if err != nil {
return Only(ctx, err)
}
r, err := repository.Connect(ctx, acct, s, options.Control())
if err != nil {
return Only(ctx, errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider))
}
defer utils.CloseRepo(ctx, r)
sel := oneDriveBackupCreateSelectors(user)
bo, err := r.NewBackup(ctx, sel)
if err != nil {
return Only(ctx, errors.Wrap(err, "Failed to initialize OneDrive backup"))
}
err = bo.Run(ctx)
if err != nil {
return Only(ctx, errors.Wrap(err, "Failed to run OneDrive backup"))
}
bu, err := r.Backup(ctx, bo.Results.BackupID)
if err != nil {
return errors.Wrap(err, "Unable to retrieve backup results from storage")
}
bu.Print(ctx)
return nil
}
func validateOneDriveBackupCreateFlags(users []string) error {
if len(users) == 0 {
return errors.New("requires one or more --user ids or the wildcard --user *")
}
return nil
}
func oneDriveBackupCreateSelectors(users []string) selectors.Selector {
sel := selectors.NewOneDriveBackup()
sel.Include(sel.Users(users))
return sel.Selector
}
// ------------------------------------------------------------------------------------------------
// backup list
// ------------------------------------------------------------------------------------------------
// `corso backup list onedrive [<flag>...]`
func oneDriveListCmd() *cobra.Command {
return &cobra.Command{
Use: oneDriveServiceCommand,
Short: "List the history of M365 OneDrive service backups",
RunE: listOneDriveCmd,
Args: cobra.NoArgs,
}
}
// lists the history of backup operations
func listOneDriveCmd(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
s, acct, err := config.GetStorageAndAccount(ctx, true, nil)
if err != nil {
return Only(ctx, err)
}
r, err := repository.Connect(ctx, acct, s, options.Control())
if err != nil {
return Only(ctx, errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider))
}
defer utils.CloseRepo(ctx, r)
bs, err := r.Backups(ctx)
if err != nil {
return Only(ctx, errors.Wrap(err, "Failed to list backups in the repository"))
}
backup.PrintAll(ctx, bs)
return nil
}
// ------------------------------------------------------------------------------------------------
// backup details
// ------------------------------------------------------------------------------------------------
// `corso backup details onedrive [<flag>...]`
func oneDriveDetailsCmd() *cobra.Command {
return &cobra.Command{
Use: oneDriveServiceCommand,
Short: "Shows the details of a M365 OneDrive service backup",
RunE: detailsOneDriveCmd,
Args: cobra.NoArgs,
}
}
// lists the history of backup operations
func detailsOneDriveCmd(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if utils.HasNoFlagsAndShownHelp(cmd) {
return nil
}
s, acct, err := config.GetStorageAndAccount(ctx, true, nil)
if err != nil {
return Only(ctx, err)
}
r, err := repository.Connect(ctx, acct, s, options.Control())
if err != nil {
return Only(ctx, errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider))
}
defer utils.CloseRepo(ctx, r)
ds, _, err := r.BackupDetails(ctx, backupID)
if err != nil {
return Only(ctx, errors.Wrap(err, "Failed to get backup details in the repository"))
}
// TODO: Support selectors and filters
ds.PrintEntries(ctx)
return nil
}
// `corso backup delete onedrive [<flag>...]`
func oneDriveDeleteCmd() *cobra.Command {
return &cobra.Command{
Use: oneDriveServiceCommand,
Short: "Delete backed-up M365 OneDrive service data",
RunE: deleteOneDriveCmd,
Args: cobra.NoArgs,
}
}
// deletes an exchange service backup.
func deleteOneDriveCmd(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if utils.HasNoFlagsAndShownHelp(cmd) {
return nil
}
s, acct, err := config.GetStorageAndAccount(ctx, true, nil)
if err != nil {
return Only(ctx, err)
}
r, err := repository.Connect(ctx, acct, s, options.Control())
if err != nil {
return Only(ctx, errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider))
}
defer utils.CloseRepo(ctx, r)
if err := r.DeleteBackup(ctx, model.StableID(backupID)); err != nil {
return Only(ctx, errors.Wrapf(err, "Deleting backup %s", backupID))
}
return nil
}

View File

@ -0,0 +1,76 @@
package backup
import (
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/tester"
)
type OneDriveSuite struct {
suite.Suite
}
func TestOneDriveSuite(t *testing.T) {
suite.Run(t, new(OneDriveSuite))
}
func (suite *OneDriveSuite) TestAddOneDriveCommands() {
expectUse := oneDriveServiceCommand
table := []struct {
name string
use string
expectUse string
expectShort string
expectRunE func(*cobra.Command, []string) error
}{
{"create onedrive", createCommand, expectUse, oneDriveCreateCmd().Short, createOneDriveCmd},
{"list onedrive", listCommand, expectUse, oneDriveListCmd().Short, listOneDriveCmd},
{"details onedrive", detailsCommand, expectUse, oneDriveDetailsCmd().Short, detailsOneDriveCmd},
{"delete onedrive", deleteCommand, expectUse, oneDriveDeleteCmd().Short, deleteOneDriveCmd},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
cmd := &cobra.Command{Use: test.use}
c := addOneDriveCommands(cmd)
require.NotNil(t, c)
cmds := cmd.Commands()
require.Len(t, cmds, 1)
child := cmds[0]
assert.Equal(t, test.expectUse, child.Use)
assert.Equal(t, test.expectShort, child.Short)
tester.AreSameFunc(t, test.expectRunE, child.RunE)
})
}
}
func (suite *OneDriveSuite) TestValidateOneDriveBackupCreateFlags() {
table := []struct {
name string
user []string
expect assert.ErrorAssertionFunc
}{
{
name: "no users",
expect: assert.Error,
},
{
name: "users",
user: []string{"fnord"},
expect: assert.NoError,
},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
test.expect(t, validateOneDriveBackupCreateFlags(test.user))
})
}
}

View File

@ -0,0 +1,98 @@
package restore
import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/alcionai/corso/src/cli/config"
"github.com/alcionai/corso/src/cli/options"
. "github.com/alcionai/corso/src/cli/print"
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/pkg/repository"
"github.com/alcionai/corso/src/pkg/selectors"
)
// called by restore.go to map parent subcommands to provider-specific handling.
func addOneDriveCommands(parent *cobra.Command) *cobra.Command {
var (
c *cobra.Command
fs *pflag.FlagSet
)
switch parent.Use {
case restoreCommand:
c, fs = utils.AddCommand(parent, oneDriveRestoreCmd())
fs.StringVar(&backupID, "backup", "", "ID of the backup to restore")
cobra.CheckErr(c.MarkFlagRequired("backup"))
fs.StringSliceVar(&user,
"user", nil,
"Restore all data by user ID; accepts "+utils.Wildcard+" to select all users")
// others
options.AddOperationFlags(c)
}
return c
}
const oneDriveServiceCommand = "onedrive"
// `corso restore onedrive [<flag>...]`
func oneDriveRestoreCmd() *cobra.Command {
return &cobra.Command{
Use: oneDriveServiceCommand,
Short: "Restore M365 OneDrive service data",
RunE: restoreOneDriveCmd,
Args: cobra.NoArgs,
}
}
// processes an onedrive service restore.
func restoreOneDriveCmd(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if utils.HasNoFlagsAndShownHelp(cmd) {
return nil
}
if err := utils.ValidateOneDriveRestoreFlags(backupID); err != nil {
return err
}
s, a, err := config.GetStorageAndAccount(ctx, true, nil)
if err != nil {
return Only(ctx, err)
}
r, err := repository.Connect(ctx, a, s, options.Control())
if err != nil {
return Only(ctx, errors.Wrapf(err, "Failed to connect to the %s repository", s.Provider))
}
defer utils.CloseRepo(ctx, r)
sel := selectors.NewOneDriveRestore()
if user != nil {
sel.Include(sel.Users(user))
}
// 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)
if err != nil {
return Only(ctx, errors.Wrap(err, "Failed to initialize OneDrive restore"))
}
if err := ro.Run(ctx); err != nil {
return Only(ctx, errors.Wrap(err, "Failed to run OneDrive restore"))
}
Infof(ctx, "Restored OneDrive in %s for user %s.\n", s.Provider, sel.ToPrintable().Resources())
return nil
}

View File

@ -0,0 +1,50 @@
package restore
import (
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/tester"
)
type OneDriveSuite struct {
suite.Suite
}
func TestOneDriveSuite(t *testing.T) {
suite.Run(t, new(OneDriveSuite))
}
func (suite *OneDriveSuite) TestAddOneDriveCommands() {
expectUse := oneDriveServiceCommand
table := []struct {
name string
use string
expectUse string
expectShort string
expectRunE func(*cobra.Command, []string) error
}{
{"restore onedrive", restoreCommand, expectUse, oneDriveRestoreCmd().Short, restoreOneDriveCmd},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
cmd := &cobra.Command{Use: test.use}
c := addOneDriveCommands(cmd)
require.NotNil(t, c)
cmds := cmd.Commands()
require.Len(t, cmds, 1)
child := cmds[0]
assert.Equal(t, test.expectUse, child.Use)
assert.Equal(t, test.expectShort, child.Short)
tester.AreSameFunc(t, test.expectRunE, child.RunE)
})
}
}

View File

@ -6,6 +6,7 @@ import (
var restoreCommands = []func(parent *cobra.Command) *cobra.Command{
addExchangeCommands,
addOneDriveCommands,
}
// AddCommands attaches all `corso restore * *` commands to the parent.

14
src/cli/utils/onedrive.go Normal file
View File

@ -0,0 +1,14 @@
package utils
import (
"errors"
)
// ValidateOneDriveRestoreFlags checks common flags for correctness and interdependencies
func ValidateOneDriveRestoreFlags(backupID string) error {
if len(backupID) == 0 {
return errors.New("a backup ID is required")
}
return nil
}

View File

@ -16,6 +16,7 @@ import (
"github.com/alcionai/corso/src/internal/path"
"github.com/alcionai/corso/src/internal/stats"
"github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/selectors"
@ -118,16 +119,27 @@ func (op *RestoreOperation) Run(ctx context.Context) (err error) {
return err
}
er, err := op.Selectors.ToExchangeRestore()
if err != nil {
opStats.readErr = err
return err
}
var fds *details.Details
// format the details and retrieve the items from kopia
fds := er.Reduce(ctx, d)
if len(fds.Entries) == 0 {
return errors.New("nothing to restore: no items in the backup match the provided selectors")
switch op.Selectors.Service {
case selectors.ServiceExchange:
er, err := op.Selectors.ToExchangeRestore()
if err != nil {
opStats.readErr = err
return err
}
// format the details and retrieve the items from kopia
fds = er.Reduce(ctx, d)
if len(fds.Entries) == 0 {
return errors.New("nothing to restore: no items in the backup match the provided selectors")
}
case selectors.ServiceOneDrive:
// TODO: Reduce `details` here when we add support for OneDrive restore filters
fds = d
default:
return errors.Errorf("Service %s not supported", op.Selectors.Service)
}
fdsPaths := fds.Paths()

View File

@ -22,6 +22,13 @@ type (
OneDriveBackup struct {
oneDrive
}
// OneDriveRestorep provides an api for selecting
// data scopes applicable to the OneDrive service,
// plus restore-specific methods.
OneDriveRestore struct {
oneDrive
}
)
// NewOneDriveBackup produces a new Selector with the service set to ServiceOneDrive.
@ -47,6 +54,29 @@ func (s Selector) ToOneDriveBackup() (*OneDriveBackup, error) {
return &src, nil
}
// NewOneDriveRestore produces a new Selector with the service set to ServiceOneDrive.
func NewOneDriveRestore() *OneDriveRestore {
src := OneDriveRestore{
oneDrive{
newSelector(ServiceOneDrive),
},
}
return &src
}
// ToOneDriveRestore transforms the generic selector into an OneDriveRestore.
// Errors if the service defined by the selector is not ServiceOneDrive.
func (s Selector) ToOneDriveRestore() (*OneDriveRestore, error) {
if s.Service != ServiceOneDrive {
return nil, badCastErr(ServiceOneDrive, s.Service)
}
src := OneDriveRestore{oneDrive{s}}
return &src, nil
}
// Printable creates the minimized display of a selector, formatted for human readability.
func (s oneDrive) Printable() Printable {
return toPrintable[OneDriveScope](s.Selector)

View File

@ -157,3 +157,20 @@ func (suite *OneDriveSelectorSuite) TestOneDriveSelector_Exclude_Users() {
)
}
}
func (suite *OneDriveSelectorSuite) TestNewOneDriveRestore() {
t := suite.T()
or := NewOneDriveRestore()
assert.Equal(t, or.Service, ServiceOneDrive)
assert.NotZero(t, or.Scopes())
}
func (suite *OneDriveSelectorSuite) TestToOneDriveRestore() {
t := suite.T()
eb := NewOneDriveRestore()
s := eb.Selector
or, err := s.ToOneDriveRestore()
require.NoError(t, err)
assert.Equal(t, or.Service, ServiceOneDrive)
assert.NotZero(t, or.Scopes())
}