Exch generic item (#4351)
Use generic item structs in Exchange code --- #### 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 - [ ] 🌻 Feature - [ ] 🐛 Bugfix - [ ] 🗺️ Documentation - [ ] 🤖 Supportability/Tests - [ ] 💻 CI/Deployment - [x] 🧹 Tech Debt/Cleanup #### Issue(s) * #4191 #### Test Plan - [ ] 💪 Manual - [x] ⚡ Unit test - [x] 💚 E2E
This commit is contained in:
parent
0df876d106
commit
cf3744bcfb
@ -21,7 +21,7 @@ func NewDeletedItem(itemID string) Item {
|
|||||||
// TODO(ashmrtn): This really doesn't need to be set since deleted items are
|
// TODO(ashmrtn): This really doesn't need to be set since deleted items are
|
||||||
// never passed to the actual storage engine. Setting it for now so tests
|
// never passed to the actual storage engine. Setting it for now so tests
|
||||||
// don't break.
|
// don't break.
|
||||||
modTime: time.Now(),
|
modTime: time.Now().UTC(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
"github.com/spatialcurrent/go-lazy/pkg/lazy"
|
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
@ -27,9 +26,7 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
_ data.BackupCollection = &prefetchCollection{}
|
_ data.BackupCollection = &prefetchCollection{}
|
||||||
_ data.Item = &Item{}
|
_ data.BackupCollection = &lazyFetchCollection{}
|
||||||
_ data.ItemInfo = &Item{}
|
|
||||||
_ data.ItemModTime = &Item{}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -230,11 +227,7 @@ func (col *prefetchCollection) streamItems(
|
|||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
defer func() { <-semaphoreCh }()
|
defer func() { <-semaphoreCh }()
|
||||||
|
|
||||||
stream <- &Item{
|
stream <- data.NewDeletedItem(id)
|
||||||
id: id,
|
|
||||||
modTime: time.Now().UTC(), // removed items have no modTime entry.
|
|
||||||
deleted: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
atomic.AddInt64(&success, 1)
|
atomic.AddInt64(&success, 1)
|
||||||
|
|
||||||
@ -285,12 +278,10 @@ func (col *prefetchCollection) streamItems(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stream <- &Item{
|
stream <- data.NewPrefetchedItem(
|
||||||
id: id,
|
io.NopCloser(bytes.NewReader(itemData)),
|
||||||
message: itemData,
|
id,
|
||||||
info: info,
|
details.ItemInfo{Exchange: info})
|
||||||
modTime: info.Modified,
|
|
||||||
}
|
|
||||||
|
|
||||||
atomic.AddInt64(&success, 1)
|
atomic.AddInt64(&success, 1)
|
||||||
atomic.AddInt64(&totalBytes, info.Size)
|
atomic.AddInt64(&totalBytes, info.Size)
|
||||||
@ -377,11 +368,7 @@ func (col *lazyFetchCollection) streamItems(
|
|||||||
|
|
||||||
// delete all removed items
|
// delete all removed items
|
||||||
for id := range col.removed {
|
for id := range col.removed {
|
||||||
stream <- &Item{
|
stream <- data.NewDeletedItem(id)
|
||||||
id: id,
|
|
||||||
modTime: time.Now().UTC(), // removed items have no modTime entry.
|
|
||||||
deleted: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
atomic.AddInt64(&success, 1)
|
atomic.AddInt64(&success, 1)
|
||||||
|
|
||||||
@ -405,16 +392,19 @@ func (col *lazyFetchCollection) streamItems(
|
|||||||
"service", path.ExchangeService.String(),
|
"service", path.ExchangeService.String(),
|
||||||
"category", col.Category().String())
|
"category", col.Category().String())
|
||||||
|
|
||||||
stream <- &lazyItem{
|
stream <- data.NewLazyItem(
|
||||||
ctx: ictx,
|
ictx,
|
||||||
userID: user,
|
&lazyItemGetter{
|
||||||
id: id,
|
userID: user,
|
||||||
getter: col.getter,
|
itemID: id,
|
||||||
modTime: modTime,
|
getter: col.getter,
|
||||||
immutableIDs: col.Opts().ToggleFeatures.ExchangeImmutableIDs,
|
modTime: modTime,
|
||||||
parentPath: parentPath,
|
immutableIDs: col.Opts().ToggleFeatures.ExchangeImmutableIDs,
|
||||||
errs: errs,
|
parentPath: parentPath,
|
||||||
}
|
},
|
||||||
|
id,
|
||||||
|
modTime,
|
||||||
|
errs)
|
||||||
|
|
||||||
atomic.AddInt64(&success, 1)
|
atomic.AddInt64(&success, 1)
|
||||||
|
|
||||||
@ -424,137 +414,48 @@ func (col *lazyFetchCollection) streamItems(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Item represents a single item retrieved from exchange
|
type lazyItemGetter struct {
|
||||||
type Item struct {
|
getter itemGetterSerializer
|
||||||
id string
|
userID string
|
||||||
// TODO: We may need this to be a "oneOf" of `message`, `contact`, etc.
|
itemID string
|
||||||
// going forward. Using []byte for now but I assume we'll have
|
parentPath string
|
||||||
// some structured type in here (serialization to []byte can be done in `Read`)
|
modTime time.Time
|
||||||
message []byte
|
|
||||||
info *details.ExchangeInfo // temporary change to bring populate function into directory
|
|
||||||
// TODO(ashmrtn): Can probably eventually be sourced from info as there's a
|
|
||||||
// request to provide modtime in ItemInfo structs.
|
|
||||||
modTime time.Time
|
|
||||||
|
|
||||||
// true if the item was marked by graph as deleted.
|
|
||||||
deleted bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Item) ID() string {
|
|
||||||
return i.id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Item) ToReader() io.ReadCloser {
|
|
||||||
return io.NopCloser(bytes.NewReader(i.message))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i Item) Deleted() bool {
|
|
||||||
return i.deleted
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Item) Info() (details.ItemInfo, error) {
|
|
||||||
return details.ItemInfo{Exchange: i.info}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Item) ModTime() time.Time {
|
|
||||||
return i.modTime
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewItem(
|
|
||||||
identifier string,
|
|
||||||
dataBytes []byte,
|
|
||||||
detail details.ExchangeInfo,
|
|
||||||
modTime time.Time,
|
|
||||||
) Item {
|
|
||||||
return Item{
|
|
||||||
id: identifier,
|
|
||||||
message: dataBytes,
|
|
||||||
info: &detail,
|
|
||||||
modTime: modTime,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// lazyItem represents a single item retrieved from exchange that lazily fetches
|
|
||||||
// the item's data when the first call to ToReader().Read() is made.
|
|
||||||
type lazyItem struct {
|
|
||||||
ctx context.Context
|
|
||||||
userID string
|
|
||||||
id string
|
|
||||||
parentPath string
|
|
||||||
getter itemGetterSerializer
|
|
||||||
errs *fault.Bus
|
|
||||||
|
|
||||||
modTime time.Time
|
|
||||||
// info holds the Exchnage-specific details information for this item. Store
|
|
||||||
// a pointer in this struct so the golang garbage collector can collect the
|
|
||||||
// Item struct once kopia is done with it. The ExchangeInfo struct needs to
|
|
||||||
// stick around until the end of the backup though as backup details is
|
|
||||||
// written last.
|
|
||||||
info *details.ExchangeInfo
|
|
||||||
|
|
||||||
immutableIDs bool
|
immutableIDs bool
|
||||||
|
|
||||||
delInFlight bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i lazyItem) ID() string {
|
func (lig *lazyItemGetter) GetData(
|
||||||
return i.id
|
ctx context.Context,
|
||||||
}
|
) (io.ReadCloser, *details.ItemInfo, bool, error) {
|
||||||
|
itemData, info, err := getItemAndInfo(
|
||||||
func (i *lazyItem) ToReader() io.ReadCloser {
|
ctx,
|
||||||
return lazy.NewLazyReadCloser(func() (io.ReadCloser, error) {
|
lig.getter,
|
||||||
itemData, info, err := getItemAndInfo(
|
lig.userID,
|
||||||
i.ctx,
|
lig.itemID,
|
||||||
i.getter,
|
lig.immutableIDs,
|
||||||
i.userID,
|
lig.parentPath)
|
||||||
i.ID(),
|
if err != nil {
|
||||||
i.immutableIDs,
|
// If an item was deleted then return an empty file so we don't fail
|
||||||
i.parentPath)
|
// the backup and return a sentinel error when asked for ItemInfo so
|
||||||
if err != nil {
|
// we don't display the item in the backup.
|
||||||
// If an item was deleted then return an empty file so we don't fail
|
//
|
||||||
// the backup and return a sentinel error when asked for ItemInfo so
|
// The item will be deleted from kopia on the next backup when the
|
||||||
// we don't display the item in the backup.
|
// delta token shows it's removed.
|
||||||
//
|
if graph.IsErrDeletedInFlight(err) {
|
||||||
// The item will be deleted from kopia on the next backup when the
|
logger.CtxErr(ctx, err).Info("item not found")
|
||||||
// delta token shows it's removed.
|
return nil, nil, true, nil
|
||||||
if graph.IsErrDeletedInFlight(err) {
|
|
||||||
logger.CtxErr(i.ctx, err).Info("item not found")
|
|
||||||
|
|
||||||
i.delInFlight = true
|
|
||||||
|
|
||||||
return io.NopCloser(bytes.NewReader([]byte{})), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = clues.Stack(err)
|
|
||||||
i.errs.AddRecoverable(i.ctx, err)
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
i.info = info
|
err = clues.Stack(err)
|
||||||
// Update the mod time to what we already told kopia about. This is required
|
|
||||||
// for proper details merging.
|
|
||||||
i.info.Modified = i.modTime
|
|
||||||
|
|
||||||
return io.NopCloser(bytes.NewReader(itemData)), nil
|
return nil, nil, false, err
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i lazyItem) Deleted() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i lazyItem) Info() (details.ItemInfo, error) {
|
|
||||||
if i.delInFlight {
|
|
||||||
return details.ItemInfo{}, clues.Stack(data.ErrNotFound).WithClues(i.ctx)
|
|
||||||
} else if i.info == nil {
|
|
||||||
return details.ItemInfo{}, clues.New("requesting ItemInfo before data retrieval").
|
|
||||||
WithClues(i.ctx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return details.ItemInfo{Exchange: i.info}, nil
|
// Update the mod time to what we already told kopia about. This is required
|
||||||
}
|
// for proper details merging.
|
||||||
|
info.Modified = lig.modTime
|
||||||
|
|
||||||
func (i lazyItem) ModTime() time.Time {
|
return io.NopCloser(bytes.NewReader(itemData)),
|
||||||
return i.modTime
|
&details.ItemInfo{Exchange: info},
|
||||||
|
false,
|
||||||
|
nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,31 +36,37 @@ func TestCollectionUnitSuite(t *testing.T) {
|
|||||||
suite.Run(t, &CollectionUnitSuite{Suite: tester.NewUnitSuite(t)})
|
suite.Run(t, &CollectionUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *CollectionUnitSuite) TestReader_Valid() {
|
func (suite *CollectionUnitSuite) TestPrefetchedItem_Reader() {
|
||||||
m := []byte("test message")
|
table := []struct {
|
||||||
description := "aFile"
|
name string
|
||||||
ed := &Item{id: description, message: m}
|
readData []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "HasData",
|
||||||
|
readData: []byte("test message"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty",
|
||||||
|
readData: []byte{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
for _, test := range table {
|
||||||
_, err := buf.ReadFrom(ed.ToReader())
|
suite.Run(test.name, func() {
|
||||||
assert.NoError(suite.T(), err, clues.ToCore(err))
|
t := suite.T()
|
||||||
assert.Equal(suite.T(), buf.Bytes(), m)
|
|
||||||
assert.Equal(suite.T(), description, ed.ID())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *CollectionUnitSuite) TestReader_Empty() {
|
ed := data.NewPrefetchedItem(
|
||||||
var (
|
io.NopCloser(bytes.NewReader(test.readData)),
|
||||||
empty []byte
|
"itemID",
|
||||||
expected int64
|
details.ItemInfo{})
|
||||||
t = suite.T()
|
|
||||||
)
|
|
||||||
|
|
||||||
ed := &Item{message: empty}
|
buf := &bytes.Buffer{}
|
||||||
buf := &bytes.Buffer{}
|
_, err := buf.ReadFrom(ed.ToReader())
|
||||||
received, err := buf.ReadFrom(ed.ToReader())
|
assert.NoError(t, err, "reading data: %v", clues.ToCore(err))
|
||||||
|
assert.Equal(t, test.readData, buf.Bytes(), "read data")
|
||||||
assert.Equal(t, expected, received)
|
assert.Equal(t, "itemID", ed.ID(), "item ID")
|
||||||
assert.NoError(t, err, clues.ToCore(err))
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *CollectionUnitSuite) TestNewCollection_state() {
|
func (suite *CollectionUnitSuite) TestNewCollection_state() {
|
||||||
@ -480,9 +486,14 @@ func (suite *CollectionUnitSuite) TestLazyItem_NoRead_GetInfo_Errors() {
|
|||||||
ctx, flush := tester.NewContext(t)
|
ctx, flush := tester.NewContext(t)
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
li := lazyItem{ctx: ctx}
|
li := data.NewLazyItem(
|
||||||
|
ctx,
|
||||||
|
nil,
|
||||||
|
"itemID",
|
||||||
|
time.Now(),
|
||||||
|
fault.New(true))
|
||||||
|
|
||||||
_, err := li.Info()
|
_, err := li.(data.ItemInfo).Info()
|
||||||
assert.Error(suite.T(), err, "Info without reading data should error")
|
assert.Error(suite.T(), err, "Info without reading data should error")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -558,30 +569,37 @@ func (suite *CollectionUnitSuite) TestLazyItem() {
|
|||||||
SerializeErr: test.serializeErr,
|
SerializeErr: test.serializeErr,
|
||||||
}
|
}
|
||||||
|
|
||||||
li := &lazyItem{
|
li := data.NewLazyItem(
|
||||||
ctx: ctx,
|
ctx,
|
||||||
userID: "userID",
|
&lazyItemGetter{
|
||||||
id: "itemID",
|
userID: "userID",
|
||||||
parentPath: parentPath,
|
itemID: "itemID",
|
||||||
getter: getter,
|
getter: getter,
|
||||||
errs: fault.New(true),
|
modTime: test.modTime,
|
||||||
modTime: test.modTime,
|
immutableIDs: false,
|
||||||
immutableIDs: false,
|
parentPath: parentPath,
|
||||||
}
|
},
|
||||||
|
"itemID",
|
||||||
|
test.modTime,
|
||||||
|
fault.New(true))
|
||||||
|
|
||||||
assert.False(t, li.Deleted(), "item shouldn't be marked deleted")
|
assert.False(t, li.Deleted(), "item shouldn't be marked deleted")
|
||||||
assert.Equal(t, test.modTime, li.ModTime(), "item mod time")
|
assert.Equal(
|
||||||
|
t,
|
||||||
|
test.modTime,
|
||||||
|
li.(data.ItemModTime).ModTime(),
|
||||||
|
"item mod time")
|
||||||
|
|
||||||
data, err := io.ReadAll(li.ToReader())
|
readData, err := io.ReadAll(li.ToReader())
|
||||||
if test.expectReadErrType == nil {
|
if test.expectReadErrType == nil {
|
||||||
assert.NoError(t, err, "reading item data: %v", clues.ToCore(err))
|
assert.NoError(t, err, "reading item data: %v", clues.ToCore(err))
|
||||||
} else {
|
} else {
|
||||||
assert.ErrorIs(t, err, test.expectReadErrType, "read error")
|
assert.ErrorIs(t, err, test.expectReadErrType, "read error")
|
||||||
}
|
}
|
||||||
|
|
||||||
test.dataCheck(t, data, "read item data")
|
test.dataCheck(t, readData, "read item data")
|
||||||
|
|
||||||
info, err := li.Info()
|
info, err := li.(data.ItemInfo).Info()
|
||||||
|
|
||||||
// Didn't expect an error getting info, it should be valid.
|
// Didn't expect an error getting info, it should be valid.
|
||||||
if !test.expectInfoErr {
|
if !test.expectInfoErr {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user