package backup import ( "context" "encoding/json" "fmt" "strings" "github.com/alcionai/clues" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/alcionai/corso/src/cli/flags" . "github.com/alcionai/corso/src/cli/print" "github.com/alcionai/corso/src/cli/utils" "github.com/alcionai/corso/src/internal/common/color" "github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/observe" "github.com/alcionai/corso/src/pkg/backup" "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/errs/core" "github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/repository" "github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/store" ) var ErrEmptyBackup = clues.New("no items in backup") // --------------------------------------------------------------------------- // adding commands to cobra // --------------------------------------------------------------------------- var subCommandFuncs = []func() *cobra.Command{ createCmd, listCmd, detailsCmd, deleteCmd, } var serviceCommands = []func(cmd *cobra.Command) *cobra.Command{ addExchangeCommands, addOneDriveCommands, addSharePointCommands, addGroupsCommands, } // AddCommands attaches all `corso backup * *` commands to the parent. func AddCommands(cmd *cobra.Command) { backupC := backupCmd() cmd.AddCommand(backupC) for _, sc := range subCommandFuncs { subCommand := sc() backupC.AddCommand(subCommand) for _, addBackupTo := range serviceCommands { sc := addBackupTo(subCommand) flags.AddAllProviderFlags(sc) flags.AddAllStorageFlags(sc) } } } // --------------------------------------------------------------------------- // commands // --------------------------------------------------------------------------- // The backup category of commands. // `corso backup [] [...]` func backupCmd() *cobra.Command { return &cobra.Command{ Use: "backup", Short: "Backup your service data", Long: `Backup the data stored in one of your M365 services.`, RunE: handleBackupCmd, Args: cobra.NoArgs, } } // Handler for flat calls to `corso backup`. // Produces the same output as `corso backup --help`. func handleBackupCmd(cmd *cobra.Command, args []string) error { return cmd.Help() } // The backup create subcommand. // `corso backup create [...]` var createCommand = "create" func createCmd() *cobra.Command { return &cobra.Command{ Use: createCommand, Short: "Backup an M365 Service", RunE: handleCreateCmd, Args: cobra.NoArgs, } } // Handler for calls to `corso backup create`. // Produces the same output as `corso backup create --help`. func handleCreateCmd(cmd *cobra.Command, args []string) error { return cmd.Help() } // The backup list subcommand. // `corso backup list [...]` var listCommand = "list" func listCmd() *cobra.Command { return &cobra.Command{ Use: listCommand, Short: "List the history of backups", RunE: handleListCmd, Args: cobra.NoArgs, } } // Handler for calls to `corso backup list`. // Produces the same output as `corso backup list --help`. func handleListCmd(cmd *cobra.Command, args []string) error { return cmd.Help() } // The backup details subcommand. // `corso backup details [...]` var detailsCommand = "details" func detailsCmd() *cobra.Command { return &cobra.Command{ Use: detailsCommand, Short: "Shows the details of a backup", RunE: handleDetailsCmd, Args: cobra.NoArgs, } } // Handler for calls to `corso backup details`. // Produces the same output as `corso backup details --help`. func handleDetailsCmd(cmd *cobra.Command, args []string) error { return cmd.Help() } // The backup delete subcommand. // `corso backup delete [...]` var deleteCommand = "delete" func deleteCmd() *cobra.Command { return &cobra.Command{ Use: deleteCommand, Short: "Deletes a backup", RunE: handleDeleteCmd, Args: cobra.NoArgs, } } // Handler for calls to `corso backup delete`. // Produces the same output as `corso backup delete --help`. func handleDeleteCmd(cmd *cobra.Command, args []string) error { return cmd.Help() } // --------------------------------------------------------------------------- // common handlers // --------------------------------------------------------------------------- // standard set of selector behavior that we want used in the cli var defaultSelectorConfig = selectors.Config{OnlyMatchItemNames: true} func genericCreateCommand( ctx context.Context, r repository.Repositoryer, serviceName string, selectorSet []selectors.Selector, ins idname.Cacher, ) error { var ( bIDs []string errs = []error{} ) for _, discSel := range selectorSet { discSel.Configure(defaultSelectorConfig) var ( owner = discSel.DiscreteOwner ictx = clues.Add(ctx, "resource_owner_selected", owner) ) logger.Ctx(ictx).Infof("setting up backup") bo, err := r.NewBackupWithLookup(ictx, discSel, ins) if err != nil { cerr := clues.WrapWC(ictx, err, owner) errs = append(errs, cerr) meta, err := json.Marshal(cerr.Core().Values) if err != nil { meta = []byte("Unable to marshal error metadata") } Errf(ictx, "%s\nMessage: %v\nMetadata:%s", "Unable to complete backup", err, meta) continue } ictx = clues.Add( ictx, "resource_owner_id", bo.ResourceOwner.ID(), "resource_owner_name", clues.Hide(bo.ResourceOwner.Name())) logger.Ctx(ictx).Infof("running backup") err = bo.Run(ictx) if err != nil { if errors.Is(err, core.ErrServiceNotEnabled) { logger.Ctx(ictx).Infow("service not enabled", "resource_owner_id", bo.ResourceOwner.ID(), "service", serviceName) continue } cerr := clues.Wrap(err, owner) errs = append(errs, cerr) meta, err := json.Marshal(cerr.Core().Values) if err != nil { meta = []byte("Unable to marshal error metadata") } Errf(ictx, "%s\nMessage: %v\nMetadata:%s", "Unable to complete backup", err, meta) continue } bIDs = append(bIDs, string(bo.Results.BackupID)) if !DisplayJSONFormat() { Infof(ictx, fmt.Sprintf("Backup complete %s %s", observe.Bullet, color.BlueOutput(bo.Results.BackupID))) printBackupStats(ictx, r, string(bo.Results.BackupID)) } else { Infof(ictx, "Backup complete - ID: %v\n", bo.Results.BackupID) } } bups, berrs := r.Backups(ctx, bIDs) if berrs.Failure() != nil { return Only(ctx, clues.Wrap(berrs.Failure(), "Unable to retrieve backup results from storage")) } if len(bups) > 0 { Info(ctx, "\nCompleted Backups:") backup.PrintAll(ctx, bups) } if len(errs) > 0 { sb := fmt.Sprintf("%d of %d backups failed:\n", len(errs), len(selectorSet)) for i, e := range errs { logger.CtxErr(ctx, e).Errorf("Backup %d of %d failed", i+1, len(selectorSet)) sb += "∙ " + e.Error() + "\n" } return Only(ctx, clues.New(sb)) } return nil } // genericDeleteCommand is a helper function that all services can use // for the removal of an entry from the repository func genericDeleteCommand( cmd *cobra.Command, pst path.ServiceType, designation string, bID, args []string, ) error { if utils.HasNoFlagsAndShownHelp(cmd) { return nil } if flags.RunModeFV == flags.RunModeFlagTest { return nil } ctx := clues.Add(cmd.Context(), "delete_backup_id", bID) r, _, err := utils.GetAccountAndConnect(ctx, cmd, pst) if err != nil { return Only(ctx, err) } defer utils.CloseRepo(ctx, r) if err := r.DeleteBackups(ctx, true, bID...); err != nil { return Only(ctx, clues.Wrap(err, fmt.Sprintf("Deleting backup %v", bID))) } Infof(ctx, "Deleted %s backup %v", designation, bID) return nil } // genericListCommand is a helper function that all services can use // to display the backup IDs saved within the repository func genericListCommand( cmd *cobra.Command, bID string, service path.ServiceType, args []string, ) error { ctx := cmd.Context() if flags.RunModeFV == flags.RunModeFlagTest { return nil } r, _, err := utils.GetAccountAndConnect(ctx, cmd, service) if err != nil { return Only(ctx, err) } defer utils.CloseRepo(ctx, r) if len(bID) > 0 { fe, b, errs := r.GetBackupErrors(ctx, bID) if errs.Failure() != nil { if errors.Is(errs.Failure(), data.ErrNotFound) { return Only(ctx, clues.New("No backup exists with the id "+bID)) } return Only(ctx, clues.Wrap(errs.Failure(), "Failed to list backup id "+bID)) } b.Print(ctx) fe.PrintItems( ctx, !ifShow(flags.ListAlertsFV), !ifShow(flags.FailedItemsFV), !ifShow(flags.ListSkippedItemsFV), !ifShow(flags.ListRecoveredErrorsFV)) return nil } bs, err := r.BackupsByTag(ctx, store.Service(service)) if err != nil { return Only(ctx, clues.Wrap(err, "Failed to list backups in the repository")) } backup.PrintAll(ctx, bs) return nil } func genericDetailsCommand( cmd *cobra.Command, backupID string, sel selectors.Selector, ) (*details.Details, error) { ctx := cmd.Context() r, rdao, err := utils.GetAccountAndConnect(ctx, cmd, path.OneDriveService) if err != nil { return nil, clues.Stack(err) } defer utils.CloseRepo(ctx, r) return genericDetailsCore( ctx, r, backupID, sel, rdao.Opts) } func genericDetailsCore( ctx context.Context, bg repository.BackupGetter, backupID string, sel selectors.Selector, opts control.Options, ) (*details.Details, error) { ctx = clues.Add(ctx, "backup_id", backupID) sel.Configure(selectors.Config{OnlyMatchItemNames: true}) d, _, errs := bg.GetBackupDetails(ctx, backupID) // TODO: log/track recoverable errors if errs.Failure() != nil { if errors.Is(errs.Failure(), data.ErrNotFound) { return nil, clues.New("no backup exists with the id " + backupID) } return nil, clues.Wrap(errs.Failure(), "Failed to get backup details in the repository") } if len(d.Entries) == 0 { return nil, ErrEmptyBackup } if opts.SkipReduce { return d, nil } d, err := sel.Reduce(ctx, d, errs) if err != nil { return nil, clues.Wrap(err, "filtering backup details to selection") } return d, nil } // --------------------------------------------------------------------------- // helper funcs // --------------------------------------------------------------------------- func ifShow(flag string) bool { return strings.ToLower(strings.TrimSpace(flag)) == "show" } func printBackupStats(ctx context.Context, r repository.Repositoryer, bid string) { b, err := r.Backup(ctx, bid) if err != nil { logger.CtxErr(ctx, err).Error("finding backup immediately after backup operation completion") } b.ToPrintable().Stats.PrintProperties(ctx) }