Add stats to export operations (#4461)
Have a way to gather stats about the exported data.
Users can now call `ExportOperation.GetStats()` at the end of the run to get the stats for the operations. The data will be in the format `map[path.CategoryType]data.KindStats` whre `KindStats` is:
```go
type KindStats struct {
BytesRead int64
ResourceCount int64
}
```
---
#### Does this PR need a docs update or release note?
- [ ] ✅ Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [x] ⛔ No
#### Type of change
<!--- Please check the type of change your PR introduces: --->
- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup
#### Issue(s)
<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* https://github.com/alcionai/corso/issues/4311
#### Test Plan
<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [x] ⚡ Unit test
- [ ] 💚 E2E
This commit is contained in:
parent
107b6883d5
commit
040257f8be
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Skips graph calls for expired item download URLs.
|
- Skips graph calls for expired item download URLs.
|
||||||
|
- Export operation now shows the stats at the end of the run
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Catch and report cases where a protected resource is locked out of access. SDK consumers have a new errs sentinel that allows them to check for this case.
|
- Catch and report cases where a protected resource is locked out of access. SDK consumers have a new errs sentinel that allows them to check for this case.
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/cli/flags"
|
"github.com/alcionai/corso/src/cli/flags"
|
||||||
@ -110,5 +111,14 @@ func runExport(
|
|||||||
return Only(ctx, err)
|
return Only(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stats := eo.GetStats()
|
||||||
|
if len(stats) > 0 {
|
||||||
|
Infof(ctx, "\nExport details")
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, s := range stats {
|
||||||
|
Infof(ctx, "%s: %d items (%s)", k.HumanString(), s.ResourceCount, humanize.Bytes(uint64(s.BytesRead)))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
package data
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
)
|
||||||
|
|
||||||
type CollectionStats struct {
|
type CollectionStats struct {
|
||||||
Folders,
|
Folders,
|
||||||
Objects,
|
Objects,
|
||||||
@ -15,3 +22,68 @@ func (cs CollectionStats) IsZero() bool {
|
|||||||
func (cs CollectionStats) String() string {
|
func (cs CollectionStats) String() string {
|
||||||
return cs.Details
|
return cs.Details
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type KindStats struct {
|
||||||
|
BytesRead int64
|
||||||
|
ResourceCount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExportStats struct {
|
||||||
|
// data is kept private so that we can enforce atomic int updates
|
||||||
|
data map[path.CategoryType]KindStats
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *ExportStats) UpdateBytes(kind path.CategoryType, bytesRead int64) {
|
||||||
|
if es.data == nil {
|
||||||
|
es.data = map[path.CategoryType]KindStats{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ks := es.data[kind]
|
||||||
|
atomic.AddInt64(&ks.BytesRead, bytesRead)
|
||||||
|
es.data[kind] = ks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *ExportStats) UpdateResourceCount(kind path.CategoryType) {
|
||||||
|
if es.data == nil {
|
||||||
|
es.data = map[path.CategoryType]KindStats{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ks := es.data[kind]
|
||||||
|
atomic.AddInt64(&ks.ResourceCount, 1)
|
||||||
|
es.data[kind] = ks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *ExportStats) GetStats() map[path.CategoryType]KindStats {
|
||||||
|
return es.data
|
||||||
|
}
|
||||||
|
|
||||||
|
type statsReader struct {
|
||||||
|
io.ReadCloser
|
||||||
|
kind path.CategoryType
|
||||||
|
stats *ExportStats
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sr *statsReader) Read(p []byte) (int, error) {
|
||||||
|
n, err := sr.ReadCloser.Read(p)
|
||||||
|
sr.stats.UpdateBytes(sr.kind, int64(n))
|
||||||
|
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a function that will take a reader and return a reader that
|
||||||
|
// will update the stats
|
||||||
|
func ReaderWithStats(
|
||||||
|
reader io.ReadCloser,
|
||||||
|
kind path.CategoryType,
|
||||||
|
stats *ExportStats,
|
||||||
|
) io.ReadCloser {
|
||||||
|
if reader == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &statsReader{
|
||||||
|
ReadCloser: reader,
|
||||||
|
kind: kind,
|
||||||
|
stats: stats,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -12,18 +12,21 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
"github.com/alcionai/corso/src/pkg/export"
|
"github.com/alcionai/corso/src/pkg/export"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewExportCollection(
|
func NewExportCollection(
|
||||||
baseDir string,
|
baseDir string,
|
||||||
backingCollection []data.RestoreCollection,
|
backingCollection []data.RestoreCollection,
|
||||||
backupVersion int,
|
backupVersion int,
|
||||||
|
stats *data.ExportStats,
|
||||||
) export.Collectioner {
|
) export.Collectioner {
|
||||||
return export.BaseCollection{
|
return export.BaseCollection{
|
||||||
BaseDir: baseDir,
|
BaseDir: baseDir,
|
||||||
BackingCollection: backingCollection,
|
BackingCollection: backingCollection,
|
||||||
BackupVersion: backupVersion,
|
BackupVersion: backupVersion,
|
||||||
Stream: streamItems,
|
Stream: streamItems,
|
||||||
|
Stats: stats,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,6 +37,7 @@ func streamItems(
|
|||||||
backupVersion int,
|
backupVersion int,
|
||||||
cec control.ExportConfig,
|
cec control.ExportConfig,
|
||||||
ch chan<- export.Item,
|
ch chan<- export.Item,
|
||||||
|
stats *data.ExportStats,
|
||||||
) {
|
) {
|
||||||
defer close(ch)
|
defer close(ch)
|
||||||
|
|
||||||
@ -47,11 +51,22 @@ func streamItems(
|
|||||||
}
|
}
|
||||||
|
|
||||||
name, err := getItemName(ctx, itemUUID, backupVersion, rc)
|
name, err := getItemName(ctx, itemUUID, backupVersion, rc)
|
||||||
|
if err != nil {
|
||||||
|
ch <- export.Item{
|
||||||
|
ID: itemUUID,
|
||||||
|
Error: err,
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.UpdateResourceCount(path.FilesCategory)
|
||||||
|
body := data.ReaderWithStats(item.ToReader(), path.FilesCategory, stats)
|
||||||
|
|
||||||
ch <- export.Item{
|
ch <- export.Item{
|
||||||
ID: itemUUID,
|
ID: itemUUID,
|
||||||
Name: name,
|
Name: name,
|
||||||
Body: item.ToReader(),
|
Body: body,
|
||||||
Error: err,
|
Error: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
"github.com/alcionai/corso/src/pkg/export"
|
"github.com/alcionai/corso/src/pkg/export"
|
||||||
"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/services/m365/api"
|
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ func NewExportCollection(
|
|||||||
backingCollections []data.RestoreCollection,
|
backingCollections []data.RestoreCollection,
|
||||||
backupVersion int,
|
backupVersion int,
|
||||||
cec control.ExportConfig,
|
cec control.ExportConfig,
|
||||||
|
stats *data.ExportStats,
|
||||||
) export.Collectioner {
|
) export.Collectioner {
|
||||||
return export.BaseCollection{
|
return export.BaseCollection{
|
||||||
BaseDir: baseDir,
|
BaseDir: baseDir,
|
||||||
@ -30,6 +32,7 @@ func NewExportCollection(
|
|||||||
BackupVersion: backupVersion,
|
BackupVersion: backupVersion,
|
||||||
Cfg: cec,
|
Cfg: cec,
|
||||||
Stream: streamItems,
|
Stream: streamItems,
|
||||||
|
Stats: stats,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,6 +43,7 @@ func streamItems(
|
|||||||
backupVersion int,
|
backupVersion int,
|
||||||
cec control.ExportConfig,
|
cec control.ExportConfig,
|
||||||
ch chan<- export.Item,
|
ch chan<- export.Item,
|
||||||
|
stats *data.ExportStats,
|
||||||
) {
|
) {
|
||||||
defer close(ch)
|
defer close(ch)
|
||||||
|
|
||||||
@ -54,6 +58,9 @@ func streamItems(
|
|||||||
Error: err,
|
Error: err,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
stats.UpdateResourceCount(path.ChannelMessagesCategory)
|
||||||
|
body = data.ReaderWithStats(body, path.ChannelMessagesCategory, stats)
|
||||||
|
|
||||||
ch <- export.Item{
|
ch <- export.Item{
|
||||||
ID: item.ID(),
|
ID: item.ID(),
|
||||||
// channel message items have no name
|
// channel message items have no name
|
||||||
|
|||||||
@ -90,7 +90,8 @@ func (suite *ExportUnitSuite) TestStreamItems() {
|
|||||||
[]data.RestoreCollection{test.backingColl},
|
[]data.RestoreCollection{test.backingColl},
|
||||||
version.NoBackup,
|
version.NoBackup,
|
||||||
control.DefaultExportConfig(),
|
control.DefaultExportConfig(),
|
||||||
ch)
|
ch,
|
||||||
|
&data.ExportStats{})
|
||||||
|
|
||||||
var (
|
var (
|
||||||
itm export.Item
|
itm export.Item
|
||||||
|
|||||||
@ -27,6 +27,7 @@ func (ctrl *Controller) ProduceExportCollections(
|
|||||||
exportCfg control.ExportConfig,
|
exportCfg control.ExportConfig,
|
||||||
opts control.Options,
|
opts control.Options,
|
||||||
dcs []data.RestoreCollection,
|
dcs []data.RestoreCollection,
|
||||||
|
stats *data.ExportStats,
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) ([]export.Collectioner, error) {
|
) ([]export.Collectioner, error) {
|
||||||
ctx, end := diagnostics.Span(ctx, "m365:export")
|
ctx, end := diagnostics.Span(ctx, "m365:export")
|
||||||
@ -51,6 +52,7 @@ func (ctrl *Controller) ProduceExportCollections(
|
|||||||
opts,
|
opts,
|
||||||
dcs,
|
dcs,
|
||||||
deets,
|
deets,
|
||||||
|
stats,
|
||||||
errs)
|
errs)
|
||||||
case selectors.ServiceSharePoint:
|
case selectors.ServiceSharePoint:
|
||||||
expCollections, err = sharepoint.ProduceExportCollections(
|
expCollections, err = sharepoint.ProduceExportCollections(
|
||||||
@ -61,6 +63,7 @@ func (ctrl *Controller) ProduceExportCollections(
|
|||||||
dcs,
|
dcs,
|
||||||
ctrl.backupDriveIDNames,
|
ctrl.backupDriveIDNames,
|
||||||
deets,
|
deets,
|
||||||
|
stats,
|
||||||
errs)
|
errs)
|
||||||
case selectors.ServiceGroups:
|
case selectors.ServiceGroups:
|
||||||
expCollections, err = groups.ProduceExportCollections(
|
expCollections, err = groups.ProduceExportCollections(
|
||||||
@ -72,6 +75,7 @@ func (ctrl *Controller) ProduceExportCollections(
|
|||||||
ctrl.backupDriveIDNames,
|
ctrl.backupDriveIDNames,
|
||||||
ctrl.backupSiteIDWebURL,
|
ctrl.backupSiteIDWebURL,
|
||||||
deets,
|
deets,
|
||||||
|
stats,
|
||||||
errs)
|
errs)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -90,6 +90,7 @@ func (ctrl Controller) ProduceExportCollections(
|
|||||||
_ control.ExportConfig,
|
_ control.ExportConfig,
|
||||||
_ control.Options,
|
_ control.Options,
|
||||||
_ []data.RestoreCollection,
|
_ []data.RestoreCollection,
|
||||||
|
_ *data.ExportStats,
|
||||||
_ *fault.Bus,
|
_ *fault.Bus,
|
||||||
) ([]export.Collectioner, error) {
|
) ([]export.Collectioner, error) {
|
||||||
return nil, ctrl.Err
|
return nil, ctrl.Err
|
||||||
|
|||||||
@ -29,6 +29,7 @@ func ProduceExportCollections(
|
|||||||
backupDriveIDNames idname.Cacher,
|
backupDriveIDNames idname.Cacher,
|
||||||
backupSiteIDWebURL idname.Cacher,
|
backupSiteIDWebURL idname.Cacher,
|
||||||
deets *details.Builder,
|
deets *details.Builder,
|
||||||
|
stats *data.ExportStats,
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) ([]export.Collectioner, error) {
|
) ([]export.Collectioner, error) {
|
||||||
var (
|
var (
|
||||||
@ -52,7 +53,8 @@ func ProduceExportCollections(
|
|||||||
path.Builder{}.Append(folders...).String(),
|
path.Builder{}.Append(folders...).String(),
|
||||||
[]data.RestoreCollection{restoreColl},
|
[]data.RestoreCollection{restoreColl},
|
||||||
backupVersion,
|
backupVersion,
|
||||||
exportCfg)
|
exportCfg,
|
||||||
|
stats)
|
||||||
case path.LibrariesCategory:
|
case path.LibrariesCategory:
|
||||||
drivePath, err := path.ToDrivePath(restoreColl.FullPath())
|
drivePath, err := path.ToDrivePath(restoreColl.FullPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -91,7 +93,8 @@ func ProduceExportCollections(
|
|||||||
coll = drive.NewExportCollection(
|
coll = drive.NewExportCollection(
|
||||||
baseDir.String(),
|
baseDir.String(),
|
||||||
[]data.RestoreCollection{restoreColl},
|
[]data.RestoreCollection{restoreColl},
|
||||||
backupVersion)
|
backupVersion,
|
||||||
|
stats)
|
||||||
default:
|
default:
|
||||||
el.AddRecoverable(
|
el.AddRecoverable(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
@ -64,8 +65,8 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_messages() {
|
|||||||
itemID = "itemID"
|
itemID = "itemID"
|
||||||
containerName = "channelID"
|
containerName = "channelID"
|
||||||
dii = groupMock.ItemInfo()
|
dii = groupMock.ItemInfo()
|
||||||
body = io.NopCloser(bytes.NewBufferString(
|
content = `{"displayname": "` + dii.Groups.ItemName + `"}`
|
||||||
`{"displayname": "` + dii.Groups.ItemName + `"}`))
|
body = io.NopCloser(bytes.NewBufferString(content))
|
||||||
exportCfg = control.ExportConfig{}
|
exportCfg = control.ExportConfig{}
|
||||||
expectedPath = path.ChannelMessagesCategory.HumanString() + "/" + containerName
|
expectedPath = path.ChannelMessagesCategory.HumanString() + "/" + containerName
|
||||||
expectedItems = []export.Item{
|
expectedItems = []export.Item{
|
||||||
@ -96,6 +97,8 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_messages() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stats := data.ExportStats{}
|
||||||
|
|
||||||
ecs, err := ProduceExportCollections(
|
ecs, err := ProduceExportCollections(
|
||||||
ctx,
|
ctx,
|
||||||
int(version.Backup),
|
int(version.Backup),
|
||||||
@ -105,6 +108,7 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_messages() {
|
|||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
|
&stats,
|
||||||
fault.New(true))
|
fault.New(true))
|
||||||
assert.NoError(t, err, "export collections error")
|
assert.NoError(t, err, "export collections error")
|
||||||
assert.Len(t, ecs, 1, "num of collections")
|
assert.Len(t, ecs, 1, "num of collections")
|
||||||
@ -113,7 +117,15 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_messages() {
|
|||||||
|
|
||||||
fitems := []export.Item{}
|
fitems := []export.Item{}
|
||||||
|
|
||||||
|
size := 0
|
||||||
|
|
||||||
for item := range ecs[0].Items(ctx) {
|
for item := range ecs[0].Items(ctx) {
|
||||||
|
b, err := io.ReadAll(item.Body)
|
||||||
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
// count up size for tests
|
||||||
|
size += len(b)
|
||||||
|
|
||||||
// have to nil out body, otherwise assert fails due to
|
// have to nil out body, otherwise assert fails due to
|
||||||
// pointer memory location differences
|
// pointer memory location differences
|
||||||
item.Body = nil
|
item.Body = nil
|
||||||
@ -121,6 +133,11 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_messages() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, expectedItems, fitems, "items")
|
assert.Equal(t, expectedItems, fitems, "items")
|
||||||
|
|
||||||
|
expectedStats := data.ExportStats{}
|
||||||
|
expectedStats.UpdateBytes(path.ChannelMessagesCategory, int64(size))
|
||||||
|
expectedStats.UpdateResourceCount(path.ChannelMessagesCategory)
|
||||||
|
assert.Equal(t, expectedStats, stats, "stats")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ExportUnitSuite) TestExportRestoreCollections_libraries() {
|
func (suite *ExportUnitSuite) TestExportRestoreCollections_libraries() {
|
||||||
@ -182,6 +199,8 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_libraries() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stats := data.ExportStats{}
|
||||||
|
|
||||||
ecs, err := ProduceExportCollections(
|
ecs, err := ProduceExportCollections(
|
||||||
ctx,
|
ctx,
|
||||||
int(version.Backup),
|
int(version.Backup),
|
||||||
@ -191,6 +210,7 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_libraries() {
|
|||||||
driveNameCache,
|
driveNameCache,
|
||||||
siteWebURLCache,
|
siteWebURLCache,
|
||||||
nil,
|
nil,
|
||||||
|
&stats,
|
||||||
fault.New(true))
|
fault.New(true))
|
||||||
assert.NoError(t, err, "export collections error")
|
assert.NoError(t, err, "export collections error")
|
||||||
assert.Len(t, ecs, 1, "num of collections")
|
assert.Len(t, ecs, 1, "num of collections")
|
||||||
@ -199,9 +219,24 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_libraries() {
|
|||||||
|
|
||||||
fitems := []export.Item{}
|
fitems := []export.Item{}
|
||||||
|
|
||||||
|
size := 0
|
||||||
|
|
||||||
for item := range ecs[0].Items(ctx) {
|
for item := range ecs[0].Items(ctx) {
|
||||||
|
// unwrap the body from stats reader
|
||||||
|
b, err := io.ReadAll(item.Body)
|
||||||
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
size += len(b)
|
||||||
|
bitem := io.NopCloser(bytes.NewBuffer(b))
|
||||||
|
item.Body = bitem
|
||||||
|
|
||||||
fitems = append(fitems, item)
|
fitems = append(fitems, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, expectedItems, fitems, "items")
|
assert.Equal(t, expectedItems, fitems, "items")
|
||||||
|
|
||||||
|
expectedStats := data.ExportStats{}
|
||||||
|
expectedStats.UpdateBytes(path.FilesCategory, int64(size))
|
||||||
|
expectedStats.UpdateResourceCount(path.FilesCategory)
|
||||||
|
assert.Equal(t, expectedStats, stats, "stats")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ func ProduceExportCollections(
|
|||||||
opts control.Options,
|
opts control.Options,
|
||||||
dcs []data.RestoreCollection,
|
dcs []data.RestoreCollection,
|
||||||
deets *details.Builder,
|
deets *details.Builder,
|
||||||
|
stats *data.ExportStats,
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) ([]export.Collectioner, error) {
|
) ([]export.Collectioner, error) {
|
||||||
var (
|
var (
|
||||||
@ -43,7 +44,8 @@ func ProduceExportCollections(
|
|||||||
drive.NewExportCollection(
|
drive.NewExportCollection(
|
||||||
baseDir.String(),
|
baseDir.String(),
|
||||||
[]data.RestoreCollection{dc},
|
[]data.RestoreCollection{dc},
|
||||||
backupVersion))
|
backupVersion,
|
||||||
|
stats))
|
||||||
}
|
}
|
||||||
|
|
||||||
return ec, el.Failure()
|
return ec, el.Failure()
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
@ -19,6 +20,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
"github.com/alcionai/corso/src/pkg/export"
|
"github.com/alcionai/corso/src/pkg/export"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ExportUnitSuite struct {
|
type ExportUnitSuite struct {
|
||||||
@ -245,15 +247,32 @@ func (suite *ExportUnitSuite) TestGetItems() {
|
|||||||
ctx, flush := tester.NewContext(t)
|
ctx, flush := tester.NewContext(t)
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
|
stats := data.ExportStats{}
|
||||||
ec := drive.NewExportCollection(
|
ec := drive.NewExportCollection(
|
||||||
"",
|
"",
|
||||||
[]data.RestoreCollection{test.backingCollection},
|
[]data.RestoreCollection{test.backingCollection},
|
||||||
test.version)
|
test.version,
|
||||||
|
&stats)
|
||||||
|
|
||||||
items := ec.Items(ctx)
|
items := ec.Items(ctx)
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
size := 0
|
||||||
fitems := []export.Item{}
|
fitems := []export.Item{}
|
||||||
|
|
||||||
for item := range items {
|
for item := range items {
|
||||||
|
if item.Error == nil {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.Body != nil {
|
||||||
|
b, err := io.ReadAll(item.Body)
|
||||||
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
size += len(b)
|
||||||
|
item.Body = io.NopCloser(bytes.NewBuffer(b))
|
||||||
|
}
|
||||||
|
|
||||||
fitems = append(fitems, item)
|
fitems = append(fitems, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,6 +287,19 @@ func (suite *ExportUnitSuite) TestGetItems() {
|
|||||||
assert.Equal(t, test.expectedItems[i].Body, item.Body, "body")
|
assert.Equal(t, test.expectedItems[i].Body, item.Body, "body")
|
||||||
assert.ErrorIs(t, item.Error, test.expectedItems[i].Error)
|
assert.ErrorIs(t, item.Error, test.expectedItems[i].Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var expectedStats data.ExportStats
|
||||||
|
|
||||||
|
if size+count > 0 { // it is only initialized if we have something
|
||||||
|
expectedStats = data.ExportStats{}
|
||||||
|
expectedStats.UpdateBytes(path.FilesCategory, int64(size))
|
||||||
|
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
expectedStats.UpdateResourceCount(path.FilesCategory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expectedStats, stats, "stats")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -312,6 +344,8 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stats := data.ExportStats{}
|
||||||
|
|
||||||
ecs, err := ProduceExportCollections(
|
ecs, err := ProduceExportCollections(
|
||||||
ctx,
|
ctx,
|
||||||
int(version.Backup),
|
int(version.Backup),
|
||||||
@ -319,14 +353,30 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections() {
|
|||||||
control.DefaultOptions(),
|
control.DefaultOptions(),
|
||||||
dcs,
|
dcs,
|
||||||
nil,
|
nil,
|
||||||
|
&stats,
|
||||||
fault.New(true))
|
fault.New(true))
|
||||||
assert.NoError(t, err, "export collections error")
|
assert.NoError(t, err, "export collections error")
|
||||||
assert.Len(t, ecs, 1, "num of collections")
|
assert.Len(t, ecs, 1, "num of collections")
|
||||||
|
|
||||||
fitems := []export.Item{}
|
fitems := []export.Item{}
|
||||||
|
size := 0
|
||||||
|
|
||||||
for item := range ecs[0].Items(ctx) {
|
for item := range ecs[0].Items(ctx) {
|
||||||
|
// unwrap the body from stats reader
|
||||||
|
b, err := io.ReadAll(item.Body)
|
||||||
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
size += len(b)
|
||||||
|
bitem := io.NopCloser(bytes.NewBuffer(b))
|
||||||
|
item.Body = bitem
|
||||||
|
|
||||||
fitems = append(fitems, item)
|
fitems = append(fitems, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, expectedItems, fitems, "items")
|
assert.Equal(t, expectedItems, fitems, "items")
|
||||||
|
|
||||||
|
expectedStats := data.ExportStats{}
|
||||||
|
expectedStats.UpdateBytes(path.FilesCategory, int64(size))
|
||||||
|
expectedStats.UpdateResourceCount(path.FilesCategory)
|
||||||
|
assert.Equal(t, expectedStats, stats, "stats")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ func ProduceExportCollections(
|
|||||||
dcs []data.RestoreCollection,
|
dcs []data.RestoreCollection,
|
||||||
backupDriveIDNames idname.CacheBuilder,
|
backupDriveIDNames idname.CacheBuilder,
|
||||||
deets *details.Builder,
|
deets *details.Builder,
|
||||||
|
stats *data.ExportStats,
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) ([]export.Collectioner, error) {
|
) ([]export.Collectioner, error) {
|
||||||
var (
|
var (
|
||||||
@ -56,7 +57,8 @@ func ProduceExportCollections(
|
|||||||
drive.NewExportCollection(
|
drive.NewExportCollection(
|
||||||
baseDir.String(),
|
baseDir.String(),
|
||||||
[]data.RestoreCollection{dc},
|
[]data.RestoreCollection{dc},
|
||||||
backupVersion))
|
backupVersion,
|
||||||
|
stats))
|
||||||
}
|
}
|
||||||
|
|
||||||
return ec, el.Failure()
|
return ec, el.Failure()
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
@ -98,6 +99,8 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stats := data.ExportStats{}
|
||||||
|
|
||||||
ecs, err := ProduceExportCollections(
|
ecs, err := ProduceExportCollections(
|
||||||
ctx,
|
ctx,
|
||||||
int(version.Backup),
|
int(version.Backup),
|
||||||
@ -106,6 +109,7 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections() {
|
|||||||
dcs,
|
dcs,
|
||||||
cache,
|
cache,
|
||||||
nil,
|
nil,
|
||||||
|
&stats,
|
||||||
fault.New(true))
|
fault.New(true))
|
||||||
assert.NoError(t, err, "export collections error")
|
assert.NoError(t, err, "export collections error")
|
||||||
assert.Len(t, ecs, 1, "num of collections")
|
assert.Len(t, ecs, 1, "num of collections")
|
||||||
@ -113,9 +117,24 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections() {
|
|||||||
assert.Equal(t, expectedPath, ecs[0].BasePath(), "base dir")
|
assert.Equal(t, expectedPath, ecs[0].BasePath(), "base dir")
|
||||||
|
|
||||||
fitems := []export.Item{}
|
fitems := []export.Item{}
|
||||||
|
size := 0
|
||||||
|
|
||||||
for item := range ecs[0].Items(ctx) {
|
for item := range ecs[0].Items(ctx) {
|
||||||
|
// unwrap the body from stats reader
|
||||||
|
b, err := io.ReadAll(item.Body)
|
||||||
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
size += len(b)
|
||||||
|
bitem := io.NopCloser(bytes.NewBuffer(b))
|
||||||
|
item.Body = bitem
|
||||||
|
|
||||||
fitems = append(fitems, item)
|
fitems = append(fitems, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, expectedItems, fitems, "items")
|
assert.Equal(t, expectedItems, fitems, "items")
|
||||||
|
|
||||||
|
expectedStats := data.ExportStats{}
|
||||||
|
expectedStats.UpdateBytes(path.FilesCategory, int64(size))
|
||||||
|
expectedStats.UpdateResourceCount(path.FilesCategory)
|
||||||
|
assert.Equal(t, expectedStats, stats, "stats")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/export"
|
"github.com/alcionai/corso/src/pkg/export"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"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/selectors"
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
"github.com/alcionai/corso/src/pkg/store"
|
"github.com/alcionai/corso/src/pkg/store"
|
||||||
)
|
)
|
||||||
@ -46,6 +47,7 @@ type ExportOperation struct {
|
|||||||
Selectors selectors.Selector
|
Selectors selectors.Selector
|
||||||
ExportCfg control.ExportConfig
|
ExportCfg control.ExportConfig
|
||||||
Version string
|
Version string
|
||||||
|
stats data.ExportStats
|
||||||
|
|
||||||
acct account.Account
|
acct account.Account
|
||||||
ec inject.ExportConsumer
|
ec inject.ExportConsumer
|
||||||
@ -72,6 +74,7 @@ func NewExportOperation(
|
|||||||
Selectors: sel,
|
Selectors: sel,
|
||||||
Version: "v0",
|
Version: "v0",
|
||||||
ec: ec,
|
ec: ec,
|
||||||
|
stats: data.ExportStats{},
|
||||||
}
|
}
|
||||||
if err := op.validate(); err != nil {
|
if err := op.validate(); err != nil {
|
||||||
return ExportOperation{}, err
|
return ExportOperation{}, err
|
||||||
@ -247,7 +250,7 @@ func (op *ExportOperation) do(
|
|||||||
opStats.resourceCount = 1
|
opStats.resourceCount = 1
|
||||||
opStats.cs = dcs
|
opStats.cs = dcs
|
||||||
|
|
||||||
expCollections, err := exportRestoreCollections(
|
expCollections, err := produceExportCollections(
|
||||||
ctx,
|
ctx,
|
||||||
op.ec,
|
op.ec,
|
||||||
bup.Version,
|
bup.Version,
|
||||||
@ -255,6 +258,9 @@ func (op *ExportOperation) do(
|
|||||||
op.ExportCfg,
|
op.ExportCfg,
|
||||||
op.Options,
|
op.Options,
|
||||||
dcs,
|
dcs,
|
||||||
|
// We also have opStats, but that tracks different data.
|
||||||
|
// Maybe we can look into merging them some time in the future.
|
||||||
|
&op.stats,
|
||||||
op.Errors)
|
op.Errors)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, clues.Stack(err)
|
return nil, clues.Stack(err)
|
||||||
@ -310,11 +316,19 @@ func (op *ExportOperation) finalizeMetrics(
|
|||||||
return op.Errors.Failure()
|
return op.Errors.Failure()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetStats returns the stats of the export operation. You should only
|
||||||
|
// be calling this once the export collections have been read and process
|
||||||
|
// as the data that will be available here will be the data that was read
|
||||||
|
// and processed.
|
||||||
|
func (op *ExportOperation) GetStats() map[path.CategoryType]data.KindStats {
|
||||||
|
return op.stats.GetStats()
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Exporter funcs
|
// Exporter funcs
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func exportRestoreCollections(
|
func produceExportCollections(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
ec inject.ExportConsumer,
|
ec inject.ExportConsumer,
|
||||||
backupVersion int,
|
backupVersion int,
|
||||||
@ -322,6 +336,7 @@ func exportRestoreCollections(
|
|||||||
exportCfg control.ExportConfig,
|
exportCfg control.ExportConfig,
|
||||||
opts control.Options,
|
opts control.Options,
|
||||||
dcs []data.RestoreCollection,
|
dcs []data.RestoreCollection,
|
||||||
|
exportStats *data.ExportStats,
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) ([]export.Collectioner, error) {
|
) ([]export.Collectioner, error) {
|
||||||
complete := observe.MessageWithCompletion(ctx, "Preparing export")
|
complete := observe.MessageWithCompletion(ctx, "Preparing export")
|
||||||
@ -337,6 +352,7 @@ func exportRestoreCollections(
|
|||||||
exportCfg,
|
exportCfg,
|
||||||
opts,
|
opts,
|
||||||
dcs,
|
dcs,
|
||||||
|
exportStats,
|
||||||
errs)
|
errs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, clues.Wrap(err, "exporting collections")
|
return nil, clues.Wrap(err, "exporting collections")
|
||||||
|
|||||||
@ -88,6 +88,7 @@ type (
|
|||||||
exportCfg control.ExportConfig,
|
exportCfg control.ExportConfig,
|
||||||
opts control.Options,
|
opts control.Options,
|
||||||
dcs []data.RestoreCollection,
|
dcs []data.RestoreCollection,
|
||||||
|
stats *data.ExportStats,
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) ([]export.Collectioner, error)
|
) ([]export.Collectioner, error)
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,8 @@ type itemStreamer func(
|
|||||||
backingColls []data.RestoreCollection,
|
backingColls []data.RestoreCollection,
|
||||||
backupVersion int,
|
backupVersion int,
|
||||||
cfg control.ExportConfig,
|
cfg control.ExportConfig,
|
||||||
ch chan<- Item)
|
ch chan<- Item,
|
||||||
|
stats *data.ExportStats)
|
||||||
|
|
||||||
// BaseCollection holds the foundational details of an export collection.
|
// BaseCollection holds the foundational details of an export collection.
|
||||||
type BaseCollection struct {
|
type BaseCollection struct {
|
||||||
@ -45,6 +46,8 @@ type BaseCollection struct {
|
|||||||
Cfg control.ExportConfig
|
Cfg control.ExportConfig
|
||||||
|
|
||||||
Stream itemStreamer
|
Stream itemStreamer
|
||||||
|
|
||||||
|
Stats *data.ExportStats
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bc BaseCollection) BasePath() string {
|
func (bc BaseCollection) BasePath() string {
|
||||||
@ -53,7 +56,7 @@ func (bc BaseCollection) BasePath() string {
|
|||||||
|
|
||||||
func (bc BaseCollection) Items(ctx context.Context) <-chan Item {
|
func (bc BaseCollection) Items(ctx context.Context) <-chan Item {
|
||||||
ch := make(chan Item)
|
ch := make(chan Item)
|
||||||
go bc.Stream(ctx, bc.BackingCollection, bc.BackupVersion, bc.Cfg, ch)
|
go bc.Stream(ctx, bc.BackingCollection, bc.BackupVersion, bc.Cfg, ch, bc.Stats)
|
||||||
|
|
||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user