package operations import ( "context" "time" "github.com/pkg/errors" "github.com/alcionai/corso/internal/connector" "github.com/alcionai/corso/internal/connector/support" "github.com/alcionai/corso/internal/data" "github.com/alcionai/corso/internal/kopia" "github.com/alcionai/corso/internal/model" "github.com/alcionai/corso/internal/stats" "github.com/alcionai/corso/pkg/account" "github.com/alcionai/corso/pkg/backup" "github.com/alcionai/corso/pkg/backup/details" "github.com/alcionai/corso/pkg/control" "github.com/alcionai/corso/pkg/selectors" "github.com/alcionai/corso/pkg/store" ) // BackupOperation wraps an operation with backup-specific props. type BackupOperation struct { operation Results BackupResults `json:"results"` Selectors selectors.Selector `json:"selectors"` Version string `json:"version"` account account.Account } // BackupResults aggregate the details of the result of the operation. type BackupResults struct { stats.ReadWrites stats.StartAndEndTime BackupID model.StableID `json:"backupID"` } // NewBackupOperation constructs and validates a backup operation. func NewBackupOperation( ctx context.Context, opts control.Options, kw *kopia.Wrapper, sw *store.Wrapper, acct account.Account, selector selectors.Selector, ) (BackupOperation, error) { op := BackupOperation{ operation: newOperation(opts, kw, sw), Selectors: selector, Version: "v0", account: acct, } if err := op.validate(); err != nil { return BackupOperation{}, err } return op, nil } func (op BackupOperation) validate() error { return op.operation.validate() } // aggregates stats from the backup.Run(). // primarily used so that the defer can take in a // pointer wrapping the values, while those values // get populated asynchronously. type backupStats struct { k *kopia.BackupStats gc *support.ConnectorOperationStatus readErr, writeErr error } // Run begins a synchronous backup operation. func (op *BackupOperation) Run(ctx context.Context) (err error) { // TODO: persist initial state of backupOperation in modelstore // persist operation results to the model store on exit var ( opStats backupStats backupDetails *details.Details ) defer func() { op.persistResults(time.Now(), &opStats) err = op.createBackupModels(ctx, opStats.k.SnapshotID, backupDetails) if err != nil { opStats.writeErr = err // todo: ^ we're not persisting this yet, except for the error shown to the user. } }() // retrieve data from the producer gc, err := connector.NewGraphConnector(op.account) if err != nil { opStats.readErr = err return errors.Wrap(err, "connecting to graph api") } var cs []data.Collection cs, err = gc.ExchangeDataCollection(ctx, op.Selectors) if err != nil { opStats.readErr = err return errors.Wrap(err, "retrieving service data") } // hand the results to the consumer opStats.k, backupDetails, err = op.kopia.BackupCollections(ctx, cs) if err != nil { opStats.writeErr = err return errors.Wrap(err, "backing up service data") } opStats.gc = gc.AwaitStatus() return err } // writes the results metrics to the operation results. // later stored in the manifest using createBackupModels. func (op *BackupOperation) persistResults( started time.Time, opStats *backupStats, ) { op.Status = Completed if opStats.k.TotalFileCount == 0 && (opStats.readErr != nil || opStats.writeErr != nil) { op.Status = Failed } op.Results.ReadErrors = opStats.readErr op.Results.WriteErrors = opStats.writeErr if opStats.gc != nil { op.Results.ItemsRead = opStats.gc.Successful } if opStats.k != nil { op.Results.ItemsWritten = opStats.k.TotalFileCount } op.Results.StartedAt = started op.Results.CompletedAt = time.Now() } // stores the operation details, results, and selectors in the backup manifest. func (op *BackupOperation) createBackupModels( ctx context.Context, snapID string, backupDetails *details.Details, ) error { err := op.store.Put(ctx, model.BackupDetailsSchema, &backupDetails.DetailsModel) if err != nil { return errors.Wrap(err, "creating backupdetails model") } b := backup.New( snapID, string(backupDetails.ModelStoreID), op.Status.String(), op.Selectors, op.Results.ReadWrites, op.Results.StartAndEndTime, ) err = op.store.Put(ctx, model.BackupSchema, b) if err != nil { return errors.Wrap(err, "creating backup model") } op.Results.BackupID = b.ID return nil }