add generic details command (#4352)
centralizes details command processing in the cli --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🧹 Tech Debt/Cleanup #### Issue(s) * #2025
This commit is contained in:
parent
5eaf95052d
commit
b15f8a6fcd
@ -16,6 +16,8 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
"github.com/alcionai/corso/src/internal/m365/graph"
|
"github.com/alcionai/corso/src/internal/m365/graph"
|
||||||
"github.com/alcionai/corso/src/pkg/backup"
|
"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/logger"
|
"github.com/alcionai/corso/src/pkg/logger"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
"github.com/alcionai/corso/src/pkg/repository"
|
"github.com/alcionai/corso/src/pkg/repository"
|
||||||
@ -163,7 +165,7 @@ func handleDeleteCmd(cmd *cobra.Command, args []string) error {
|
|||||||
// standard set of selector behavior that we want used in the cli
|
// standard set of selector behavior that we want used in the cli
|
||||||
var defaultSelectorConfig = selectors.Config{OnlyMatchItemNames: true}
|
var defaultSelectorConfig = selectors.Config{OnlyMatchItemNames: true}
|
||||||
|
|
||||||
func runBackups(
|
func genericCreateCommand(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
r repository.Repositoryer,
|
r repository.Repositoryer,
|
||||||
serviceName string,
|
serviceName string,
|
||||||
@ -332,6 +334,65 @@ func genericListCommand(
|
|||||||
return nil
|
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 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 {
|
func ifShow(flag string) bool {
|
||||||
return strings.ToLower(strings.TrimSpace(flag)) == "show"
|
return strings.ToLower(strings.TrimSpace(flag)) == "show"
|
||||||
}
|
}
|
||||||
|
|||||||
68
src/cli/backup/backup_test.go
Normal file
68
src/cli/backup/backup_test.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package backup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/cli/utils/testdata"
|
||||||
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
|
dtd "github.com/alcionai/corso/src/pkg/backup/details/testdata"
|
||||||
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BackupUnitSuite struct {
|
||||||
|
tester.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackupUnitSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &BackupUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupUnitSuite) TestGenericDetailsCore() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
expected := append(
|
||||||
|
append(
|
||||||
|
dtd.GetItemsForVersion(
|
||||||
|
t,
|
||||||
|
path.ExchangeService,
|
||||||
|
path.EmailCategory,
|
||||||
|
0,
|
||||||
|
-1),
|
||||||
|
dtd.GetItemsForVersion(
|
||||||
|
t,
|
||||||
|
path.ExchangeService,
|
||||||
|
path.EventsCategory,
|
||||||
|
0,
|
||||||
|
-1)...),
|
||||||
|
dtd.GetItemsForVersion(
|
||||||
|
t,
|
||||||
|
path.ExchangeService,
|
||||||
|
path.ContactsCategory,
|
||||||
|
0,
|
||||||
|
-1)...)
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
bg := testdata.VersionedBackupGetter{
|
||||||
|
Details: dtd.GetDetailsSetForVersion(t, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
sel := selectors.NewExchangeBackup([]string{"user-id"})
|
||||||
|
sel.Include(sel.AllData())
|
||||||
|
|
||||||
|
output, err := genericDetailsCore(
|
||||||
|
ctx,
|
||||||
|
bg,
|
||||||
|
"backup-ID",
|
||||||
|
sel.Selector,
|
||||||
|
control.DefaultOptions())
|
||||||
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
|
assert.ElementsMatch(t, expected, output.Entries)
|
||||||
|
}
|
||||||
@ -1,21 +1,15 @@
|
|||||||
package backup
|
package backup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/cli/flags"
|
"github.com/alcionai/corso/src/cli/flags"
|
||||||
. "github.com/alcionai/corso/src/cli/print"
|
. "github.com/alcionai/corso/src/cli/print"
|
||||||
"github.com/alcionai/corso/src/cli/utils"
|
"github.com/alcionai/corso/src/cli/utils"
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
|
||||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"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/selectors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -182,7 +176,7 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error {
|
|||||||
selectorSet = append(selectorSet, discSel.Selector)
|
selectorSet = append(selectorSet, discSel.Selector)
|
||||||
}
|
}
|
||||||
|
|
||||||
return runBackups(
|
return genericCreateCommand(
|
||||||
ctx,
|
ctx,
|
||||||
r,
|
r,
|
||||||
"Exchange",
|
"Exchange",
|
||||||
@ -272,74 +266,31 @@ func detailsExchangeCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return runDetailsExchangeCmd(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDetailsExchangeCmd(cmd *cobra.Command) error {
|
||||||
ctx := cmd.Context()
|
ctx := cmd.Context()
|
||||||
opts := utils.MakeExchangeOpts(cmd)
|
opts := utils.MakeExchangeOpts(cmd)
|
||||||
|
|
||||||
r, rdao, err := utils.GetAccountAndConnect(ctx, cmd, path.ExchangeService)
|
sel := utils.IncludeExchangeRestoreDataSelectors(opts)
|
||||||
|
sel.Configure(selectors.Config{OnlyMatchItemNames: true})
|
||||||
|
utils.FilterExchangeRestoreInfoSelectors(sel, opts)
|
||||||
|
|
||||||
|
ds, err := genericDetailsCommand(cmd, flags.BackupIDFV, sel.Selector)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Only(ctx, err)
|
return Only(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer utils.CloseRepo(ctx, r)
|
if len(ds.Entries) > 0 {
|
||||||
|
ds.PrintEntries(ctx)
|
||||||
ds, err := runDetailsExchangeCmd(
|
} else {
|
||||||
ctx,
|
|
||||||
r,
|
|
||||||
flags.BackupIDFV,
|
|
||||||
opts,
|
|
||||||
rdao.Opts.SkipReduce)
|
|
||||||
if err != nil {
|
|
||||||
return Only(ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ds.Entries) == 0 {
|
|
||||||
Info(ctx, selectors.ErrorNoMatchingItems)
|
Info(ctx, selectors.ErrorNoMatchingItems)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ds.PrintEntries(ctx)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// runDetailsExchangeCmd actually performs the lookup in backup details.
|
|
||||||
// the fault.Errors return is always non-nil. Callers should check if
|
|
||||||
// errs.Failure() == nil.
|
|
||||||
func runDetailsExchangeCmd(
|
|
||||||
ctx context.Context,
|
|
||||||
r repository.BackupGetter,
|
|
||||||
backupID string,
|
|
||||||
opts utils.ExchangeOpts,
|
|
||||||
skipReduce bool,
|
|
||||||
) (*details.Details, error) {
|
|
||||||
if err := utils.ValidateExchangeRestoreFlags(backupID, opts); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = clues.Add(ctx, "backup_id", backupID)
|
|
||||||
|
|
||||||
d, _, errs := r.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")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = clues.Add(ctx, "details_entries", len(d.Entries))
|
|
||||||
|
|
||||||
if !skipReduce {
|
|
||||||
sel := utils.IncludeExchangeRestoreDataSelectors(opts)
|
|
||||||
sel.Configure(selectors.Config{OnlyMatchItemNames: true})
|
|
||||||
utils.FilterExchangeRestoreInfoSelectors(sel, opts)
|
|
||||||
d = sel.Reduce(ctx, d, errs)
|
|
||||||
}
|
|
||||||
|
|
||||||
return d, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
// backup delete
|
// backup delete
|
||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package backup
|
package backup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -15,10 +14,7 @@ import (
|
|||||||
flagsTD "github.com/alcionai/corso/src/cli/flags/testdata"
|
flagsTD "github.com/alcionai/corso/src/cli/flags/testdata"
|
||||||
cliTD "github.com/alcionai/corso/src/cli/testdata"
|
cliTD "github.com/alcionai/corso/src/cli/testdata"
|
||||||
"github.com/alcionai/corso/src/cli/utils"
|
"github.com/alcionai/corso/src/cli/utils"
|
||||||
utilsTD "github.com/alcionai/corso/src/cli/utils/testdata"
|
|
||||||
"github.com/alcionai/corso/src/internal/tester"
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
"github.com/alcionai/corso/src/internal/version"
|
|
||||||
dtd "github.com/alcionai/corso/src/pkg/backup/details/testdata"
|
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -368,51 +364,3 @@ func (suite *ExchangeUnitSuite) TestExchangeBackupCreateSelectors() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ExchangeUnitSuite) TestExchangeBackupDetailsSelectors() {
|
|
||||||
for v := 0; v <= version.Backup; v++ {
|
|
||||||
suite.Run(fmt.Sprintf("version%d", v), func() {
|
|
||||||
for _, test := range utilsTD.ExchangeOptionDetailLookups {
|
|
||||||
suite.Run(test.Name, func() {
|
|
||||||
t := suite.T()
|
|
||||||
|
|
||||||
ctx, flush := tester.NewContext(t)
|
|
||||||
defer flush()
|
|
||||||
|
|
||||||
bg := utilsTD.VersionedBackupGetter{
|
|
||||||
Details: dtd.GetDetailsSetForVersion(t, v),
|
|
||||||
}
|
|
||||||
|
|
||||||
output, err := runDetailsExchangeCmd(
|
|
||||||
ctx,
|
|
||||||
bg,
|
|
||||||
"backup-ID",
|
|
||||||
test.Opts(t, v),
|
|
||||||
false)
|
|
||||||
assert.NoError(t, err, clues.ToCore(err))
|
|
||||||
assert.ElementsMatch(t, test.Expected(t, v), output.Entries)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *ExchangeUnitSuite) TestExchangeBackupDetailsSelectorsBadFormats() {
|
|
||||||
for _, test := range utilsTD.BadExchangeOptionsFormats {
|
|
||||||
suite.Run(test.Name, func() {
|
|
||||||
t := suite.T()
|
|
||||||
|
|
||||||
ctx, flush := tester.NewContext(t)
|
|
||||||
defer flush()
|
|
||||||
|
|
||||||
output, err := runDetailsExchangeCmd(
|
|
||||||
ctx,
|
|
||||||
test.BackupGetter,
|
|
||||||
"backup-ID",
|
|
||||||
test.Opts(t, version.Backup),
|
|
||||||
false)
|
|
||||||
assert.Error(t, err, clues.ToCore(err))
|
|
||||||
assert.Empty(t, output)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package backup
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
@ -14,12 +13,9 @@ import (
|
|||||||
. "github.com/alcionai/corso/src/cli/print"
|
. "github.com/alcionai/corso/src/cli/print"
|
||||||
"github.com/alcionai/corso/src/cli/utils"
|
"github.com/alcionai/corso/src/cli/utils"
|
||||||
"github.com/alcionai/corso/src/internal/common/idname"
|
"github.com/alcionai/corso/src/internal/common/idname"
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
|
||||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/filters"
|
"github.com/alcionai/corso/src/pkg/filters"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"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/selectors"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365"
|
"github.com/alcionai/corso/src/pkg/services/m365"
|
||||||
)
|
)
|
||||||
@ -174,7 +170,7 @@ func createGroupsCmd(cmd *cobra.Command, args []string) error {
|
|||||||
selectorSet = append(selectorSet, discSel.Selector)
|
selectorSet = append(selectorSet, discSel.Selector)
|
||||||
}
|
}
|
||||||
|
|
||||||
return runBackups(
|
return genericCreateCommand(
|
||||||
ctx,
|
ctx,
|
||||||
r,
|
r,
|
||||||
"Group",
|
"Group",
|
||||||
@ -225,74 +221,31 @@ func detailsGroupsCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return runDetailsGroupsCmd(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDetailsGroupsCmd(cmd *cobra.Command) error {
|
||||||
ctx := cmd.Context()
|
ctx := cmd.Context()
|
||||||
opts := utils.MakeGroupsOpts(cmd)
|
opts := utils.MakeGroupsOpts(cmd)
|
||||||
|
|
||||||
r, rdao, err := utils.GetAccountAndConnect(ctx, cmd, path.GroupsService)
|
sel := utils.IncludeGroupsRestoreDataSelectors(ctx, opts)
|
||||||
|
sel.Configure(selectors.Config{OnlyMatchItemNames: true})
|
||||||
|
utils.FilterGroupsRestoreInfoSelectors(sel, opts)
|
||||||
|
|
||||||
|
ds, err := genericDetailsCommand(cmd, flags.BackupIDFV, sel.Selector)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Only(ctx, err)
|
return Only(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer utils.CloseRepo(ctx, r)
|
if len(ds.Entries) > 0 {
|
||||||
|
ds.PrintEntries(ctx)
|
||||||
ds, err := runDetailsGroupsCmd(
|
} else {
|
||||||
ctx,
|
|
||||||
r,
|
|
||||||
flags.BackupIDFV,
|
|
||||||
opts,
|
|
||||||
rdao.Opts.SkipReduce)
|
|
||||||
if err != nil {
|
|
||||||
return Only(ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ds.Entries) == 0 {
|
|
||||||
Info(ctx, selectors.ErrorNoMatchingItems)
|
Info(ctx, selectors.ErrorNoMatchingItems)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ds.PrintEntries(ctx)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// runDetailsGroupsCmd actually performs the lookup in backup details.
|
|
||||||
// the fault.Errors return is always non-nil. Callers should check if
|
|
||||||
// errs.Failure() == nil.
|
|
||||||
func runDetailsGroupsCmd(
|
|
||||||
ctx context.Context,
|
|
||||||
r repository.BackupGetter,
|
|
||||||
backupID string,
|
|
||||||
opts utils.GroupsOpts,
|
|
||||||
skipReduce bool,
|
|
||||||
) (*details.Details, error) {
|
|
||||||
if err := utils.ValidateGroupsRestoreFlags(backupID, opts); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = clues.Add(ctx, "backup_id", backupID)
|
|
||||||
|
|
||||||
d, _, errs := r.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")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = clues.Add(ctx, "details_entries", len(d.Entries))
|
|
||||||
|
|
||||||
if !skipReduce {
|
|
||||||
sel := utils.IncludeGroupsRestoreDataSelectors(ctx, opts)
|
|
||||||
sel.Configure(selectors.Config{OnlyMatchItemNames: true})
|
|
||||||
utils.FilterGroupsRestoreInfoSelectors(sel, opts)
|
|
||||||
d = sel.Reduce(ctx, d, errs)
|
|
||||||
}
|
|
||||||
|
|
||||||
return d, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
// backup delete
|
// backup delete
|
||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
||||||
"github.com/alcionai/corso/src/pkg/account"
|
"github.com/alcionai/corso/src/pkg/account"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
ctrlRepo "github.com/alcionai/corso/src/pkg/control/repository"
|
|
||||||
"github.com/alcionai/corso/src/pkg/repository"
|
"github.com/alcionai/corso/src/pkg/repository"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/mock"
|
"github.com/alcionai/corso/src/pkg/services/m365/api/mock"
|
||||||
@ -160,7 +159,7 @@ func prepM365Test(
|
|||||||
repository.NewRepoID)
|
repository.NewRepoID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
err = repo.Initialize(ctx, ctrlRepo.Retention{})
|
err = repo.Initialize(ctx, repository.InitConfig{})
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
return dependencies{
|
return dependencies{
|
||||||
|
|||||||
@ -1,21 +1,15 @@
|
|||||||
package backup
|
package backup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/cli/flags"
|
"github.com/alcionai/corso/src/cli/flags"
|
||||||
. "github.com/alcionai/corso/src/cli/print"
|
. "github.com/alcionai/corso/src/cli/print"
|
||||||
"github.com/alcionai/corso/src/cli/utils"
|
"github.com/alcionai/corso/src/cli/utils"
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
|
||||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"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/selectors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -162,7 +156,7 @@ func createOneDriveCmd(cmd *cobra.Command, args []string) error {
|
|||||||
selectorSet = append(selectorSet, discSel.Selector)
|
selectorSet = append(selectorSet, discSel.Selector)
|
||||||
}
|
}
|
||||||
|
|
||||||
return runBackups(
|
return genericCreateCommand(
|
||||||
ctx,
|
ctx,
|
||||||
r,
|
r,
|
||||||
"OneDrive",
|
"OneDrive",
|
||||||
@ -229,74 +223,31 @@ func detailsOneDriveCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return runDetailsOneDriveCmd(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDetailsOneDriveCmd(cmd *cobra.Command) error {
|
||||||
ctx := cmd.Context()
|
ctx := cmd.Context()
|
||||||
opts := utils.MakeOneDriveOpts(cmd)
|
opts := utils.MakeOneDriveOpts(cmd)
|
||||||
|
|
||||||
r, rdao, err := utils.GetAccountAndConnect(ctx, cmd, path.OneDriveService)
|
sel := utils.IncludeOneDriveRestoreDataSelectors(opts)
|
||||||
|
sel.Configure(selectors.Config{OnlyMatchItemNames: true})
|
||||||
|
utils.FilterOneDriveRestoreInfoSelectors(sel, opts)
|
||||||
|
|
||||||
|
ds, err := genericDetailsCommand(cmd, flags.BackupIDFV, sel.Selector)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Only(ctx, err)
|
return Only(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer utils.CloseRepo(ctx, r)
|
if len(ds.Entries) > 0 {
|
||||||
|
ds.PrintEntries(ctx)
|
||||||
ds, err := runDetailsOneDriveCmd(
|
} else {
|
||||||
ctx,
|
|
||||||
r,
|
|
||||||
flags.BackupIDFV,
|
|
||||||
opts,
|
|
||||||
rdao.Opts.SkipReduce)
|
|
||||||
if err != nil {
|
|
||||||
return Only(ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ds.Entries) == 0 {
|
|
||||||
Info(ctx, selectors.ErrorNoMatchingItems)
|
Info(ctx, selectors.ErrorNoMatchingItems)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ds.PrintEntries(ctx)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// runDetailsOneDriveCmd actually performs the lookup in backup details.
|
|
||||||
// the fault.Errors return is always non-nil. Callers should check if
|
|
||||||
// errs.Failure() == nil.
|
|
||||||
func runDetailsOneDriveCmd(
|
|
||||||
ctx context.Context,
|
|
||||||
r repository.BackupGetter,
|
|
||||||
backupID string,
|
|
||||||
opts utils.OneDriveOpts,
|
|
||||||
skipReduce bool,
|
|
||||||
) (*details.Details, error) {
|
|
||||||
if err := utils.ValidateOneDriveRestoreFlags(backupID, opts); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = clues.Add(ctx, "backup_id", backupID)
|
|
||||||
|
|
||||||
d, _, errs := r.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")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = clues.Add(ctx, "details_entries", len(d.Entries))
|
|
||||||
|
|
||||||
if !skipReduce {
|
|
||||||
sel := utils.IncludeOneDriveRestoreDataSelectors(opts)
|
|
||||||
sel.Configure(selectors.Config{OnlyMatchItemNames: true})
|
|
||||||
utils.FilterOneDriveRestoreInfoSelectors(sel, opts)
|
|
||||||
d = sel.Reduce(ctx, d, errs)
|
|
||||||
}
|
|
||||||
|
|
||||||
return d, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// `corso backup delete onedrive [<flag>...]`
|
// `corso backup delete onedrive [<flag>...]`
|
||||||
func oneDriveDeleteCmd() *cobra.Command {
|
func oneDriveDeleteCmd() *cobra.Command {
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package backup
|
package backup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
@ -14,10 +13,7 @@ import (
|
|||||||
flagsTD "github.com/alcionai/corso/src/cli/flags/testdata"
|
flagsTD "github.com/alcionai/corso/src/cli/flags/testdata"
|
||||||
cliTD "github.com/alcionai/corso/src/cli/testdata"
|
cliTD "github.com/alcionai/corso/src/cli/testdata"
|
||||||
"github.com/alcionai/corso/src/cli/utils"
|
"github.com/alcionai/corso/src/cli/utils"
|
||||||
utilsTD "github.com/alcionai/corso/src/cli/utils/testdata"
|
|
||||||
"github.com/alcionai/corso/src/internal/tester"
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
"github.com/alcionai/corso/src/internal/version"
|
|
||||||
dtd "github.com/alcionai/corso/src/pkg/backup/details/testdata"
|
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -227,51 +223,3 @@ func (suite *OneDriveUnitSuite) TestValidateOneDriveBackupCreateFlags() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *OneDriveUnitSuite) TestOneDriveBackupDetailsSelectors() {
|
|
||||||
for v := 0; v <= version.Backup; v++ {
|
|
||||||
suite.Run(fmt.Sprintf("version%d", v), func() {
|
|
||||||
for _, test := range utilsTD.OneDriveOptionDetailLookups {
|
|
||||||
suite.Run(test.Name, func() {
|
|
||||||
t := suite.T()
|
|
||||||
|
|
||||||
ctx, flush := tester.NewContext(t)
|
|
||||||
defer flush()
|
|
||||||
|
|
||||||
bg := utilsTD.VersionedBackupGetter{
|
|
||||||
Details: dtd.GetDetailsSetForVersion(t, v),
|
|
||||||
}
|
|
||||||
|
|
||||||
output, err := runDetailsOneDriveCmd(
|
|
||||||
ctx,
|
|
||||||
bg,
|
|
||||||
"backup-ID",
|
|
||||||
test.Opts(t, v),
|
|
||||||
false)
|
|
||||||
assert.NoError(t, err, clues.ToCore(err))
|
|
||||||
assert.ElementsMatch(t, test.Expected(t, v), output.Entries)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *OneDriveUnitSuite) TestOneDriveBackupDetailsSelectorsBadFormats() {
|
|
||||||
for _, test := range utilsTD.BadOneDriveOptionsFormats {
|
|
||||||
suite.Run(test.Name, func() {
|
|
||||||
t := suite.T()
|
|
||||||
|
|
||||||
ctx, flush := tester.NewContext(t)
|
|
||||||
defer flush()
|
|
||||||
|
|
||||||
output, err := runDetailsOneDriveCmd(
|
|
||||||
ctx,
|
|
||||||
test.BackupGetter,
|
|
||||||
"backup-ID",
|
|
||||||
test.Opts(t, version.Backup),
|
|
||||||
false)
|
|
||||||
assert.Error(t, err, clues.ToCore(err))
|
|
||||||
assert.Empty(t, output)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
@ -13,12 +12,9 @@ import (
|
|||||||
. "github.com/alcionai/corso/src/cli/print"
|
. "github.com/alcionai/corso/src/cli/print"
|
||||||
"github.com/alcionai/corso/src/cli/utils"
|
"github.com/alcionai/corso/src/cli/utils"
|
||||||
"github.com/alcionai/corso/src/internal/common/idname"
|
"github.com/alcionai/corso/src/internal/common/idname"
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
|
||||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/filters"
|
"github.com/alcionai/corso/src/pkg/filters"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"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/selectors"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365"
|
"github.com/alcionai/corso/src/pkg/services/m365"
|
||||||
)
|
)
|
||||||
@ -179,7 +175,7 @@ func createSharePointCmd(cmd *cobra.Command, args []string) error {
|
|||||||
selectorSet = append(selectorSet, discSel.Selector)
|
selectorSet = append(selectorSet, discSel.Selector)
|
||||||
}
|
}
|
||||||
|
|
||||||
return runBackups(
|
return genericCreateCommand(
|
||||||
ctx,
|
ctx,
|
||||||
r,
|
r,
|
||||||
"SharePoint",
|
"SharePoint",
|
||||||
@ -303,7 +299,7 @@ func deleteSharePointCmd(cmd *cobra.Command, args []string) error {
|
|||||||
// backup details
|
// backup details
|
||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// `corso backup details onedrive [<flag>...]`
|
// `corso backup details SharePoint [<flag>...]`
|
||||||
func sharePointDetailsCmd() *cobra.Command {
|
func sharePointDetailsCmd() *cobra.Command {
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
Use: sharePointServiceCommand,
|
Use: sharePointServiceCommand,
|
||||||
@ -324,70 +320,27 @@ func detailsSharePointCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return runDetailsSharePointCmd(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDetailsSharePointCmd(cmd *cobra.Command) error {
|
||||||
ctx := cmd.Context()
|
ctx := cmd.Context()
|
||||||
opts := utils.MakeSharePointOpts(cmd)
|
opts := utils.MakeSharePointOpts(cmd)
|
||||||
|
|
||||||
r, rdao, err := utils.GetAccountAndConnect(ctx, cmd, path.SharePointService)
|
sel := utils.IncludeSharePointRestoreDataSelectors(ctx, opts)
|
||||||
|
sel.Configure(selectors.Config{OnlyMatchItemNames: true})
|
||||||
|
utils.FilterSharePointRestoreInfoSelectors(sel, opts)
|
||||||
|
|
||||||
|
ds, err := genericDetailsCommand(cmd, flags.BackupIDFV, sel.Selector)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Only(ctx, err)
|
return Only(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer utils.CloseRepo(ctx, r)
|
if len(ds.Entries) > 0 {
|
||||||
|
ds.PrintEntries(ctx)
|
||||||
ds, err := runDetailsSharePointCmd(
|
} else {
|
||||||
ctx,
|
|
||||||
r,
|
|
||||||
flags.BackupIDFV,
|
|
||||||
opts,
|
|
||||||
rdao.Opts.SkipReduce)
|
|
||||||
if err != nil {
|
|
||||||
return Only(ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ds.Entries) == 0 {
|
|
||||||
Info(ctx, selectors.ErrorNoMatchingItems)
|
Info(ctx, selectors.ErrorNoMatchingItems)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ds.PrintEntries(ctx)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// runDetailsSharePointCmd actually performs the lookup in backup details.
|
|
||||||
// the fault.Errors return is always non-nil. Callers should check if
|
|
||||||
// errs.Failure() == nil.
|
|
||||||
func runDetailsSharePointCmd(
|
|
||||||
ctx context.Context,
|
|
||||||
r repository.BackupGetter,
|
|
||||||
backupID string,
|
|
||||||
opts utils.SharePointOpts,
|
|
||||||
skipReduce bool,
|
|
||||||
) (*details.Details, error) {
|
|
||||||
if err := utils.ValidateSharePointRestoreFlags(backupID, opts); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = clues.Add(ctx, "backup_id", backupID)
|
|
||||||
|
|
||||||
d, _, errs := r.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")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = clues.Add(ctx, "details_entries", len(d.Entries))
|
|
||||||
|
|
||||||
if !skipReduce {
|
|
||||||
sel := utils.IncludeSharePointRestoreDataSelectors(ctx, opts)
|
|
||||||
sel.Configure(selectors.Config{OnlyMatchItemNames: true})
|
|
||||||
utils.FilterSharePointRestoreInfoSelectors(sel, opts)
|
|
||||||
d = sel.Reduce(ctx, d, errs)
|
|
||||||
}
|
|
||||||
|
|
||||||
return d, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package backup
|
package backup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -15,11 +14,8 @@ import (
|
|||||||
flagsTD "github.com/alcionai/corso/src/cli/flags/testdata"
|
flagsTD "github.com/alcionai/corso/src/cli/flags/testdata"
|
||||||
cliTD "github.com/alcionai/corso/src/cli/testdata"
|
cliTD "github.com/alcionai/corso/src/cli/testdata"
|
||||||
"github.com/alcionai/corso/src/cli/utils"
|
"github.com/alcionai/corso/src/cli/utils"
|
||||||
utilsTD "github.com/alcionai/corso/src/cli/utils/testdata"
|
|
||||||
"github.com/alcionai/corso/src/internal/common/idname"
|
"github.com/alcionai/corso/src/internal/common/idname"
|
||||||
"github.com/alcionai/corso/src/internal/tester"
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
"github.com/alcionai/corso/src/internal/version"
|
|
||||||
dtd "github.com/alcionai/corso/src/pkg/backup/details/testdata"
|
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
"github.com/alcionai/corso/src/pkg/selectors"
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
)
|
)
|
||||||
@ -339,51 +335,3 @@ func (suite *SharePointUnitSuite) TestSharePointBackupCreateSelectors() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *SharePointUnitSuite) TestSharePointBackupDetailsSelectors() {
|
|
||||||
for v := 0; v <= version.Backup; v++ {
|
|
||||||
suite.Run(fmt.Sprintf("version%d", v), func() {
|
|
||||||
for _, test := range utilsTD.SharePointOptionDetailLookups {
|
|
||||||
suite.Run(test.Name, func() {
|
|
||||||
t := suite.T()
|
|
||||||
|
|
||||||
ctx, flush := tester.NewContext(t)
|
|
||||||
defer flush()
|
|
||||||
|
|
||||||
bg := utilsTD.VersionedBackupGetter{
|
|
||||||
Details: dtd.GetDetailsSetForVersion(t, v),
|
|
||||||
}
|
|
||||||
|
|
||||||
output, err := runDetailsSharePointCmd(
|
|
||||||
ctx,
|
|
||||||
bg,
|
|
||||||
"backup-ID",
|
|
||||||
test.Opts(t, v),
|
|
||||||
false)
|
|
||||||
assert.NoError(t, err, clues.ToCore(err))
|
|
||||||
assert.ElementsMatch(t, test.Expected(t, v), output.Entries)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *SharePointUnitSuite) TestSharePointBackupDetailsSelectorsBadFormats() {
|
|
||||||
for _, test := range utilsTD.BadSharePointOptionsFormats {
|
|
||||||
suite.Run(test.Name, func() {
|
|
||||||
t := suite.T()
|
|
||||||
|
|
||||||
ctx, flush := tester.NewContext(t)
|
|
||||||
defer flush()
|
|
||||||
|
|
||||||
output, err := runDetailsSharePointCmd(
|
|
||||||
ctx,
|
|
||||||
test.BackupGetter,
|
|
||||||
"backup-ID",
|
|
||||||
test.Opts(t, version.Backup),
|
|
||||||
false)
|
|
||||||
assert.Error(t, err, clues.ToCore(err))
|
|
||||||
assert.Empty(t, output)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -85,7 +85,7 @@ func initFilesystemCmd(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
opt := utils.ControlWithConfig(cfg)
|
opt := utils.ControlWithConfig(cfg)
|
||||||
// Retention is not supported for filesystem repos.
|
// Retention is not supported for filesystem repos.
|
||||||
retention := ctrlRepo.Retention{}
|
retentionOpts := ctrlRepo.Retention{}
|
||||||
|
|
||||||
// SendStartCorsoEvent uses distict ID as tenant ID because repoID is still not generated
|
// SendStartCorsoEvent uses distict ID as tenant ID because repoID is still not generated
|
||||||
utils.SendStartCorsoEvent(
|
utils.SendStartCorsoEvent(
|
||||||
@ -116,7 +116,9 @@ func initFilesystemCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return Only(ctx, clues.Wrap(err, "Failed to construct the repository controller"))
|
return Only(ctx, clues.Wrap(err, "Failed to construct the repository controller"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = r.Initialize(ctx, retention); err != nil {
|
ric := repository.InitConfig{RetentionOpts: retentionOpts}
|
||||||
|
|
||||||
|
if err = r.Initialize(ctx, ric); err != nil {
|
||||||
if flags.SucceedIfExistsFV && errors.Is(err, repository.ErrorRepoAlreadyExists) {
|
if flags.SucceedIfExistsFV && errors.Is(err, repository.ErrorRepoAlreadyExists) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -207,7 +209,7 @@ func connectFilesystemCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return Only(ctx, clues.Wrap(err, "Failed to create a repository controller"))
|
return Only(ctx, clues.Wrap(err, "Failed to create a repository controller"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.Connect(ctx); err != nil {
|
if err := r.Connect(ctx, repository.ConnConfig{}); err != nil {
|
||||||
return Only(ctx, clues.Stack(ErrConnectingRepo, err))
|
return Only(ctx, clues.Stack(ErrConnectingRepo, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
||||||
"github.com/alcionai/corso/src/pkg/account"
|
"github.com/alcionai/corso/src/pkg/account"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
ctrlRepo "github.com/alcionai/corso/src/pkg/control/repository"
|
|
||||||
"github.com/alcionai/corso/src/pkg/repository"
|
"github.com/alcionai/corso/src/pkg/repository"
|
||||||
"github.com/alcionai/corso/src/pkg/storage"
|
"github.com/alcionai/corso/src/pkg/storage"
|
||||||
storeTD "github.com/alcionai/corso/src/pkg/storage/testdata"
|
storeTD "github.com/alcionai/corso/src/pkg/storage/testdata"
|
||||||
@ -138,7 +137,7 @@ func (suite *FilesystemE2ESuite) TestConnectFilesystemCmd() {
|
|||||||
repository.NewRepoID)
|
repository.NewRepoID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
err = r.Initialize(ctx, ctrlRepo.Retention{})
|
err = r.Initialize(ctx, repository.InitConfig{})
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
// then test it
|
// then test it
|
||||||
|
|||||||
@ -138,7 +138,9 @@ func initS3Cmd(cmd *cobra.Command, args []string) error {
|
|||||||
return Only(ctx, clues.Wrap(err, "Failed to construct the repository controller"))
|
return Only(ctx, clues.Wrap(err, "Failed to construct the repository controller"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = r.Initialize(ctx, retentionOpts); err != nil {
|
ric := repository.InitConfig{RetentionOpts: retentionOpts}
|
||||||
|
|
||||||
|
if err = r.Initialize(ctx, ric); err != nil {
|
||||||
if flags.SucceedIfExistsFV && errors.Is(err, repository.ErrorRepoAlreadyExists) {
|
if flags.SucceedIfExistsFV && errors.Is(err, repository.ErrorRepoAlreadyExists) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -221,7 +223,7 @@ func connectS3Cmd(cmd *cobra.Command, args []string) error {
|
|||||||
return Only(ctx, clues.Wrap(err, "Failed to create a repository controller"))
|
return Only(ctx, clues.Wrap(err, "Failed to create a repository controller"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.Connect(ctx); err != nil {
|
if err := r.Connect(ctx, repository.ConnConfig{}); err != nil {
|
||||||
return Only(ctx, clues.Stack(ErrConnectingRepo, err))
|
return Only(ctx, clues.Stack(ErrConnectingRepo, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,6 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
||||||
"github.com/alcionai/corso/src/pkg/account"
|
"github.com/alcionai/corso/src/pkg/account"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
ctrlRepo "github.com/alcionai/corso/src/pkg/control/repository"
|
|
||||||
"github.com/alcionai/corso/src/pkg/repository"
|
"github.com/alcionai/corso/src/pkg/repository"
|
||||||
"github.com/alcionai/corso/src/pkg/storage"
|
"github.com/alcionai/corso/src/pkg/storage"
|
||||||
storeTD "github.com/alcionai/corso/src/pkg/storage/testdata"
|
storeTD "github.com/alcionai/corso/src/pkg/storage/testdata"
|
||||||
@ -214,7 +213,7 @@ func (suite *S3E2ESuite) TestConnectS3Cmd() {
|
|||||||
repository.NewRepoID)
|
repository.NewRepoID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
err = r.Initialize(ctx, ctrlRepo.Retention{})
|
err = r.Initialize(ctx, repository.InitConfig{})
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
// then test it
|
// then test it
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
||||||
"github.com/alcionai/corso/src/pkg/account"
|
"github.com/alcionai/corso/src/pkg/account"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
ctrlRepo "github.com/alcionai/corso/src/pkg/control/repository"
|
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
"github.com/alcionai/corso/src/pkg/repository"
|
"github.com/alcionai/corso/src/pkg/repository"
|
||||||
"github.com/alcionai/corso/src/pkg/selectors"
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
@ -92,7 +91,7 @@ func (suite *RestoreExchangeE2ESuite) SetupSuite() {
|
|||||||
repository.NewRepoID)
|
repository.NewRepoID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
err = suite.repo.Initialize(ctx, ctrlRepo.Retention{})
|
err = suite.repo.Initialize(ctx, repository.InitConfig{Service: path.ExchangeService})
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
suite.backupOps = make(map[path.CategoryType]operations.BackupOperation)
|
suite.backupOps = make(map[path.CategoryType]operations.BackupOperation)
|
||||||
|
|||||||
@ -78,16 +78,10 @@ func GetAccountAndConnectWithOverrides(
|
|||||||
return nil, RepoDetailsAndOpts{}, clues.Wrap(err, "creating a repository controller")
|
return nil, RepoDetailsAndOpts{}, clues.Wrap(err, "creating a repository controller")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.Connect(ctx); err != nil {
|
if err := r.Connect(ctx, repository.ConnConfig{Service: pst}); err != nil {
|
||||||
return nil, RepoDetailsAndOpts{}, clues.Wrap(err, "connecting to the "+cfg.Storage.Provider.String()+" repository")
|
return nil, RepoDetailsAndOpts{}, clues.Wrap(err, "connecting to the "+cfg.Storage.Provider.String()+" repository")
|
||||||
}
|
}
|
||||||
|
|
||||||
// this initializes our graph api client configurations,
|
|
||||||
// including control options such as concurency limitations.
|
|
||||||
if _, err := r.ConnectToM365(ctx, pst); err != nil {
|
|
||||||
return nil, RepoDetailsAndOpts{}, clues.Wrap(err, "connecting to m365")
|
|
||||||
}
|
|
||||||
|
|
||||||
rdao := RepoDetailsAndOpts{
|
rdao := RepoDetailsAndOpts{
|
||||||
Repo: cfg,
|
Repo: cfg,
|
||||||
Opts: opts,
|
Opts: opts,
|
||||||
|
|||||||
@ -72,7 +72,7 @@ func deleteBackups(
|
|||||||
// Only supported for S3 repos currently.
|
// Only supported for S3 repos currently.
|
||||||
func pitrListBackups(
|
func pitrListBackups(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
service path.ServiceType,
|
pst path.ServiceType,
|
||||||
pitr time.Time,
|
pitr time.Time,
|
||||||
backupIDs []string,
|
backupIDs []string,
|
||||||
) error {
|
) error {
|
||||||
@ -113,14 +113,14 @@ func pitrListBackups(
|
|||||||
return clues.Wrap(err, "creating a repo")
|
return clues.Wrap(err, "creating a repo")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = r.Connect(ctx)
|
err = r.Connect(ctx, repository.ConnConfig{Service: pst})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return clues.Wrap(err, "connecting to the repository")
|
return clues.Wrap(err, "connecting to the repository")
|
||||||
}
|
}
|
||||||
|
|
||||||
defer r.Close(ctx)
|
defer r.Close(ctx)
|
||||||
|
|
||||||
backups, err := r.BackupsByTag(ctx, store.Service(service))
|
backups, err := r.BackupsByTag(ctx, store.Service(pst))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return clues.Wrap(err, "listing backups").WithClues(ctx)
|
return clues.Wrap(err, "listing backups").WithClues(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,20 +79,29 @@ func NewController(
|
|||||||
return nil, clues.Wrap(err, "creating api client").WithClues(ctx)
|
return nil, clues.Wrap(err, "creating api client").WithClues(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
rc := resource.UnknownResource
|
var rCli *resourceClient
|
||||||
|
|
||||||
switch pst {
|
// no failure for unknown service.
|
||||||
case path.ExchangeService, path.OneDriveService:
|
// In that case we create a controller that doesn't attempt to look up any resource
|
||||||
rc = resource.Users
|
// data. This case helps avoid unnecessary service calls when the end user is running
|
||||||
case path.GroupsService:
|
// repo init and connect commands via the CLI. All other callers should be expected
|
||||||
rc = resource.Groups
|
// to pass in a known service, or else expect downstream failures.
|
||||||
case path.SharePointService:
|
if pst != path.UnknownService {
|
||||||
rc = resource.Sites
|
rc := resource.UnknownResource
|
||||||
}
|
|
||||||
|
|
||||||
rCli, err := getResourceClient(rc, ac)
|
switch pst {
|
||||||
if err != nil {
|
case path.ExchangeService, path.OneDriveService:
|
||||||
return nil, clues.Wrap(err, "creating resource client").WithClues(ctx)
|
rc = resource.Users
|
||||||
|
case path.GroupsService:
|
||||||
|
rc = resource.Groups
|
||||||
|
case path.SharePointService:
|
||||||
|
rc = resource.Sites
|
||||||
|
}
|
||||||
|
|
||||||
|
rCli, err = getResourceClient(rc, ac)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.Wrap(err, "creating resource client").WithClues(ctx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctrl := Controller{
|
ctrl := Controller{
|
||||||
@ -110,6 +119,10 @@ func NewController(
|
|||||||
return &ctrl, nil
|
return &ctrl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctrl *Controller) VerifyAccess(ctx context.Context) error {
|
||||||
|
return ctrl.AC.Access().GetToken(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Processing Status
|
// Processing Status
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -195,7 +208,7 @@ func getResourceClient(rc resource.Category, ac api.Client) (*resourceClient, er
|
|||||||
case resource.Groups:
|
case resource.Groups:
|
||||||
return &resourceClient{enum: rc, getter: ac.Groups()}, nil
|
return &resourceClient{enum: rc, getter: ac.Groups()}, nil
|
||||||
default:
|
default:
|
||||||
return nil, clues.New("unrecognized owner resource enum").With("resource_enum", rc)
|
return nil, clues.New("unrecognized owner resource type").With("resource_enum", rc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,9 +15,9 @@ var ErrorUnknownService = clues.New("unknown service string")
|
|||||||
// Metadata services are not considered valid service types for resource paths
|
// Metadata services are not considered valid service types for resource paths
|
||||||
// though they can be used for metadata paths.
|
// though they can be used for metadata paths.
|
||||||
//
|
//
|
||||||
// The order of the enums below can be changed, but the string representation of
|
// The string representaton of each enum _must remain the same_. In case of
|
||||||
// each enum must remain the same or migration code needs to be added to handle
|
// changes to those values, we'll need migration code to handle transitions
|
||||||
// changes to the string format.
|
// across states else we'll get marshalling/unmarshalling errors.
|
||||||
type ServiceType int
|
type ServiceType int
|
||||||
|
|
||||||
//go:generate stringer -type=ServiceType -linecomment
|
//go:generate stringer -type=ServiceType -linecomment
|
||||||
|
|||||||
359
src/pkg/repository/backups.go
Normal file
359
src/pkg/repository/backups.go
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/kopia/kopia/repo/manifest"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/idname"
|
||||||
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
|
"github.com/alcionai/corso/src/internal/kopia"
|
||||||
|
"github.com/alcionai/corso/src/internal/m365/collection/drive/metadata"
|
||||||
|
"github.com/alcionai/corso/src/internal/model"
|
||||||
|
"github.com/alcionai/corso/src/internal/operations"
|
||||||
|
"github.com/alcionai/corso/src/internal/streamstore"
|
||||||
|
"github.com/alcionai/corso/src/internal/version"
|
||||||
|
"github.com/alcionai/corso/src/pkg/backup"
|
||||||
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||||
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
|
"github.com/alcionai/corso/src/pkg/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BackupGetter deals with retrieving metadata about backups from the
|
||||||
|
// repository.
|
||||||
|
type BackupGetter interface {
|
||||||
|
Backup(ctx context.Context, id string) (*backup.Backup, error)
|
||||||
|
Backups(ctx context.Context, ids []string) ([]*backup.Backup, *fault.Bus)
|
||||||
|
BackupsByTag(ctx context.Context, fs ...store.FilterOption) ([]*backup.Backup, error)
|
||||||
|
GetBackupDetails(
|
||||||
|
ctx context.Context,
|
||||||
|
backupID string,
|
||||||
|
) (*details.Details, *backup.Backup, *fault.Bus)
|
||||||
|
GetBackupErrors(
|
||||||
|
ctx context.Context,
|
||||||
|
backupID string,
|
||||||
|
) (*fault.Errors, *backup.Backup, *fault.Bus)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Backuper interface {
|
||||||
|
NewBackup(
|
||||||
|
ctx context.Context,
|
||||||
|
self selectors.Selector,
|
||||||
|
) (operations.BackupOperation, error)
|
||||||
|
NewBackupWithLookup(
|
||||||
|
ctx context.Context,
|
||||||
|
self selectors.Selector,
|
||||||
|
ins idname.Cacher,
|
||||||
|
) (operations.BackupOperation, error)
|
||||||
|
DeleteBackups(
|
||||||
|
ctx context.Context,
|
||||||
|
failOnMissing bool,
|
||||||
|
ids ...string,
|
||||||
|
) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBackup generates a BackupOperation runner.
|
||||||
|
func (r repository) NewBackup(
|
||||||
|
ctx context.Context,
|
||||||
|
sel selectors.Selector,
|
||||||
|
) (operations.BackupOperation, error) {
|
||||||
|
return r.NewBackupWithLookup(ctx, sel, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBackupWithLookup generates a BackupOperation runner.
|
||||||
|
// ownerIDToName and ownerNameToID are optional populations, in case the caller has
|
||||||
|
// already generated those values.
|
||||||
|
func (r repository) NewBackupWithLookup(
|
||||||
|
ctx context.Context,
|
||||||
|
sel selectors.Selector,
|
||||||
|
ins idname.Cacher,
|
||||||
|
) (operations.BackupOperation, error) {
|
||||||
|
err := r.ConnectDataProvider(ctx, sel.PathService())
|
||||||
|
if err != nil {
|
||||||
|
return operations.BackupOperation{}, clues.Wrap(err, "connecting to m365")
|
||||||
|
}
|
||||||
|
|
||||||
|
ownerID, ownerName, err := r.Provider.PopulateProtectedResourceIDAndName(ctx, sel.DiscreteOwner, ins)
|
||||||
|
if err != nil {
|
||||||
|
return operations.BackupOperation{}, clues.Wrap(err, "resolving resource owner details")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: retrieve display name from gc
|
||||||
|
sel = sel.SetDiscreteOwnerIDName(ownerID, ownerName)
|
||||||
|
|
||||||
|
return operations.NewBackupOperation(
|
||||||
|
ctx,
|
||||||
|
r.Opts,
|
||||||
|
r.dataLayer,
|
||||||
|
store.NewWrapper(r.modelStore),
|
||||||
|
r.Provider,
|
||||||
|
r.Account,
|
||||||
|
sel,
|
||||||
|
sel, // the selector acts as an IDNamer for its discrete resource owner.
|
||||||
|
r.Bus)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup retrieves a backup by id.
|
||||||
|
func (r repository) Backup(ctx context.Context, id string) (*backup.Backup, error) {
|
||||||
|
return getBackup(ctx, id, store.NewWrapper(r.modelStore))
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBackup handles the processing for Backup.
|
||||||
|
func getBackup(
|
||||||
|
ctx context.Context,
|
||||||
|
id string,
|
||||||
|
sw store.BackupGetter,
|
||||||
|
) (*backup.Backup, error) {
|
||||||
|
b, err := sw.GetBackup(ctx, model.StableID(id))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errWrapper(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backups lists backups by ID. Returns as many backups as possible with
|
||||||
|
// errors for the backups it was unable to retrieve.
|
||||||
|
func (r repository) Backups(ctx context.Context, ids []string) ([]*backup.Backup, *fault.Bus) {
|
||||||
|
var (
|
||||||
|
bups []*backup.Backup
|
||||||
|
errs = fault.New(false)
|
||||||
|
sw = store.NewWrapper(r.modelStore)
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
ictx := clues.Add(ctx, "backup_id", id)
|
||||||
|
|
||||||
|
b, err := sw.GetBackup(ictx, model.StableID(id))
|
||||||
|
if err != nil {
|
||||||
|
errs.AddRecoverable(ctx, errWrapper(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
bups = append(bups, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bups, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackupsByTag lists all backups in a repository that contain all the tags
|
||||||
|
// specified.
|
||||||
|
func (r repository) BackupsByTag(ctx context.Context, fs ...store.FilterOption) ([]*backup.Backup, error) {
|
||||||
|
sw := store.NewWrapper(r.modelStore)
|
||||||
|
return backupsByTag(ctx, sw, fs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// backupsByTag returns all backups matching all provided tags.
|
||||||
|
//
|
||||||
|
// TODO(ashmrtn): This exists mostly for testing, but we could restructure the
|
||||||
|
// code in this file so there's a more elegant mocking solution.
|
||||||
|
func backupsByTag(
|
||||||
|
ctx context.Context,
|
||||||
|
sw store.BackupWrapper,
|
||||||
|
fs []store.FilterOption,
|
||||||
|
) ([]*backup.Backup, error) {
|
||||||
|
bs, err := sw.GetBackups(ctx, fs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.Stack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out assist backup bases as they're considered incomplete and we
|
||||||
|
// haven't been displaying them before now.
|
||||||
|
res := make([]*backup.Backup, 0, len(bs))
|
||||||
|
|
||||||
|
for _, b := range bs {
|
||||||
|
if t := b.Tags[model.BackupTypeTag]; t != model.AssistBackup {
|
||||||
|
res = append(res, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackupDetails returns the specified backup.Details
|
||||||
|
func (r repository) GetBackupDetails(
|
||||||
|
ctx context.Context,
|
||||||
|
backupID string,
|
||||||
|
) (*details.Details, *backup.Backup, *fault.Bus) {
|
||||||
|
errs := fault.New(false)
|
||||||
|
|
||||||
|
deets, bup, err := getBackupDetails(
|
||||||
|
ctx,
|
||||||
|
backupID,
|
||||||
|
r.Account.ID(),
|
||||||
|
r.dataLayer,
|
||||||
|
store.NewWrapper(r.modelStore),
|
||||||
|
errs)
|
||||||
|
|
||||||
|
return deets, bup, errs.Fail(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBackupDetails handles the processing for GetBackupDetails.
|
||||||
|
func getBackupDetails(
|
||||||
|
ctx context.Context,
|
||||||
|
backupID, tenantID string,
|
||||||
|
kw *kopia.Wrapper,
|
||||||
|
sw store.BackupGetter,
|
||||||
|
errs *fault.Bus,
|
||||||
|
) (*details.Details, *backup.Backup, error) {
|
||||||
|
b, err := sw.GetBackup(ctx, model.StableID(backupID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errWrapper(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ssid := b.StreamStoreID
|
||||||
|
if len(ssid) == 0 {
|
||||||
|
ssid = b.DetailsID
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ssid) == 0 {
|
||||||
|
return nil, b, clues.New("no streamstore id in backup").WithClues(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
sstore = streamstore.NewStreamer(kw, tenantID, b.Selector.PathService())
|
||||||
|
deets details.Details
|
||||||
|
)
|
||||||
|
|
||||||
|
err = sstore.Read(
|
||||||
|
ctx,
|
||||||
|
ssid,
|
||||||
|
streamstore.DetailsReader(details.UnmarshalTo(&deets)),
|
||||||
|
errs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retroactively fill in isMeta information for items in older
|
||||||
|
// backup versions without that info
|
||||||
|
// version.Restore2 introduces the IsMeta flag, so only v1 needs a check.
|
||||||
|
if b.Version >= version.OneDrive1DataAndMetaFiles && b.Version < version.OneDrive3IsMetaMarker {
|
||||||
|
for _, d := range deets.Entries {
|
||||||
|
if d.OneDrive != nil {
|
||||||
|
d.OneDrive.IsMeta = metadata.HasMetaSuffix(d.RepoRef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deets.DetailsModel = deets.FilterMetaFiles()
|
||||||
|
|
||||||
|
return &deets, b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackupErrors returns the specified backup's fault.Errors
|
||||||
|
func (r repository) GetBackupErrors(
|
||||||
|
ctx context.Context,
|
||||||
|
backupID string,
|
||||||
|
) (*fault.Errors, *backup.Backup, *fault.Bus) {
|
||||||
|
errs := fault.New(false)
|
||||||
|
|
||||||
|
fe, bup, err := getBackupErrors(
|
||||||
|
ctx,
|
||||||
|
backupID,
|
||||||
|
r.Account.ID(),
|
||||||
|
r.dataLayer,
|
||||||
|
store.NewWrapper(r.modelStore),
|
||||||
|
errs)
|
||||||
|
|
||||||
|
return fe, bup, errs.Fail(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBackupErrors handles the processing for GetBackupErrors.
|
||||||
|
func getBackupErrors(
|
||||||
|
ctx context.Context,
|
||||||
|
backupID, tenantID string,
|
||||||
|
kw *kopia.Wrapper,
|
||||||
|
sw store.BackupGetter,
|
||||||
|
errs *fault.Bus,
|
||||||
|
) (*fault.Errors, *backup.Backup, error) {
|
||||||
|
b, err := sw.GetBackup(ctx, model.StableID(backupID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errWrapper(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ssid := b.StreamStoreID
|
||||||
|
if len(ssid) == 0 {
|
||||||
|
return nil, b, clues.New("missing streamstore id in backup").WithClues(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
sstore = streamstore.NewStreamer(kw, tenantID, b.Selector.PathService())
|
||||||
|
fe fault.Errors
|
||||||
|
)
|
||||||
|
|
||||||
|
err = sstore.Read(
|
||||||
|
ctx,
|
||||||
|
ssid,
|
||||||
|
streamstore.FaultErrorsReader(fault.UnmarshalErrorsTo(&fe)),
|
||||||
|
errs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &fe, b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBackups removes the backups from both the model store and the backup
|
||||||
|
// storage.
|
||||||
|
//
|
||||||
|
// If failOnMissing is true then returns an error if a backup model can't be
|
||||||
|
// found. Otherwise ignores missing backup models.
|
||||||
|
//
|
||||||
|
// Missing models or snapshots during the actual deletion do not cause errors.
|
||||||
|
//
|
||||||
|
// All backups are delete as an atomic unit so any failures will result in no
|
||||||
|
// deletions.
|
||||||
|
func (r repository) DeleteBackups(
|
||||||
|
ctx context.Context,
|
||||||
|
failOnMissing bool,
|
||||||
|
ids ...string,
|
||||||
|
) error {
|
||||||
|
return deleteBackups(ctx, store.NewWrapper(r.modelStore), failOnMissing, ids...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteBackup handles the processing for backup deletion.
|
||||||
|
func deleteBackups(
|
||||||
|
ctx context.Context,
|
||||||
|
sw store.BackupGetterModelDeleter,
|
||||||
|
failOnMissing bool,
|
||||||
|
ids ...string,
|
||||||
|
) error {
|
||||||
|
// Although we haven't explicitly stated it, snapshots are technically
|
||||||
|
// manifests in kopia. This means we can use the same delete API to remove
|
||||||
|
// them and backup models. Deleting all of them together gives us both
|
||||||
|
// atomicity guarantees (around when data will be flushed) and helps reduce
|
||||||
|
// the number of manifest blobs that kopia will create.
|
||||||
|
var toDelete []manifest.ID
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
b, err := sw.GetBackup(ctx, model.StableID(id))
|
||||||
|
if err != nil {
|
||||||
|
if !failOnMissing && errors.Is(err, data.ErrNotFound) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return clues.Stack(errWrapper(err)).
|
||||||
|
WithClues(ctx).
|
||||||
|
With("delete_backup_id", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
toDelete = append(toDelete, b.ModelStoreID)
|
||||||
|
|
||||||
|
if len(b.SnapshotID) > 0 {
|
||||||
|
toDelete = append(toDelete, manifest.ID(b.SnapshotID))
|
||||||
|
}
|
||||||
|
|
||||||
|
ssid := b.StreamStoreID
|
||||||
|
if len(ssid) == 0 {
|
||||||
|
ssid = b.DetailsID
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ssid) > 0 {
|
||||||
|
toDelete = append(toDelete, manifest.ID(ssid))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sw.DeleteWithModelStoreIDs(ctx, toDelete...)
|
||||||
|
}
|
||||||
88
src/pkg/repository/data_providers.go
Normal file
88
src/pkg/repository/data_providers.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/m365"
|
||||||
|
"github.com/alcionai/corso/src/internal/observe"
|
||||||
|
"github.com/alcionai/corso/src/internal/operations/inject"
|
||||||
|
"github.com/alcionai/corso/src/pkg/account"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DataProvider interface {
|
||||||
|
inject.BackupProducer
|
||||||
|
inject.ExportConsumer
|
||||||
|
inject.RestoreConsumer
|
||||||
|
|
||||||
|
VerifyAccess(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataProviderConnector interface {
|
||||||
|
// ConnectDataProvider initializes configurations
|
||||||
|
// and establishes the client connection with the
|
||||||
|
// data provider for this operation.
|
||||||
|
ConnectDataProvider(
|
||||||
|
ctx context.Context,
|
||||||
|
pst path.ServiceType,
|
||||||
|
) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) ConnectDataProvider(
|
||||||
|
ctx context.Context,
|
||||||
|
pst path.ServiceType,
|
||||||
|
) error {
|
||||||
|
var (
|
||||||
|
provider DataProvider
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
switch r.Account.Provider {
|
||||||
|
case account.ProviderM365:
|
||||||
|
provider, err = connectToM365(ctx, *r, pst)
|
||||||
|
default:
|
||||||
|
err = clues.New("unrecognized provider").WithClues(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "connecting data provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := provider.VerifyAccess(ctx); err != nil {
|
||||||
|
return clues.Wrap(err, fmt.Sprintf("verifying %s account connection", r.Account.Provider))
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Provider = provider
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectToM365(
|
||||||
|
ctx context.Context,
|
||||||
|
r repository,
|
||||||
|
pst path.ServiceType,
|
||||||
|
) (*m365.Controller, error) {
|
||||||
|
if r.Provider != nil {
|
||||||
|
ctrl, ok := r.Provider.(*m365.Controller)
|
||||||
|
if !ok {
|
||||||
|
// if the provider is initialized to a non-m365 controller, we should not
|
||||||
|
// attempt to connnect to m365 afterward.
|
||||||
|
return nil, clues.New("Attempted to connect to multiple data providers")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
progressBar := observe.MessageWithCompletion(ctx, "Connecting to M365")
|
||||||
|
defer close(progressBar)
|
||||||
|
|
||||||
|
ctrl, err := m365.NewController(ctx, r.Account, pst, r.Opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.Wrap(err, "creating m365 client controller")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctrl, nil
|
||||||
|
}
|
||||||
40
src/pkg/repository/exports.go
Normal file
40
src/pkg/repository/exports.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/model"
|
||||||
|
"github.com/alcionai/corso/src/internal/operations"
|
||||||
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
|
"github.com/alcionai/corso/src/pkg/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Exporter interface {
|
||||||
|
NewExport(
|
||||||
|
ctx context.Context,
|
||||||
|
backupID string,
|
||||||
|
sel selectors.Selector,
|
||||||
|
exportCfg control.ExportConfig,
|
||||||
|
) (operations.ExportOperation, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExport generates a exportOperation runner.
|
||||||
|
func (r repository) NewExport(
|
||||||
|
ctx context.Context,
|
||||||
|
backupID string,
|
||||||
|
sel selectors.Selector,
|
||||||
|
exportCfg control.ExportConfig,
|
||||||
|
) (operations.ExportOperation, error) {
|
||||||
|
return operations.NewExportOperation(
|
||||||
|
ctx,
|
||||||
|
r.Opts,
|
||||||
|
r.dataLayer,
|
||||||
|
store.NewWrapper(r.modelStore),
|
||||||
|
r.Provider,
|
||||||
|
r.Account,
|
||||||
|
model.StableID(backupID),
|
||||||
|
sel,
|
||||||
|
exportCfg,
|
||||||
|
r.Bus)
|
||||||
|
}
|
||||||
@ -21,7 +21,6 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/backup"
|
"github.com/alcionai/corso/src/pkg/backup"
|
||||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
ctrlRepo "github.com/alcionai/corso/src/pkg/control/repository"
|
|
||||||
ctrlTD "github.com/alcionai/corso/src/pkg/control/testdata"
|
ctrlTD "github.com/alcionai/corso/src/pkg/control/testdata"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
@ -111,7 +110,7 @@ func initM365Repo(t *testing.T) (
|
|||||||
repository.NewRepoID)
|
repository.NewRepoID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
err = r.Initialize(ctx, ctrlRepo.Retention{})
|
err = r.Initialize(ctx, repository.InitConfig{})
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
return ctx, r, ac, st
|
return ctx, r, ac, st
|
||||||
|
|||||||
@ -6,31 +6,20 @@ import (
|
|||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/kopia/kopia/repo/manifest"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/common/crash"
|
"github.com/alcionai/corso/src/internal/common/crash"
|
||||||
"github.com/alcionai/corso/src/internal/common/idname"
|
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
"github.com/alcionai/corso/src/internal/events"
|
"github.com/alcionai/corso/src/internal/events"
|
||||||
"github.com/alcionai/corso/src/internal/kopia"
|
"github.com/alcionai/corso/src/internal/kopia"
|
||||||
"github.com/alcionai/corso/src/internal/m365"
|
|
||||||
"github.com/alcionai/corso/src/internal/m365/collection/drive/metadata"
|
|
||||||
"github.com/alcionai/corso/src/internal/model"
|
"github.com/alcionai/corso/src/internal/model"
|
||||||
"github.com/alcionai/corso/src/internal/observe"
|
"github.com/alcionai/corso/src/internal/observe"
|
||||||
"github.com/alcionai/corso/src/internal/operations"
|
"github.com/alcionai/corso/src/internal/operations"
|
||||||
"github.com/alcionai/corso/src/internal/streamstore"
|
|
||||||
"github.com/alcionai/corso/src/internal/version"
|
|
||||||
"github.com/alcionai/corso/src/pkg/account"
|
"github.com/alcionai/corso/src/pkg/account"
|
||||||
"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/control"
|
||||||
ctrlRepo "github.com/alcionai/corso/src/pkg/control/repository"
|
ctrlRepo "github.com/alcionai/corso/src/pkg/control/repository"
|
||||||
"github.com/alcionai/corso/src/pkg/count"
|
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
|
||||||
"github.com/alcionai/corso/src/pkg/logger"
|
"github.com/alcionai/corso/src/pkg/logger"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
"github.com/alcionai/corso/src/pkg/selectors"
|
|
||||||
"github.com/alcionai/corso/src/pkg/storage"
|
"github.com/alcionai/corso/src/pkg/storage"
|
||||||
"github.com/alcionai/corso/src/pkg/store"
|
"github.com/alcionai/corso/src/pkg/store"
|
||||||
)
|
)
|
||||||
@ -42,48 +31,24 @@ var (
|
|||||||
ErrorBackupNotFound = clues.New("no backup exists with that id")
|
ErrorBackupNotFound = clues.New("no backup exists with that id")
|
||||||
)
|
)
|
||||||
|
|
||||||
// BackupGetter deals with retrieving metadata about backups from the
|
|
||||||
// repository.
|
|
||||||
type BackupGetter interface {
|
|
||||||
Backup(ctx context.Context, id string) (*backup.Backup, error)
|
|
||||||
Backups(ctx context.Context, ids []string) ([]*backup.Backup, *fault.Bus)
|
|
||||||
BackupsByTag(ctx context.Context, fs ...store.FilterOption) ([]*backup.Backup, error)
|
|
||||||
GetBackupDetails(
|
|
||||||
ctx context.Context,
|
|
||||||
backupID string,
|
|
||||||
) (*details.Details, *backup.Backup, *fault.Bus)
|
|
||||||
GetBackupErrors(
|
|
||||||
ctx context.Context,
|
|
||||||
backupID string,
|
|
||||||
) (*fault.Errors, *backup.Backup, *fault.Bus)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Repositoryer interface {
|
type Repositoryer interface {
|
||||||
Initialize(ctx context.Context, retentionOpts ctrlRepo.Retention) error
|
Backuper
|
||||||
Connect(ctx context.Context) error
|
BackupGetter
|
||||||
|
Restorer
|
||||||
|
Exporter
|
||||||
|
DataProviderConnector
|
||||||
|
|
||||||
|
Initialize(
|
||||||
|
ctx context.Context,
|
||||||
|
cfg InitConfig,
|
||||||
|
) error
|
||||||
|
Connect(
|
||||||
|
ctx context.Context,
|
||||||
|
cfg ConnConfig,
|
||||||
|
) error
|
||||||
GetID() string
|
GetID() string
|
||||||
Close(context.Context) error
|
Close(context.Context) error
|
||||||
NewBackup(
|
|
||||||
ctx context.Context,
|
|
||||||
self selectors.Selector,
|
|
||||||
) (operations.BackupOperation, error)
|
|
||||||
NewBackupWithLookup(
|
|
||||||
ctx context.Context,
|
|
||||||
self selectors.Selector,
|
|
||||||
ins idname.Cacher,
|
|
||||||
) (operations.BackupOperation, error)
|
|
||||||
NewRestore(
|
|
||||||
ctx context.Context,
|
|
||||||
backupID string,
|
|
||||||
sel selectors.Selector,
|
|
||||||
restoreCfg control.RestoreConfig,
|
|
||||||
) (operations.RestoreOperation, error)
|
|
||||||
NewExport(
|
|
||||||
ctx context.Context,
|
|
||||||
backupID string,
|
|
||||||
sel selectors.Selector,
|
|
||||||
exportCfg control.ExportConfig,
|
|
||||||
) (operations.ExportOperation, error)
|
|
||||||
NewMaintenance(
|
NewMaintenance(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
mOpts ctrlRepo.Maintenance,
|
mOpts ctrlRepo.Maintenance,
|
||||||
@ -92,14 +57,6 @@ type Repositoryer interface {
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
rcOpts ctrlRepo.Retention,
|
rcOpts ctrlRepo.Retention,
|
||||||
) (operations.RetentionConfigOperation, error)
|
) (operations.RetentionConfigOperation, error)
|
||||||
DeleteBackups(ctx context.Context, failOnMissing bool, ids ...string) error
|
|
||||||
BackupGetter
|
|
||||||
// ConnectToM365 establishes graph api connections
|
|
||||||
// and initializes api client configurations.
|
|
||||||
ConnectToM365(
|
|
||||||
ctx context.Context,
|
|
||||||
pst path.ServiceType,
|
|
||||||
) (*m365.Controller, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repository contains storage provider information.
|
// Repository contains storage provider information.
|
||||||
@ -108,9 +65,10 @@ type repository struct {
|
|||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
Version string // in case of future breaking changes
|
Version string // in case of future breaking changes
|
||||||
|
|
||||||
Account account.Account // the user's m365 account connection details
|
Account account.Account // the user's m365 account connection details
|
||||||
Storage storage.Storage // the storage provider details and configuration
|
Storage storage.Storage // the storage provider details and configuration
|
||||||
Opts control.Options
|
Opts control.Options
|
||||||
|
Provider DataProvider // the client controller used for external user data CRUD
|
||||||
|
|
||||||
Bus events.Eventer
|
Bus events.Eventer
|
||||||
dataLayer *kopia.Wrapper
|
dataLayer *kopia.Wrapper
|
||||||
@ -125,7 +83,7 @@ func (r repository) GetID() string {
|
|||||||
func New(
|
func New(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
acct account.Account,
|
acct account.Account,
|
||||||
s storage.Storage,
|
st storage.Storage,
|
||||||
opts control.Options,
|
opts control.Options,
|
||||||
configFileRepoID string,
|
configFileRepoID string,
|
||||||
) (repo *repository, err error) {
|
) (repo *repository, err error) {
|
||||||
@ -133,16 +91,16 @@ func New(
|
|||||||
ctx,
|
ctx,
|
||||||
"acct_provider", acct.Provider.String(),
|
"acct_provider", acct.Provider.String(),
|
||||||
"acct_id", clues.Hide(acct.ID()),
|
"acct_id", clues.Hide(acct.ID()),
|
||||||
"storage_provider", s.Provider.String())
|
"storage_provider", st.Provider.String())
|
||||||
|
|
||||||
bus, err := events.NewBus(ctx, s, acct.ID(), opts)
|
bus, err := events.NewBus(ctx, st, acct.ID(), opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, clues.Wrap(err, "constructing event bus").WithClues(ctx)
|
return nil, clues.Wrap(err, "constructing event bus").WithClues(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
repoID := configFileRepoID
|
repoID := configFileRepoID
|
||||||
if len(configFileRepoID) == 0 {
|
if len(configFileRepoID) == 0 {
|
||||||
repoID = newRepoID(s)
|
repoID = newRepoID(st)
|
||||||
}
|
}
|
||||||
|
|
||||||
bus.SetRepoID(repoID)
|
bus.SetRepoID(repoID)
|
||||||
@ -151,7 +109,7 @@ func New(
|
|||||||
ID: repoID,
|
ID: repoID,
|
||||||
Version: "v1",
|
Version: "v1",
|
||||||
Account: acct,
|
Account: acct,
|
||||||
Storage: s,
|
Storage: st,
|
||||||
Bus: bus,
|
Bus: bus,
|
||||||
Opts: opts,
|
Opts: opts,
|
||||||
}
|
}
|
||||||
@ -163,17 +121,22 @@ func New(
|
|||||||
return &r, nil
|
return &r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InitConfig struct {
|
||||||
|
// tells the data provider which service to
|
||||||
|
// use for its connection pattern. Optional.
|
||||||
|
Service path.ServiceType
|
||||||
|
RetentionOpts ctrlRepo.Retention
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize will:
|
// Initialize will:
|
||||||
// - validate the m365 account & secrets
|
|
||||||
// - connect to the m365 account to ensure communication capability
|
// - connect to the m365 account to ensure communication capability
|
||||||
// - validate the provider config & secrets
|
|
||||||
// - initialize the kopia repo with the provider and retention parameters
|
// - initialize the kopia repo with the provider and retention parameters
|
||||||
// - update maintenance retention parameters as needed
|
// - update maintenance retention parameters as needed
|
||||||
// - store the configuration details
|
// - store the configuration details
|
||||||
// - connect to the provider
|
// - connect to the provider
|
||||||
func (r *repository) Initialize(
|
func (r *repository) Initialize(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
retentionOpts ctrlRepo.Retention,
|
cfg InitConfig,
|
||||||
) (err error) {
|
) (err error) {
|
||||||
ctx = clues.Add(
|
ctx = clues.Add(
|
||||||
ctx,
|
ctx,
|
||||||
@ -187,10 +150,14 @@ func (r *repository) Initialize(
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
if err := r.ConnectDataProvider(ctx, cfg.Service); err != nil {
|
||||||
|
return clues.Stack(err)
|
||||||
|
}
|
||||||
|
|
||||||
observe.Message(ctx, "Initializing repository")
|
observe.Message(ctx, "Initializing repository")
|
||||||
|
|
||||||
kopiaRef := kopia.NewConn(r.Storage)
|
kopiaRef := kopia.NewConn(r.Storage)
|
||||||
if err := kopiaRef.Initialize(ctx, r.Opts.Repo, retentionOpts); err != nil {
|
if err := kopiaRef.Initialize(ctx, r.Opts.Repo, cfg.RetentionOpts); err != nil {
|
||||||
// replace common internal errors so that sdk users can check results with errors.Is()
|
// replace common internal errors so that sdk users can check results with errors.Is()
|
||||||
if errors.Is(err, kopia.ErrorRepoAlreadyExists) {
|
if errors.Is(err, kopia.ErrorRepoAlreadyExists) {
|
||||||
return clues.Stack(ErrorRepoAlreadyExists, err).WithClues(ctx)
|
return clues.Stack(ErrorRepoAlreadyExists, err).WithClues(ctx)
|
||||||
@ -221,12 +188,21 @@ func (r *repository) Initialize(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConnConfig struct {
|
||||||
|
// tells the data provider which service to
|
||||||
|
// use for its connection pattern. Leave empty
|
||||||
|
// to skip the provider connection.
|
||||||
|
Service path.ServiceType
|
||||||
|
}
|
||||||
|
|
||||||
// Connect will:
|
// Connect will:
|
||||||
// - validate the m365 account details
|
// - connect to the m365 account
|
||||||
// - connect to the m365 account to ensure communication capability
|
|
||||||
// - connect to the provider storage
|
// - connect to the provider storage
|
||||||
// - return the connected repository
|
// - return the connected repository
|
||||||
func (r *repository) Connect(ctx context.Context) (err error) {
|
func (r *repository) Connect(
|
||||||
|
ctx context.Context,
|
||||||
|
cfg ConnConfig,
|
||||||
|
) (err error) {
|
||||||
ctx = clues.Add(
|
ctx = clues.Add(
|
||||||
ctx,
|
ctx,
|
||||||
"acct_provider", r.Account.Provider.String(),
|
"acct_provider", r.Account.Provider.String(),
|
||||||
@ -239,6 +215,10 @@ func (r *repository) Connect(ctx context.Context) (err error) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
if err := r.ConnectDataProvider(ctx, cfg.Service); err != nil {
|
||||||
|
return clues.Stack(err)
|
||||||
|
}
|
||||||
|
|
||||||
observe.Message(ctx, "Connecting to repository")
|
observe.Message(ctx, "Connecting to repository")
|
||||||
|
|
||||||
kopiaRef := kopia.NewConn(r.Storage)
|
kopiaRef := kopia.NewConn(r.Storage)
|
||||||
@ -297,98 +277,6 @@ func (r *repository) Close(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBackup generates a BackupOperation runner.
|
|
||||||
func (r repository) NewBackup(
|
|
||||||
ctx context.Context,
|
|
||||||
sel selectors.Selector,
|
|
||||||
) (operations.BackupOperation, error) {
|
|
||||||
return r.NewBackupWithLookup(ctx, sel, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBackupWithLookup generates a BackupOperation runner.
|
|
||||||
// ownerIDToName and ownerNameToID are optional populations, in case the caller has
|
|
||||||
// already generated those values.
|
|
||||||
func (r repository) NewBackupWithLookup(
|
|
||||||
ctx context.Context,
|
|
||||||
sel selectors.Selector,
|
|
||||||
ins idname.Cacher,
|
|
||||||
) (operations.BackupOperation, error) {
|
|
||||||
ctrl, err := connectToM365(ctx, sel.PathService(), r.Account, r.Opts)
|
|
||||||
if err != nil {
|
|
||||||
return operations.BackupOperation{}, clues.Wrap(err, "connecting to m365")
|
|
||||||
}
|
|
||||||
|
|
||||||
ownerID, ownerName, err := ctrl.PopulateProtectedResourceIDAndName(ctx, sel.DiscreteOwner, ins)
|
|
||||||
if err != nil {
|
|
||||||
return operations.BackupOperation{}, clues.Wrap(err, "resolving resource owner details")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: retrieve display name from gc
|
|
||||||
sel = sel.SetDiscreteOwnerIDName(ownerID, ownerName)
|
|
||||||
|
|
||||||
return operations.NewBackupOperation(
|
|
||||||
ctx,
|
|
||||||
r.Opts,
|
|
||||||
r.dataLayer,
|
|
||||||
store.NewWrapper(r.modelStore),
|
|
||||||
ctrl,
|
|
||||||
r.Account,
|
|
||||||
sel,
|
|
||||||
sel, // the selector acts as an IDNamer for its discrete resource owner.
|
|
||||||
r.Bus)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewExport generates a exportOperation runner.
|
|
||||||
func (r repository) NewExport(
|
|
||||||
ctx context.Context,
|
|
||||||
backupID string,
|
|
||||||
sel selectors.Selector,
|
|
||||||
exportCfg control.ExportConfig,
|
|
||||||
) (operations.ExportOperation, error) {
|
|
||||||
ctrl, err := connectToM365(ctx, sel.PathService(), r.Account, r.Opts)
|
|
||||||
if err != nil {
|
|
||||||
return operations.ExportOperation{}, clues.Wrap(err, "connecting to m365")
|
|
||||||
}
|
|
||||||
|
|
||||||
return operations.NewExportOperation(
|
|
||||||
ctx,
|
|
||||||
r.Opts,
|
|
||||||
r.dataLayer,
|
|
||||||
store.NewWrapper(r.modelStore),
|
|
||||||
ctrl,
|
|
||||||
r.Account,
|
|
||||||
model.StableID(backupID),
|
|
||||||
sel,
|
|
||||||
exportCfg,
|
|
||||||
r.Bus)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRestore generates a restoreOperation runner.
|
|
||||||
func (r repository) NewRestore(
|
|
||||||
ctx context.Context,
|
|
||||||
backupID string,
|
|
||||||
sel selectors.Selector,
|
|
||||||
restoreCfg control.RestoreConfig,
|
|
||||||
) (operations.RestoreOperation, error) {
|
|
||||||
ctrl, err := connectToM365(ctx, sel.PathService(), r.Account, r.Opts)
|
|
||||||
if err != nil {
|
|
||||||
return operations.RestoreOperation{}, clues.Wrap(err, "connecting to m365")
|
|
||||||
}
|
|
||||||
|
|
||||||
return operations.NewRestoreOperation(
|
|
||||||
ctx,
|
|
||||||
r.Opts,
|
|
||||||
r.dataLayer,
|
|
||||||
store.NewWrapper(r.modelStore),
|
|
||||||
ctrl,
|
|
||||||
r.Account,
|
|
||||||
model.StableID(backupID),
|
|
||||||
sel,
|
|
||||||
restoreCfg,
|
|
||||||
r.Bus,
|
|
||||||
count.New())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r repository) NewMaintenance(
|
func (r repository) NewMaintenance(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
mOpts ctrlRepo.Maintenance,
|
mOpts ctrlRepo.Maintenance,
|
||||||
@ -414,280 +302,6 @@ func (r repository) NewRetentionConfig(
|
|||||||
r.Bus)
|
r.Bus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backup retrieves a backup by id.
|
|
||||||
func (r repository) Backup(ctx context.Context, id string) (*backup.Backup, error) {
|
|
||||||
return getBackup(ctx, id, store.NewWrapper(r.modelStore))
|
|
||||||
}
|
|
||||||
|
|
||||||
// getBackup handles the processing for Backup.
|
|
||||||
func getBackup(
|
|
||||||
ctx context.Context,
|
|
||||||
id string,
|
|
||||||
sw store.BackupGetter,
|
|
||||||
) (*backup.Backup, error) {
|
|
||||||
b, err := sw.GetBackup(ctx, model.StableID(id))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errWrapper(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backups lists backups by ID. Returns as many backups as possible with
|
|
||||||
// errors for the backups it was unable to retrieve.
|
|
||||||
func (r repository) Backups(ctx context.Context, ids []string) ([]*backup.Backup, *fault.Bus) {
|
|
||||||
var (
|
|
||||||
bups []*backup.Backup
|
|
||||||
errs = fault.New(false)
|
|
||||||
sw = store.NewWrapper(r.modelStore)
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, id := range ids {
|
|
||||||
ictx := clues.Add(ctx, "backup_id", id)
|
|
||||||
|
|
||||||
b, err := sw.GetBackup(ictx, model.StableID(id))
|
|
||||||
if err != nil {
|
|
||||||
errs.AddRecoverable(ctx, errWrapper(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
bups = append(bups, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
return bups, errs
|
|
||||||
}
|
|
||||||
|
|
||||||
// BackupsByTag lists all backups in a repository that contain all the tags
|
|
||||||
// specified.
|
|
||||||
func (r repository) BackupsByTag(ctx context.Context, fs ...store.FilterOption) ([]*backup.Backup, error) {
|
|
||||||
sw := store.NewWrapper(r.modelStore)
|
|
||||||
return backupsByTag(ctx, sw, fs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// backupsByTag returns all backups matching all provided tags.
|
|
||||||
//
|
|
||||||
// TODO(ashmrtn): This exists mostly for testing, but we could restructure the
|
|
||||||
// code in this file so there's a more elegant mocking solution.
|
|
||||||
func backupsByTag(
|
|
||||||
ctx context.Context,
|
|
||||||
sw store.BackupWrapper,
|
|
||||||
fs []store.FilterOption,
|
|
||||||
) ([]*backup.Backup, error) {
|
|
||||||
bs, err := sw.GetBackups(ctx, fs...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, clues.Stack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out assist backup bases as they're considered incomplete and we
|
|
||||||
// haven't been displaying them before now.
|
|
||||||
res := make([]*backup.Backup, 0, len(bs))
|
|
||||||
|
|
||||||
for _, b := range bs {
|
|
||||||
if t := b.Tags[model.BackupTypeTag]; t != model.AssistBackup {
|
|
||||||
res = append(res, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BackupDetails returns the specified backup.Details
|
|
||||||
func (r repository) GetBackupDetails(
|
|
||||||
ctx context.Context,
|
|
||||||
backupID string,
|
|
||||||
) (*details.Details, *backup.Backup, *fault.Bus) {
|
|
||||||
errs := fault.New(false)
|
|
||||||
|
|
||||||
deets, bup, err := getBackupDetails(
|
|
||||||
ctx,
|
|
||||||
backupID,
|
|
||||||
r.Account.ID(),
|
|
||||||
r.dataLayer,
|
|
||||||
store.NewWrapper(r.modelStore),
|
|
||||||
errs)
|
|
||||||
|
|
||||||
return deets, bup, errs.Fail(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getBackupDetails handles the processing for GetBackupDetails.
|
|
||||||
func getBackupDetails(
|
|
||||||
ctx context.Context,
|
|
||||||
backupID, tenantID string,
|
|
||||||
kw *kopia.Wrapper,
|
|
||||||
sw store.BackupGetter,
|
|
||||||
errs *fault.Bus,
|
|
||||||
) (*details.Details, *backup.Backup, error) {
|
|
||||||
b, err := sw.GetBackup(ctx, model.StableID(backupID))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, errWrapper(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ssid := b.StreamStoreID
|
|
||||||
if len(ssid) == 0 {
|
|
||||||
ssid = b.DetailsID
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ssid) == 0 {
|
|
||||||
return nil, b, clues.New("no streamstore id in backup").WithClues(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
sstore = streamstore.NewStreamer(kw, tenantID, b.Selector.PathService())
|
|
||||||
deets details.Details
|
|
||||||
)
|
|
||||||
|
|
||||||
err = sstore.Read(
|
|
||||||
ctx,
|
|
||||||
ssid,
|
|
||||||
streamstore.DetailsReader(details.UnmarshalTo(&deets)),
|
|
||||||
errs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retroactively fill in isMeta information for items in older
|
|
||||||
// backup versions without that info
|
|
||||||
// version.Restore2 introduces the IsMeta flag, so only v1 needs a check.
|
|
||||||
if b.Version >= version.OneDrive1DataAndMetaFiles && b.Version < version.OneDrive3IsMetaMarker {
|
|
||||||
for _, d := range deets.Entries {
|
|
||||||
if d.OneDrive != nil {
|
|
||||||
d.OneDrive.IsMeta = metadata.HasMetaSuffix(d.RepoRef)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deets.DetailsModel = deets.FilterMetaFiles()
|
|
||||||
|
|
||||||
return &deets, b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BackupErrors returns the specified backup's fault.Errors
|
|
||||||
func (r repository) GetBackupErrors(
|
|
||||||
ctx context.Context,
|
|
||||||
backupID string,
|
|
||||||
) (*fault.Errors, *backup.Backup, *fault.Bus) {
|
|
||||||
errs := fault.New(false)
|
|
||||||
|
|
||||||
fe, bup, err := getBackupErrors(
|
|
||||||
ctx,
|
|
||||||
backupID,
|
|
||||||
r.Account.ID(),
|
|
||||||
r.dataLayer,
|
|
||||||
store.NewWrapper(r.modelStore),
|
|
||||||
errs)
|
|
||||||
|
|
||||||
return fe, bup, errs.Fail(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getBackupErrors handles the processing for GetBackupErrors.
|
|
||||||
func getBackupErrors(
|
|
||||||
ctx context.Context,
|
|
||||||
backupID, tenantID string,
|
|
||||||
kw *kopia.Wrapper,
|
|
||||||
sw store.BackupGetter,
|
|
||||||
errs *fault.Bus,
|
|
||||||
) (*fault.Errors, *backup.Backup, error) {
|
|
||||||
b, err := sw.GetBackup(ctx, model.StableID(backupID))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, errWrapper(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ssid := b.StreamStoreID
|
|
||||||
if len(ssid) == 0 {
|
|
||||||
return nil, b, clues.New("missing streamstore id in backup").WithClues(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
sstore = streamstore.NewStreamer(kw, tenantID, b.Selector.PathService())
|
|
||||||
fe fault.Errors
|
|
||||||
)
|
|
||||||
|
|
||||||
err = sstore.Read(
|
|
||||||
ctx,
|
|
||||||
ssid,
|
|
||||||
streamstore.FaultErrorsReader(fault.UnmarshalErrorsTo(&fe)),
|
|
||||||
errs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &fe, b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteBackups removes the backups from both the model store and the backup
|
|
||||||
// storage.
|
|
||||||
//
|
|
||||||
// If failOnMissing is true then returns an error if a backup model can't be
|
|
||||||
// found. Otherwise ignores missing backup models.
|
|
||||||
//
|
|
||||||
// Missing models or snapshots during the actual deletion do not cause errors.
|
|
||||||
//
|
|
||||||
// All backups are delete as an atomic unit so any failures will result in no
|
|
||||||
// deletions.
|
|
||||||
func (r repository) DeleteBackups(
|
|
||||||
ctx context.Context,
|
|
||||||
failOnMissing bool,
|
|
||||||
ids ...string,
|
|
||||||
) error {
|
|
||||||
return deleteBackups(ctx, store.NewWrapper(r.modelStore), failOnMissing, ids...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteBackup handles the processing for backup deletion.
|
|
||||||
func deleteBackups(
|
|
||||||
ctx context.Context,
|
|
||||||
sw store.BackupGetterModelDeleter,
|
|
||||||
failOnMissing bool,
|
|
||||||
ids ...string,
|
|
||||||
) error {
|
|
||||||
// Although we haven't explicitly stated it, snapshots are technically
|
|
||||||
// manifests in kopia. This means we can use the same delete API to remove
|
|
||||||
// them and backup models. Deleting all of them together gives us both
|
|
||||||
// atomicity guarantees (around when data will be flushed) and helps reduce
|
|
||||||
// the number of manifest blobs that kopia will create.
|
|
||||||
var toDelete []manifest.ID
|
|
||||||
|
|
||||||
for _, id := range ids {
|
|
||||||
b, err := sw.GetBackup(ctx, model.StableID(id))
|
|
||||||
if err != nil {
|
|
||||||
if !failOnMissing && errors.Is(err, data.ErrNotFound) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return clues.Stack(errWrapper(err)).
|
|
||||||
WithClues(ctx).
|
|
||||||
With("delete_backup_id", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
toDelete = append(toDelete, b.ModelStoreID)
|
|
||||||
|
|
||||||
if len(b.SnapshotID) > 0 {
|
|
||||||
toDelete = append(toDelete, manifest.ID(b.SnapshotID))
|
|
||||||
}
|
|
||||||
|
|
||||||
ssid := b.StreamStoreID
|
|
||||||
if len(ssid) == 0 {
|
|
||||||
ssid = b.DetailsID
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ssid) > 0 {
|
|
||||||
toDelete = append(toDelete, manifest.ID(ssid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sw.DeleteWithModelStoreIDs(ctx, toDelete...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r repository) ConnectToM365(
|
|
||||||
ctx context.Context,
|
|
||||||
pst path.ServiceType,
|
|
||||||
) (*m365.Controller, error) {
|
|
||||||
ctrl, err := connectToM365(ctx, pst, r.Account, r.Opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, clues.Wrap(err, "connecting to m365")
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctrl, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Repository ID Model
|
// Repository ID Model
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -736,29 +350,6 @@ func newRepoID(s storage.Storage) string {
|
|||||||
// helpers
|
// helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
var m365nonce bool
|
|
||||||
|
|
||||||
func connectToM365(
|
|
||||||
ctx context.Context,
|
|
||||||
pst path.ServiceType,
|
|
||||||
acct account.Account,
|
|
||||||
co control.Options,
|
|
||||||
) (*m365.Controller, error) {
|
|
||||||
if !m365nonce {
|
|
||||||
m365nonce = true
|
|
||||||
|
|
||||||
progressBar := observe.MessageWithCompletion(ctx, "Connecting to M365")
|
|
||||||
defer close(progressBar)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrl, err := m365.NewController(ctx, acct, pst, co)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctrl, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func errWrapper(err error) error {
|
func errWrapper(err error) error {
|
||||||
if errors.Is(err, data.ErrNotFound) {
|
if errors.Is(err, data.ErrNotFound) {
|
||||||
return clues.Stack(ErrorBackupNotFound, err)
|
return clues.Stack(ErrorBackupNotFound, err)
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import (
|
|||||||
ctrlRepo "github.com/alcionai/corso/src/pkg/control/repository"
|
ctrlRepo "github.com/alcionai/corso/src/pkg/control/repository"
|
||||||
"github.com/alcionai/corso/src/pkg/control/testdata"
|
"github.com/alcionai/corso/src/pkg/control/testdata"
|
||||||
"github.com/alcionai/corso/src/pkg/extensions"
|
"github.com/alcionai/corso/src/pkg/extensions"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
"github.com/alcionai/corso/src/pkg/selectors"
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||||
"github.com/alcionai/corso/src/pkg/storage"
|
"github.com/alcionai/corso/src/pkg/storage"
|
||||||
@ -69,7 +70,7 @@ func (suite *RepositoryUnitSuite) TestInitialize() {
|
|||||||
NewRepoID)
|
NewRepoID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
err = r.Initialize(ctx, ctrlRepo.Retention{})
|
err = r.Initialize(ctx, InitConfig{})
|
||||||
test.errCheck(t, err, clues.ToCore(err))
|
test.errCheck(t, err, clues.ToCore(err))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -85,12 +86,12 @@ func (suite *RepositoryUnitSuite) TestConnect() {
|
|||||||
errCheck assert.ErrorAssertionFunc
|
errCheck assert.ErrorAssertionFunc
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
storage.ProviderUnknown.String(),
|
name: storage.ProviderUnknown.String(),
|
||||||
func() (storage.Storage, error) {
|
storage: func() (storage.Storage, error) {
|
||||||
return storage.NewStorage(storage.ProviderUnknown)
|
return storage.NewStorage(storage.ProviderUnknown)
|
||||||
},
|
},
|
||||||
account.Account{},
|
account: account.Account{},
|
||||||
assert.Error,
|
errCheck: assert.Error,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
@ -111,7 +112,7 @@ func (suite *RepositoryUnitSuite) TestConnect() {
|
|||||||
NewRepoID)
|
NewRepoID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
err = r.Connect(ctx)
|
err = r.Connect(ctx, ConnConfig{})
|
||||||
test.errCheck(t, err, clues.ToCore(err))
|
test.errCheck(t, err, clues.ToCore(err))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -136,12 +137,13 @@ func TestRepositoryIntegrationSuite(t *testing.T) {
|
|||||||
func (suite *RepositoryIntegrationSuite) TestInitialize() {
|
func (suite *RepositoryIntegrationSuite) TestInitialize() {
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
account account.Account
|
account func(*testing.T) account.Account
|
||||||
storage func(tester.TestT) storage.Storage
|
storage func(tester.TestT) storage.Storage
|
||||||
errCheck assert.ErrorAssertionFunc
|
errCheck assert.ErrorAssertionFunc
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "success",
|
name: "success",
|
||||||
|
account: tconfig.NewM365Account,
|
||||||
storage: storeTD.NewPrefixedS3Storage,
|
storage: storeTD.NewPrefixedS3Storage,
|
||||||
errCheck: assert.NoError,
|
errCheck: assert.NoError,
|
||||||
},
|
},
|
||||||
@ -156,13 +158,13 @@ func (suite *RepositoryIntegrationSuite) TestInitialize() {
|
|||||||
st := test.storage(t)
|
st := test.storage(t)
|
||||||
r, err := New(
|
r, err := New(
|
||||||
ctx,
|
ctx,
|
||||||
test.account,
|
test.account(t),
|
||||||
st,
|
st,
|
||||||
control.DefaultOptions(),
|
control.DefaultOptions(),
|
||||||
NewRepoID)
|
NewRepoID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
err = r.Initialize(ctx, ctrlRepo.Retention{})
|
err = r.Initialize(ctx, InitConfig{})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer func() {
|
defer func() {
|
||||||
err := r.Close(ctx)
|
err := r.Close(ctx)
|
||||||
@ -204,7 +206,7 @@ func (suite *RepositoryIntegrationSuite) TestInitializeWithRole() {
|
|||||||
NewRepoID)
|
NewRepoID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
err = r.Initialize(ctx, ctrlRepo.Retention{})
|
err = r.Initialize(ctx, InitConfig{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -218,21 +220,23 @@ func (suite *RepositoryIntegrationSuite) TestConnect() {
|
|||||||
ctx, flush := tester.NewContext(t)
|
ctx, flush := tester.NewContext(t)
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
|
acct := tconfig.NewM365Account(t)
|
||||||
|
|
||||||
// need to initialize the repository before we can test connecting to it.
|
// need to initialize the repository before we can test connecting to it.
|
||||||
st := storeTD.NewPrefixedS3Storage(t)
|
st := storeTD.NewPrefixedS3Storage(t)
|
||||||
r, err := New(
|
r, err := New(
|
||||||
ctx,
|
ctx,
|
||||||
account.Account{},
|
acct,
|
||||||
st,
|
st,
|
||||||
control.DefaultOptions(),
|
control.DefaultOptions(),
|
||||||
NewRepoID)
|
NewRepoID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
err = r.Initialize(ctx, ctrlRepo.Retention{})
|
err = r.Initialize(ctx, InitConfig{})
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
// now re-connect
|
// now re-connect
|
||||||
err = r.Connect(ctx)
|
err = r.Connect(ctx, ConnConfig{})
|
||||||
assert.NoError(t, err, clues.ToCore(err))
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,17 +246,19 @@ func (suite *RepositoryIntegrationSuite) TestConnect_sameID() {
|
|||||||
ctx, flush := tester.NewContext(t)
|
ctx, flush := tester.NewContext(t)
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
|
acct := tconfig.NewM365Account(t)
|
||||||
|
|
||||||
// need to initialize the repository before we can test connecting to it.
|
// need to initialize the repository before we can test connecting to it.
|
||||||
st := storeTD.NewPrefixedS3Storage(t)
|
st := storeTD.NewPrefixedS3Storage(t)
|
||||||
r, err := New(
|
r, err := New(
|
||||||
ctx,
|
ctx,
|
||||||
account.Account{},
|
acct,
|
||||||
st,
|
st,
|
||||||
control.DefaultOptions(),
|
control.DefaultOptions(),
|
||||||
NewRepoID)
|
NewRepoID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
err = r.Initialize(ctx, ctrlRepo.Retention{})
|
err = r.Initialize(ctx, InitConfig{})
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
oldID := r.GetID()
|
oldID := r.GetID()
|
||||||
@ -261,7 +267,7 @@ func (suite *RepositoryIntegrationSuite) TestConnect_sameID() {
|
|||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
// now re-connect
|
// now re-connect
|
||||||
err = r.Connect(ctx)
|
err = r.Connect(ctx, ConnConfig{})
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
assert.Equal(t, oldID, r.GetID())
|
assert.Equal(t, oldID, r.GetID())
|
||||||
}
|
}
|
||||||
@ -284,7 +290,8 @@ func (suite *RepositoryIntegrationSuite) TestNewBackup() {
|
|||||||
NewRepoID)
|
NewRepoID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
err = r.Initialize(ctx, ctrlRepo.Retention{})
|
// service doesn't matter here, we just need a valid value.
|
||||||
|
err = r.Initialize(ctx, InitConfig{Service: path.ExchangeService})
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
userID := tconfig.M365UserID(t)
|
userID := tconfig.M365UserID(t)
|
||||||
@ -313,7 +320,7 @@ func (suite *RepositoryIntegrationSuite) TestNewRestore() {
|
|||||||
"")
|
"")
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
err = r.Initialize(ctx, ctrlRepo.Retention{})
|
err = r.Initialize(ctx, InitConfig{})
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
ro, err := r.NewRestore(
|
ro, err := r.NewRestore(
|
||||||
@ -343,7 +350,8 @@ func (suite *RepositoryIntegrationSuite) TestNewBackupAndDelete() {
|
|||||||
NewRepoID)
|
NewRepoID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
err = r.Initialize(ctx, ctrlRepo.Retention{})
|
// service doesn't matter here, we just need a valid value.
|
||||||
|
err = r.Initialize(ctx, InitConfig{Service: path.ExchangeService})
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
userID := tconfig.M365UserID(t)
|
userID := tconfig.M365UserID(t)
|
||||||
@ -396,7 +404,7 @@ func (suite *RepositoryIntegrationSuite) TestNewMaintenance() {
|
|||||||
NewRepoID)
|
NewRepoID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
err = r.Initialize(ctx, ctrlRepo.Retention{})
|
err = r.Initialize(ctx, InitConfig{})
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
mo, err := r.NewMaintenance(ctx, ctrlRepo.Maintenance{})
|
mo, err := r.NewMaintenance(ctx, ctrlRepo.Maintenance{})
|
||||||
@ -465,11 +473,11 @@ func (suite *RepositoryIntegrationSuite) Test_Options() {
|
|||||||
NewRepoID)
|
NewRepoID)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
err = r.Initialize(ctx, ctrlRepo.Retention{})
|
err = r.Initialize(ctx, InitConfig{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, test.expectedLen, len(r.Opts.ItemExtensionFactory))
|
assert.Equal(t, test.expectedLen, len(r.Opts.ItemExtensionFactory))
|
||||||
|
|
||||||
err = r.Connect(ctx)
|
err = r.Connect(ctx, ConnConfig{})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, test.expectedLen, len(r.Opts.ItemExtensionFactory))
|
assert.Equal(t, test.expectedLen, len(r.Opts.ItemExtensionFactory))
|
||||||
})
|
})
|
||||||
|
|||||||
42
src/pkg/repository/restores.go
Normal file
42
src/pkg/repository/restores.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/model"
|
||||||
|
"github.com/alcionai/corso/src/internal/operations"
|
||||||
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
|
"github.com/alcionai/corso/src/pkg/count"
|
||||||
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
|
"github.com/alcionai/corso/src/pkg/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Restorer interface {
|
||||||
|
NewRestore(
|
||||||
|
ctx context.Context,
|
||||||
|
backupID string,
|
||||||
|
sel selectors.Selector,
|
||||||
|
restoreCfg control.RestoreConfig,
|
||||||
|
) (operations.RestoreOperation, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRestore generates a restoreOperation runner.
|
||||||
|
func (r repository) NewRestore(
|
||||||
|
ctx context.Context,
|
||||||
|
backupID string,
|
||||||
|
sel selectors.Selector,
|
||||||
|
restoreCfg control.RestoreConfig,
|
||||||
|
) (operations.RestoreOperation, error) {
|
||||||
|
return operations.NewRestoreOperation(
|
||||||
|
ctx,
|
||||||
|
r.Opts,
|
||||||
|
r.dataLayer,
|
||||||
|
store.NewWrapper(r.modelStore),
|
||||||
|
r.Provider,
|
||||||
|
r.Account,
|
||||||
|
model.StableID(backupID),
|
||||||
|
sel,
|
||||||
|
restoreCfg,
|
||||||
|
r.Bus,
|
||||||
|
count.New())
|
||||||
|
}
|
||||||
68
src/pkg/services/m365/api/access.go
Normal file
68
src/pkg/services/m365/api/access.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/m365/graph"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// controller
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func (c Client) Access() Access {
|
||||||
|
return Access{c}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access is an interface-compliant provider of the client.
|
||||||
|
type Access struct {
|
||||||
|
Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetToken retrieves a m365 application auth token using client id and secret credentials.
|
||||||
|
// This token is not normally needed in order for corso to function, and is implemented
|
||||||
|
// primarily as a way to exercise the validity of those credentials without need of specific
|
||||||
|
// permissions.
|
||||||
|
func (c Access) GetToken(
|
||||||
|
ctx context.Context,
|
||||||
|
) error {
|
||||||
|
var (
|
||||||
|
//nolint:lll
|
||||||
|
// https://learn.microsoft.com/en-us/graph/connecting-external-content-connectors-api-postman#step-5-get-an-authentication-token
|
||||||
|
rawURL = fmt.Sprintf(
|
||||||
|
"https://login.microsoftonline.com/%s/oauth2/v2.0/token",
|
||||||
|
c.Credentials.AzureTenantID)
|
||||||
|
headers = map[string]string{
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
}
|
||||||
|
body = strings.NewReader(fmt.Sprintf(
|
||||||
|
"client_id=%s"+
|
||||||
|
"&client_secret=%s"+
|
||||||
|
"&scope=https://graph.microsoft.com/.default"+
|
||||||
|
"&grant_type=client_credentials",
|
||||||
|
c.Credentials.AzureClientID,
|
||||||
|
c.Credentials.AzureClientSecret))
|
||||||
|
)
|
||||||
|
|
||||||
|
resp, err := c.Post(ctx, rawURL, headers, body)
|
||||||
|
if err != nil {
|
||||||
|
return graph.Stack(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusBadRequest {
|
||||||
|
return clues.New("incorrect tenant or application parameters")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode/100 == 4 || resp.StatusCode/100 == 5 {
|
||||||
|
return clues.New("non-2xx response: " + resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
122
src/pkg/services/m365/api/access_test.go
Normal file
122
src/pkg/services/m365/api/access_test.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package api_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
|
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
||||||
|
"github.com/alcionai/corso/src/pkg/account"
|
||||||
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
|
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AccessAPIIntgSuite struct {
|
||||||
|
tester.Suite
|
||||||
|
its intgTesterSetup
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessAPIIntgSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &AccessAPIIntgSuite{
|
||||||
|
Suite: tester.NewIntegrationSuite(
|
||||||
|
t,
|
||||||
|
[][]string{tconfig.M365AcctCredEnvs}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *AccessAPIIntgSuite) SetupSuite() {
|
||||||
|
suite.its = newIntegrationTesterSetup(suite.T())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *AccessAPIIntgSuite) TestGetToken() {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
creds func() account.M365Config
|
||||||
|
expectErr require.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "good",
|
||||||
|
creds: func() account.M365Config { return suite.its.ac.Credentials },
|
||||||
|
expectErr: require.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad tenant ID",
|
||||||
|
creds: func() account.M365Config {
|
||||||
|
creds := suite.its.ac.Credentials
|
||||||
|
creds.AzureTenantID = "ZIM"
|
||||||
|
|
||||||
|
return creds
|
||||||
|
},
|
||||||
|
expectErr: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing tenant ID",
|
||||||
|
creds: func() account.M365Config {
|
||||||
|
creds := suite.its.ac.Credentials
|
||||||
|
creds.AzureTenantID = ""
|
||||||
|
|
||||||
|
return creds
|
||||||
|
},
|
||||||
|
expectErr: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad client ID",
|
||||||
|
creds: func() account.M365Config {
|
||||||
|
creds := suite.its.ac.Credentials
|
||||||
|
creds.AzureClientID = "GIR"
|
||||||
|
|
||||||
|
return creds
|
||||||
|
},
|
||||||
|
expectErr: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing client ID",
|
||||||
|
creds: func() account.M365Config {
|
||||||
|
creds := suite.its.ac.Credentials
|
||||||
|
creds.AzureClientID = ""
|
||||||
|
|
||||||
|
return creds
|
||||||
|
},
|
||||||
|
expectErr: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad client secret",
|
||||||
|
creds: func() account.M365Config {
|
||||||
|
creds := suite.its.ac.Credentials
|
||||||
|
creds.AzureClientSecret = "MY TALLEST"
|
||||||
|
|
||||||
|
return creds
|
||||||
|
},
|
||||||
|
expectErr: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing client secret",
|
||||||
|
creds: func() account.M365Config {
|
||||||
|
creds := suite.its.ac.Credentials
|
||||||
|
creds.AzureClientSecret = ""
|
||||||
|
|
||||||
|
return creds
|
||||||
|
},
|
||||||
|
expectErr: require.Error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
ac, err := api.NewClient(suite.its.ac.Credentials, control.DefaultOptions())
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
ac.Credentials = test.creds()
|
||||||
|
|
||||||
|
err = ac.Access().GetToken(ctx)
|
||||||
|
test.expectErr(t, err, clues.ToCore(err))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
@ -119,6 +120,16 @@ func (c Client) Get(
|
|||||||
return c.Requester.Request(ctx, http.MethodGet, url, nil, headers)
|
return c.Requester.Request(ctx, http.MethodGet, url, nil, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get performs an ad-hoc get request using its graph.Requester
|
||||||
|
func (c Client) Post(
|
||||||
|
ctx context.Context,
|
||||||
|
url string,
|
||||||
|
headers map[string]string,
|
||||||
|
body io.Reader,
|
||||||
|
) (*http.Response, error) {
|
||||||
|
return c.Requester.Request(ctx, http.MethodGet, url, body, headers)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// per-call config
|
// per-call config
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user