diff --git a/src/cli/restore/exchange.go b/src/cli/restore/exchange.go index a676c0525..fad2f7e0d 100644 --- a/src/cli/restore/exchange.go +++ b/src/cli/restore/exchange.go @@ -213,11 +213,13 @@ func restoreExchangeCmd(cmd *cobra.Command, args []string) error { return Only(ctx, errors.Wrap(err, "Failed to initialize Exchange restore")) } - if err := ro.Run(ctx); err != nil { + ds, err := ro.Run(ctx) + if err != nil { return Only(ctx, errors.Wrap(err, "Failed to run Exchange restore")) } - Infof(ctx, "Restored Exchange in %s for user %s.\n", s.Provider, user) + Infof(ctx, "Restored OneDrive in %s for user %s.\n", s.Provider, sel.ToPrintable().Resources()) + ds.PrintEntries(ctx) return nil } diff --git a/src/cli/restore/onedrive.go b/src/cli/restore/onedrive.go index 745b66d35..1680cc35f 100644 --- a/src/cli/restore/onedrive.go +++ b/src/cli/restore/onedrive.go @@ -153,11 +153,13 @@ func restoreOneDriveCmd(cmd *cobra.Command, args []string) error { return Only(ctx, errors.Wrap(err, "Failed to initialize OneDrive restore")) } - if err := ro.Run(ctx); err != nil { + ds, err := ro.Run(ctx) + if 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()) + ds.PrintEntries(ctx) return nil } diff --git a/src/internal/operations/restore.go b/src/internal/operations/restore.go index 661b1ce50..6cd5a7799 100644 --- a/src/internal/operations/restore.go +++ b/src/internal/operations/restore.go @@ -93,16 +93,16 @@ type restoreStats struct { } // Run begins a synchronous restore operation. -func (op *RestoreOperation) Run(ctx context.Context) (err error) { +func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.Details, err error) { defer trace.StartRegion(ctx, "operations:restore:run").End() - startTime := time.Now() - - // persist operation results to the model store on exit - opStats := restoreStats{ - bytesRead: &stats.ByteCounter{}, - restoreID: uuid.NewString(), - } + var ( + opStats = restoreStats{ + bytesRead: &stats.ByteCounter{}, + restoreID: uuid.NewString(), + } + startTime = time.Now() + ) defer func() { err = op.persistResults(ctx, startTime, &opStats) @@ -111,13 +111,12 @@ func (op *RestoreOperation) Run(ctx context.Context) (err error) { } }() - // retrieve the restore point details - d, b, err := op.store.GetDetailsFromBackupID(ctx, op.BackupID) + deets, bup, err := op.store.GetDetailsFromBackupID(ctx, op.BackupID) if err != nil { err = errors.Wrap(err, "getting backup details for restore") opStats.readErr = err - return err + return nil, err } op.bus.Event( @@ -126,96 +125,46 @@ func (op *RestoreOperation) Run(ctx context.Context) (err error) { map[string]any{ events.StartTime: startTime, events.BackupID: op.BackupID, - events.BackupCreateTime: b.CreationTime, + events.BackupCreateTime: bup.CreationTime, events.RestoreID: opStats.restoreID, // TODO: restore options, }, ) - var fds *details.Details - - 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 selectors.ErrorNoMatchingItems - } - - case selectors.ServiceOneDrive: - odr, err := op.Selectors.ToOneDriveRestore() - if err != nil { - opStats.readErr = err - return err - } - - // format the details and retrieve the items from kopia - fds = odr.Reduce(ctx, d) - if len(fds.Entries) == 0 { - return selectors.ErrorNoMatchingItems - } - - default: - return errors.Errorf("Service %s not supported", op.Selectors.Service) + paths, err := formatDetailsForRestoration(ctx, op.Selectors, deets) + if err != nil { + opStats.readErr = err + return nil, err } - logger.Ctx(ctx).Infof("Discovered %d items in backup %s to restore", len(fds.Entries), op.BackupID) + logger.Ctx(ctx).Infof("Discovered %d items in backup %s to restore", len(paths), op.BackupID) - fdsPaths := fds.Paths() - paths := make([]path.Path, len(fdsPaths)) - - var parseErrs *multierror.Error - - for i := range fdsPaths { - p, err := path.FromDataLayerPath(fdsPaths[i], true) - if err != nil { - parseErrs = multierror.Append( - parseErrs, - errors.Wrap(err, "parsing details entry path"), - ) - - continue - } - - paths[i] = p - } - - dcs, err := op.kopia.RestoreMultipleItems(ctx, b.SnapshotID, paths, opStats.bytesRead) + dcs, err := op.kopia.RestoreMultipleItems(ctx, bup.SnapshotID, paths, opStats.bytesRead) if err != nil { err = errors.Wrap(err, "retrieving service data") + opStats.readErr = err - parseErrs = multierror.Append(parseErrs, err) - opStats.readErr = parseErrs.ErrorOrNil() - - return err + return nil, err } - opStats.readErr = parseErrs.ErrorOrNil() opStats.cs = dcs opStats.resourceCount = len(data.ResourceOwnerSet(dcs)) // restore those collections using graph gc, err := connector.NewGraphConnector(ctx, op.account) if err != nil { - err = errors.Wrap(err, "connecting to graph api") + err = errors.Wrap(err, "connecting to microsoft servers") opStats.writeErr = err - return err + return nil, err } - // TODO: return details and print in CLI - _, err = gc.RestoreDataCollections(ctx, op.Selectors, op.Destination, dcs) + restoreDetails, err = gc.RestoreDataCollections(ctx, op.Selectors, op.Destination, dcs) if err != nil { err = errors.Wrap(err, "restoring service data") opStats.writeErr = err - return err + return nil, err } opStats.started = true @@ -223,7 +172,7 @@ func (op *RestoreOperation) Run(ctx context.Context) (err error) { logger.Ctx(ctx).Debug(gc.PrintableStatus()) - return nil + return restoreDetails, nil } // persists details and statistics about the restore operation. @@ -274,3 +223,64 @@ func (op *RestoreOperation) persistResults( return nil } + +// formatDetailsForRestoration reduces the provided detail entries according to the +// selector specifications. +func formatDetailsForRestoration( + ctx context.Context, + sel selectors.Selector, + deets *details.Details, +) ([]path.Path, error) { + var fds *details.Details + + switch sel.Service { + case selectors.ServiceExchange: + er, err := sel.ToExchangeRestore() + if err != nil { + return nil, err + } + + // format the details and retrieve the items from kopia + fds = er.Reduce(ctx, deets) + if len(fds.Entries) == 0 { + return nil, selectors.ErrorNoMatchingItems + } + + case selectors.ServiceOneDrive: + odr, err := sel.ToOneDriveRestore() + if err != nil { + return nil, err + } + + // format the details and retrieve the items from kopia + fds = odr.Reduce(ctx, deets) + if len(fds.Entries) == 0 { + return nil, selectors.ErrorNoMatchingItems + } + + default: + return nil, errors.Errorf("Service %s not supported", sel.Service) + } + + var ( + errs *multierror.Error + fdsPaths = fds.Paths() + paths = make([]path.Path, len(fdsPaths)) + ) + + for i := range fdsPaths { + p, err := path.FromDataLayerPath(fdsPaths[i], true) + if err != nil { + errs = multierror.Append( + errs, + errors.Wrap(err, "parsing details entry path"), + ) + + continue + } + + paths[i] = p + } + + return paths, nil +} diff --git a/src/internal/operations/restore_test.go b/src/internal/operations/restore_test.go index aaa63aa5f..caa3c6aab 100644 --- a/src/internal/operations/restore_test.go +++ b/src/internal/operations/restore_test.go @@ -241,9 +241,13 @@ func (suite *RestoreOpIntegrationSuite) TestRestore_Run() { mb) require.NoError(t, err) - require.NoError(t, ro.Run(ctx), "restoreOp.Run()") + ds, err := ro.Run(ctx) + + require.NoError(t, err, "restoreOp.Run()") require.NotEmpty(t, ro.Results, "restoreOp results") + require.NotNil(t, ds, "restored details") assert.Equal(t, ro.Status, Completed, "restoreOp status") + assert.Equal(t, ro.Results.ItemsWritten, len(ds.Entries), "count of items written matches restored entries in details") assert.Less(t, 0, ro.Results.ItemsRead, "restore items read") assert.Less(t, 0, ro.Results.ItemsWritten, "restored items written") assert.Less(t, int64(0), ro.Results.BytesRead, "bytes read") @@ -276,7 +280,10 @@ func (suite *RestoreOpIntegrationSuite) TestRestore_Run_ErrorNoResults() { dest, mb) require.NoError(t, err) - require.Error(t, ro.Run(ctx), "restoreOp.Run() should have 0 results") + + ds, err := ro.Run(ctx) + require.Error(t, err, "restoreOp.Run() should have errored") + require.Nil(t, ds, "restoreOp.Run() should not produce details") assert.Zero(t, ro.Results.ResourceOwners, "resource owners") assert.Zero(t, ro.Results.BytesRead, "bytes read") assert.Equal(t, 1, mb.TimesCalled[events.RestoreStart], "restore-start events") diff --git a/src/pkg/repository/repository_load_test.go b/src/pkg/repository/repository_load_test.go index ea56548f4..f5e051b34 100644 --- a/src/pkg/repository/repository_load_test.go +++ b/src/pkg/repository/repository_load_test.go @@ -165,16 +165,19 @@ func runRestoreLoadTest( t.Run("restore_"+name, func(t *testing.T) { var ( err error + ds *details.Details labels = pprof.Labels("restore_load_test", name) ) pprof.Do(ctx, labels, func(ctx context.Context) { - err = r.Run(ctx) + ds, err = r.Run(ctx) }) require.NoError(t, err, "running restore") require.NotEmpty(t, r.Results, "has results after run") + require.NotNil(t, ds, "has restored details") assert.Equal(t, r.Status, operations.Completed, "restore status") + assert.Equal(t, r.Results.ItemsWritten, len(ds.Entries), "count of items written matches restored entries in details") assert.Less(t, 0, r.Results.ItemsRead, "items read") assert.Less(t, 0, r.Results.ItemsWritten, "items written") assert.Less(t, 0, r.Results.ResourceOwners, "resource owners")