tweaks and fixes found while testing (#3735)
a variety of small updates that came from manual testing of restore with various collision and destination combinations. --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🐛 Bugfix #### Issue(s) * #3562 #### Test Plan - [x] 💪 Manual
This commit is contained in:
parent
1255f06aed
commit
5ea194dc87
@ -84,14 +84,17 @@ func runRestore(
|
|||||||
return Only(ctx, clues.Wrap(err, "Failed to run "+serviceName+" restore"))
|
return Only(ctx, clues.Wrap(err, "Failed to run "+serviceName+" restore"))
|
||||||
}
|
}
|
||||||
|
|
||||||
Info(ctx, "Completed Restore:")
|
Info(ctx, "Restore Complete")
|
||||||
|
|
||||||
skipped := ro.Counter.Get(count.CollisionSkip)
|
skipped := ro.Counter.Get(count.CollisionSkip)
|
||||||
if skipped > 0 {
|
if skipped > 0 {
|
||||||
Infof(ctx, "Skipped %d items due to collision", skipped)
|
Infof(ctx, "Skipped %d items due to collision", skipped)
|
||||||
}
|
}
|
||||||
|
|
||||||
ds.Items().MaybePrintEntries(ctx)
|
dis := ds.Items()
|
||||||
|
|
||||||
|
Outf(ctx, "Restored %d items", len(dis))
|
||||||
|
dis.MaybePrintEntries(ctx)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,9 +60,8 @@ func (h contactRestoreHandler) GetContainerByName(
|
|||||||
return h.ac.GetContainerByName(ctx, userID, "", containerName)
|
return h.ac.GetContainerByName(ctx, userID, "", containerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// always returns the provided value
|
func (h contactRestoreHandler) defaultRootContainer() string {
|
||||||
func (h contactRestoreHandler) orRootContainer(c string) string {
|
return api.DefaultContacts
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h contactRestoreHandler) restore(
|
func (h contactRestoreHandler) restore(
|
||||||
@ -100,6 +99,14 @@ func restoreContact(
|
|||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
ctr *count.Bus,
|
ctr *count.Bus,
|
||||||
) (*details.ExchangeInfo, error) {
|
) (*details.ExchangeInfo, error) {
|
||||||
|
// contacts has a weird relationship with its default
|
||||||
|
// folder, which is that the folder is treated as invisible
|
||||||
|
// in many cases. If we're restoring to a blank location,
|
||||||
|
// we can interpret that as the root.
|
||||||
|
if len(destinationID) == 0 {
|
||||||
|
destinationID = api.DefaultContacts
|
||||||
|
}
|
||||||
|
|
||||||
contact, err := api.BytesToContactable(body)
|
contact, err := api.BytesToContactable(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, graph.Wrap(ctx, err, "creating contact from bytes")
|
return nil, graph.Wrap(ctx, err, "creating contact from bytes")
|
||||||
@ -139,7 +146,7 @@ func restoreContact(
|
|||||||
// at least we'll have accidentally over-produced data instead of deleting
|
// at least we'll have accidentally over-produced data instead of deleting
|
||||||
// the user's data.
|
// the user's data.
|
||||||
if shouldDeleteOriginal {
|
if shouldDeleteOriginal {
|
||||||
if err := cr.DeleteItem(ctx, userID, collisionID); err != nil {
|
if err := cr.DeleteItem(ctx, userID, collisionID); err != nil && !graph.IsErrDeletedInFlight(err) {
|
||||||
return nil, graph.Wrap(ctx, err, "deleting colliding contact")
|
return nil, graph.Wrap(ctx, err, "deleting colliding contact")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -176,6 +176,19 @@ func (suite *ContactsRestoreIntgSuite) TestRestoreContact() {
|
|||||||
assert.True(t, m.calledDelete, "old item deleted")
|
assert.True(t, m.calledDelete, "old item deleted")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "collision: replace - err already deleted",
|
||||||
|
apiMock: &contactRestoreMock{deleteItemErr: graph.ErrDeletedInFlight},
|
||||||
|
collisionMap: map[string]string{collisionKey: "smarf"},
|
||||||
|
onCollision: control.Replace,
|
||||||
|
expectErr: func(t *testing.T, err error) {
|
||||||
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
|
},
|
||||||
|
expectMock: func(t *testing.T, m *contactRestoreMock) {
|
||||||
|
assert.True(t, m.calledPost, "new item posted")
|
||||||
|
assert.True(t, m.calledDelete, "old item deleted")
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
suite.Run(test.name, func() {
|
suite.Run(test.name, func() {
|
||||||
|
|||||||
@ -806,6 +806,9 @@ func runCreateDestinationTest(
|
|||||||
gcc = handler.newContainerCache(userID)
|
gcc = handler.newContainerCache(userID)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
err := gcc.Populate(ctx, fault.New(true), handler.defaultRootContainer())
|
||||||
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
path1, err := path.Build(
|
path1, err := path.Build(
|
||||||
tenantID,
|
tenantID,
|
||||||
userID,
|
userID,
|
||||||
@ -821,7 +824,6 @@ func runCreateDestinationTest(
|
|||||||
handler.formatRestoreDestination(destinationName, path1),
|
handler.formatRestoreDestination(destinationName, path1),
|
||||||
userID,
|
userID,
|
||||||
gcc,
|
gcc,
|
||||||
true,
|
|
||||||
fault.New(true))
|
fault.New(true))
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
@ -843,7 +845,6 @@ func runCreateDestinationTest(
|
|||||||
handler.formatRestoreDestination(destinationName, path2),
|
handler.formatRestoreDestination(destinationName, path2),
|
||||||
userID,
|
userID,
|
||||||
gcc,
|
gcc,
|
||||||
false,
|
|
||||||
fault.New(true))
|
fault.New(true))
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
|
|||||||
@ -44,6 +44,10 @@ func (h eventRestoreHandler) formatRestoreDestination(
|
|||||||
destinationContainerName string,
|
destinationContainerName string,
|
||||||
_ path.Path, // ignored because calendars cannot be nested
|
_ path.Path, // ignored because calendars cannot be nested
|
||||||
) *path.Builder {
|
) *path.Builder {
|
||||||
|
if len(destinationContainerName) == 0 {
|
||||||
|
destinationContainerName = api.DefaultCalendar
|
||||||
|
}
|
||||||
|
|
||||||
return path.Builder{}.Append(destinationContainerName)
|
return path.Builder{}.Append(destinationContainerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,8 +66,8 @@ func (h eventRestoreHandler) GetContainerByName(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// always returns the provided value
|
// always returns the provided value
|
||||||
func (h eventRestoreHandler) orRootContainer(c string) string {
|
func (h eventRestoreHandler) defaultRootContainer() string {
|
||||||
return c
|
return api.DefaultCalendar
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h eventRestoreHandler) restore(
|
func (h eventRestoreHandler) restore(
|
||||||
@ -151,7 +155,7 @@ func restoreEvent(
|
|||||||
// at least we'll have accidentally over-produced data instead of deleting
|
// at least we'll have accidentally over-produced data instead of deleting
|
||||||
// the user's data.
|
// the user's data.
|
||||||
if shouldDeleteOriginal {
|
if shouldDeleteOriginal {
|
||||||
if err := er.DeleteItem(ctx, userID, collisionID); err != nil {
|
if err := er.DeleteItem(ctx, userID, collisionID); err != nil && !graph.IsErrDeletedInFlight(err) {
|
||||||
return nil, graph.Wrap(ctx, err, "deleting colliding event")
|
return nil, graph.Wrap(ctx, err, "deleting colliding event")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -224,6 +224,19 @@ func (suite *EventsRestoreIntgSuite) TestRestoreEvent() {
|
|||||||
assert.True(t, m.calledDelete, "old item deleted")
|
assert.True(t, m.calledDelete, "old item deleted")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "collision: replace - err already deleted",
|
||||||
|
apiMock: &eventRestoreMock{deleteItemErr: graph.ErrDeletedInFlight},
|
||||||
|
collisionMap: map[string]string{collisionKey: "smarf"},
|
||||||
|
onCollision: control.Replace,
|
||||||
|
expectErr: func(t *testing.T, err error) {
|
||||||
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
|
},
|
||||||
|
expectMock: func(t *testing.T, m *eventRestoreMock) {
|
||||||
|
assert.True(t, m.calledPost, "new item posted")
|
||||||
|
assert.True(t, m.calledDelete, "old item deleted")
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
suite.Run(test.name, func() {
|
suite.Run(test.name, func() {
|
||||||
|
|||||||
@ -95,12 +95,7 @@ type containerAPI interface {
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID, parentContainerID, containerName string,
|
userID, parentContainerID, containerName string,
|
||||||
) (graph.Container, error)
|
) (graph.Container, error)
|
||||||
|
defaultRootContainer() string
|
||||||
// returns either the provided value (assumed to be the root
|
|
||||||
// folder for that cache tree), or the default root container
|
|
||||||
// (if the category uses a root folder that exists above the
|
|
||||||
// restore location path).
|
|
||||||
orRootContainer(string) string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type containerByNamer interface {
|
type containerByNamer interface {
|
||||||
|
|||||||
@ -65,8 +65,7 @@ func (h mailRestoreHandler) GetContainerByName(
|
|||||||
return h.ac.GetContainerByName(ctx, userID, parentContainerID, containerName)
|
return h.ac.GetContainerByName(ctx, userID, parentContainerID, containerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// always returns rootFolderAlias
|
func (h mailRestoreHandler) defaultRootContainer() string {
|
||||||
func (h mailRestoreHandler) orRootContainer(string) string {
|
|
||||||
return api.MsgFolderRoot
|
return api.MsgFolderRoot
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,7 +150,7 @@ func restoreMail(
|
|||||||
// at least we'll have accidentally over-produced data instead of deleting
|
// at least we'll have accidentally over-produced data instead of deleting
|
||||||
// the user's data.
|
// the user's data.
|
||||||
if shouldDeleteOriginal {
|
if shouldDeleteOriginal {
|
||||||
if err := mr.DeleteItem(ctx, userID, collisionID); err != nil {
|
if err := mr.DeleteItem(ctx, userID, collisionID); err != nil && !graph.IsErrDeletedInFlight(err) {
|
||||||
return nil, graph.Wrap(ctx, err, "deleting colliding mail message")
|
return nil, graph.Wrap(ctx, err, "deleting colliding mail message")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -193,6 +193,19 @@ func (suite *MailRestoreIntgSuite) TestRestoreMail() {
|
|||||||
assert.True(t, m.calledDelete, "old item deleted")
|
assert.True(t, m.calledDelete, "old item deleted")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "collision: replace - err already deleted",
|
||||||
|
apiMock: &mailRestoreMock{deleteItemErr: graph.ErrDeletedInFlight},
|
||||||
|
collisionMap: map[string]string{collisionKey: "smarf"},
|
||||||
|
onCollision: control.Replace,
|
||||||
|
expectErr: func(t *testing.T, err error) {
|
||||||
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
|
},
|
||||||
|
expectMock: func(t *testing.T, m *mailRestoreMock) {
|
||||||
|
assert.True(t, m.calledPost, "new item posted")
|
||||||
|
assert.True(t, m.calledDelete, "old item deleted")
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
suite.Run(test.name, func() {
|
suite.Run(test.name, func() {
|
||||||
|
|||||||
@ -55,9 +55,8 @@ func ConsumeRestoreCollections(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
isNewCache bool
|
category = dc.FullPath().Category()
|
||||||
category = dc.FullPath().Category()
|
ictx = clues.Add(
|
||||||
ictx = clues.Add(
|
|
||||||
ctx,
|
ctx,
|
||||||
"restore_category", category,
|
"restore_category", category,
|
||||||
"restore_full_path", dc.FullPath())
|
"restore_full_path", dc.FullPath())
|
||||||
@ -70,8 +69,12 @@ func ConsumeRestoreCollections(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if directoryCache[category] == nil {
|
if directoryCache[category] == nil {
|
||||||
directoryCache[category] = handler.newContainerCache(userID)
|
gcr := handler.newContainerCache(userID)
|
||||||
isNewCache = true
|
if err := gcr.Populate(ctx, errs, handler.defaultRootContainer()); err != nil {
|
||||||
|
return nil, clues.Wrap(err, "populating container cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
directoryCache[category] = gcr
|
||||||
}
|
}
|
||||||
|
|
||||||
containerID, gcc, err := createDestination(
|
containerID, gcc, err := createDestination(
|
||||||
@ -80,7 +83,6 @@ func ConsumeRestoreCollections(
|
|||||||
handler.formatRestoreDestination(restoreCfg.Location, dc.FullPath()),
|
handler.formatRestoreDestination(restoreCfg.Location, dc.FullPath()),
|
||||||
userID,
|
userID,
|
||||||
directoryCache[category],
|
directoryCache[category],
|
||||||
isNewCache,
|
|
||||||
errs)
|
errs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
el.AddRecoverable(ctx, err)
|
el.AddRecoverable(ctx, err)
|
||||||
@ -240,7 +242,6 @@ func createDestination(
|
|||||||
destination *path.Builder,
|
destination *path.Builder,
|
||||||
userID string,
|
userID string,
|
||||||
gcr graph.ContainerResolver,
|
gcr graph.ContainerResolver,
|
||||||
isNewCache bool,
|
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) (string, graph.ContainerResolver, error) {
|
) (string, graph.ContainerResolver, error) {
|
||||||
var (
|
var (
|
||||||
@ -254,12 +255,11 @@ func createDestination(
|
|||||||
|
|
||||||
ictx := clues.Add(
|
ictx := clues.Add(
|
||||||
ctx,
|
ctx,
|
||||||
"is_new_cache", isNewCache,
|
|
||||||
"container_parent_id", containerParentID,
|
"container_parent_id", containerParentID,
|
||||||
"container_name", container,
|
"container_name", container,
|
||||||
"restore_location", restoreLoc)
|
"restore_location", restoreLoc)
|
||||||
|
|
||||||
fid, err := getOrPopulateContainer(
|
containerID, err := getOrPopulateContainer(
|
||||||
ictx,
|
ictx,
|
||||||
ca,
|
ca,
|
||||||
cache,
|
cache,
|
||||||
@ -267,13 +267,12 @@ func createDestination(
|
|||||||
userID,
|
userID,
|
||||||
containerParentID,
|
containerParentID,
|
||||||
container,
|
container,
|
||||||
isNewCache,
|
|
||||||
errs)
|
errs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", cache, clues.Stack(err)
|
return "", cache, clues.Stack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
containerParentID = fid
|
containerParentID = containerID
|
||||||
}
|
}
|
||||||
|
|
||||||
// containerParentID now identifies the last created container,
|
// containerParentID now identifies the last created container,
|
||||||
@ -287,7 +286,6 @@ func getOrPopulateContainer(
|
|||||||
gcr graph.ContainerResolver,
|
gcr graph.ContainerResolver,
|
||||||
restoreLoc *path.Builder,
|
restoreLoc *path.Builder,
|
||||||
userID, containerParentID, containerName string,
|
userID, containerParentID, containerName string,
|
||||||
isNewCache bool,
|
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
cached, ok := gcr.LocationInCache(restoreLoc.String())
|
cached, ok := gcr.LocationInCache(restoreLoc.String())
|
||||||
@ -318,12 +316,6 @@ func getOrPopulateContainer(
|
|||||||
|
|
||||||
folderID := ptr.Val(c.GetId())
|
folderID := ptr.Val(c.GetId())
|
||||||
|
|
||||||
if isNewCache {
|
|
||||||
if err := gcr.Populate(ctx, errs, folderID, ca.orRootContainer(restoreLoc.HeadElem())); err != nil {
|
|
||||||
return "", clues.Wrap(err, "populating container cache")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = gcr.AddToCache(ctx, c); err != nil {
|
if err = gcr.AddToCache(ctx, c); err != nil {
|
||||||
return "", clues.Wrap(err, "adding container to cache")
|
return "", clues.Wrap(err, "adding container to cache")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -143,12 +143,11 @@ func (mw *LoggingMiddleware) Intercept(
|
|||||||
|
|
||||||
// special cases where we always dump the response body, since the response
|
// special cases where we always dump the response body, since the response
|
||||||
// details might be critical to understanding the response when debugging.
|
// details might be critical to understanding the response when debugging.
|
||||||
// * 400-bad-request
|
|
||||||
// * 403-forbidden
|
|
||||||
logBody = logger.DebugAPIFV ||
|
logBody = logger.DebugAPIFV ||
|
||||||
os.Getenv(logGraphRequestsEnvKey) != "" ||
|
os.Getenv(logGraphRequestsEnvKey) != "" ||
|
||||||
resp.StatusCode == http.StatusBadRequest ||
|
resp.StatusCode == http.StatusBadRequest ||
|
||||||
resp.StatusCode == http.StatusForbidden
|
resp.StatusCode == http.StatusForbidden ||
|
||||||
|
resp.StatusCode == http.StatusConflict
|
||||||
)
|
)
|
||||||
|
|
||||||
// special case: always info-level status 429 logs
|
// special case: always info-level status 429 logs
|
||||||
|
|||||||
@ -824,10 +824,10 @@ func restoreFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
item = newItem(name, false)
|
item = newItem(name, false)
|
||||||
collisionKey = api.DriveItemCollisionKey(item)
|
collisionKey = api.DriveItemCollisionKey(item)
|
||||||
collision api.DriveCollisionItem
|
collision api.DriveCollisionItem
|
||||||
replace bool
|
shouldDeleteOriginal bool
|
||||||
)
|
)
|
||||||
|
|
||||||
if dci, ok := collisionKeyToItemID[collisionKey]; ok {
|
if dci, ok := collisionKeyToItemID[collisionKey]; ok {
|
||||||
@ -842,7 +842,7 @@ func restoreFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
collision = dci
|
collision = dci
|
||||||
replace = restoreCfg.OnCollision == control.Replace && !dci.IsFolder
|
shouldDeleteOriginal = restoreCfg.OnCollision == control.Replace && !dci.IsFolder
|
||||||
}
|
}
|
||||||
|
|
||||||
// drive items do not support PUT requests on the drive item data, so
|
// drive items do not support PUT requests on the drive item data, so
|
||||||
@ -852,8 +852,8 @@ func restoreFile(
|
|||||||
// conflict replace handling bug gets fixed, we either delete-post, and
|
// conflict replace handling bug gets fixed, we either delete-post, and
|
||||||
// risk failures in the middle, or we post w/ copy, then delete, then patch
|
// risk failures in the middle, or we post w/ copy, then delete, then patch
|
||||||
// the name, which could triple our graph calls in the worst case.
|
// the name, which could triple our graph calls in the worst case.
|
||||||
if replace {
|
if shouldDeleteOriginal {
|
||||||
if err := ir.DeleteItem(ctx, driveID, collision.ItemID); err != nil {
|
if err := ir.DeleteItem(ctx, driveID, collision.ItemID); err != nil && !graph.IsErrDeletedInFlight(err) {
|
||||||
return "", details.ItemInfo{}, clues.New("deleting colliding item")
|
return "", details.ItemInfo{}, clues.New("deleting colliding item")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -333,6 +333,7 @@ func (suite *RestoreUnitSuite) TestRestoreItem_collisionHandling() {
|
|||||||
name string
|
name string
|
||||||
collisionKeys map[string]api.DriveCollisionItem
|
collisionKeys map[string]api.DriveCollisionItem
|
||||||
onCollision control.CollisionPolicy
|
onCollision control.CollisionPolicy
|
||||||
|
deleteErr error
|
||||||
expectSkipped assert.BoolAssertionFunc
|
expectSkipped assert.BoolAssertionFunc
|
||||||
expectMock func(*testing.T, *mock.RestoreHandler)
|
expectMock func(*testing.T, *mock.RestoreHandler)
|
||||||
}{
|
}{
|
||||||
@ -391,6 +392,19 @@ func (suite *RestoreUnitSuite) TestRestoreItem_collisionHandling() {
|
|||||||
assert.Equal(t, mndiID, rh.CalledDeleteItemOn, "deleted the correct item")
|
assert.Equal(t, mndiID, rh.CalledDeleteItemOn, "deleted the correct item")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "collision, replace - err already deleted",
|
||||||
|
collisionKeys: map[string]api.DriveCollisionItem{
|
||||||
|
mock.DriveItemFileName: {ItemID: "smarf"},
|
||||||
|
},
|
||||||
|
onCollision: control.Replace,
|
||||||
|
deleteErr: graph.ErrDeletedInFlight,
|
||||||
|
expectSkipped: assert.False,
|
||||||
|
expectMock: func(t *testing.T, rh *mock.RestoreHandler) {
|
||||||
|
assert.True(t, rh.CalledPostItem, "new item posted")
|
||||||
|
assert.True(t, rh.CalledDeleteItem, "new item deleted")
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "collision, skip",
|
name: "collision, skip",
|
||||||
collisionKeys: map[string]api.DriveCollisionItem{
|
collisionKeys: map[string]api.DriveCollisionItem{
|
||||||
@ -460,8 +474,11 @@ func (suite *RestoreUnitSuite) TestRestoreItem_collisionHandling() {
|
|||||||
mndi.SetId(ptr.To(mndiID))
|
mndi.SetId(ptr.To(mndiID))
|
||||||
|
|
||||||
var (
|
var (
|
||||||
caches = NewRestoreCaches()
|
caches = NewRestoreCaches()
|
||||||
rh = &mock.RestoreHandler{PostItemResp: mndi}
|
rh = &mock.RestoreHandler{
|
||||||
|
PostItemResp: models.NewDriveItem(),
|
||||||
|
DeleteItemErr: test.deleteErr,
|
||||||
|
}
|
||||||
restoreCfg = control.RestoreConfig{OnCollision: test.onCollision}
|
restoreCfg = control.RestoreConfig{OnCollision: test.onCollision}
|
||||||
dpb = odConsts.DriveFolderPrefixBuilder("driveID1")
|
dpb = odConsts.DriveFolderPrefixBuilder("driveID1")
|
||||||
)
|
)
|
||||||
|
|||||||
@ -271,7 +271,7 @@ func (op *RestoreOperation) do(
|
|||||||
op.Errors,
|
op.Errors,
|
||||||
op.Counter)
|
op.Counter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, clues.Wrap(err, "restoring collections")
|
return nil, clues.Stack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
opStats.ctrl = op.rc.Wait()
|
opStats.ctrl = op.rc.Wait()
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import (
|
|||||||
|
|
||||||
// Max number of items for which we will print details. If there are
|
// Max number of items for which we will print details. If there are
|
||||||
// more than this, then we just show a summary.
|
// more than this, then we just show a summary.
|
||||||
const maxPrintLimit = 15
|
const maxPrintLimit = 50
|
||||||
|
|
||||||
// LocationIDer provides access to location information but guarantees that it
|
// LocationIDer provides access to location information but guarantees that it
|
||||||
// can also generate a unique location (among items in the same service but
|
// can also generate a unique location (among items in the same service but
|
||||||
@ -504,13 +504,9 @@ func (ents entrySet) PrintEntries(ctx context.Context) {
|
|||||||
// MaybePrintEntries is same as PrintEntries, but only prints if we
|
// MaybePrintEntries is same as PrintEntries, but only prints if we
|
||||||
// have less than 15 items or is not json output.
|
// have less than 15 items or is not json output.
|
||||||
func (ents entrySet) MaybePrintEntries(ctx context.Context) {
|
func (ents entrySet) MaybePrintEntries(ctx context.Context) {
|
||||||
if len(ents) > maxPrintLimit &&
|
if len(ents) <= maxPrintLimit ||
|
||||||
!print.DisplayJSONFormat() &&
|
print.DisplayJSONFormat() ||
|
||||||
!print.DisplayVerbose() {
|
print.DisplayVerbose() {
|
||||||
// TODO: Should we detect if the user is piping the output and
|
|
||||||
// print if that is the case?
|
|
||||||
print.Outf(ctx, "Restored %d items.", len(ents))
|
|
||||||
} else {
|
|
||||||
printEntries(ctx, ents)
|
printEntries(ctx, ents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,16 +17,18 @@ const (
|
|||||||
// get easily misspelled.
|
// get easily misspelled.
|
||||||
// eg: we don't need a const for "id"
|
// eg: we don't need a const for "id"
|
||||||
const (
|
const (
|
||||||
attendees = "attendees"
|
|
||||||
bccRecipients = "bccRecipients"
|
bccRecipients = "bccRecipients"
|
||||||
ccRecipients = "ccRecipients"
|
ccRecipients = "ccRecipients"
|
||||||
createdDateTime = "createdDateTime"
|
createdDateTime = "createdDateTime"
|
||||||
displayName = "displayName"
|
displayName = "displayName"
|
||||||
emailAddresses = "emailAddresses"
|
emailAddresses = "emailAddresses"
|
||||||
givenName = "givenName"
|
givenName = "givenName"
|
||||||
|
isCancelled = "isCancelled"
|
||||||
|
isDraft = "isDraft"
|
||||||
mobilePhone = "mobilePhone"
|
mobilePhone = "mobilePhone"
|
||||||
parentFolderID = "parentFolderId"
|
parentFolderID = "parentFolderId"
|
||||||
receivedDateTime = "receivedDateTime"
|
receivedDateTime = "receivedDateTime"
|
||||||
|
recurrence = "recurrence"
|
||||||
sentDateTime = "sentDateTime"
|
sentDateTime = "sentDateTime"
|
||||||
surname = "surname"
|
surname = "surname"
|
||||||
toRecipients = "toRecipients"
|
toRecipients = "toRecipients"
|
||||||
|
|||||||
@ -112,6 +112,12 @@ func (c Contacts) NewContactsPager(
|
|||||||
options.QueryParameters.Select = selectProps
|
options.QueryParameters.Select = selectProps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if we have no container ID, we need to fetch the
|
||||||
|
// base contacts container ID.
|
||||||
|
if len(containerID) == 0 {
|
||||||
|
containerID = DefaultContacts
|
||||||
|
}
|
||||||
|
|
||||||
builder := c.Stable.
|
builder := c.Stable.
|
||||||
Client().
|
Client().
|
||||||
Users().
|
Users().
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -701,7 +702,17 @@ func EventFromMap(ev map[string]any) (models.Eventable, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func eventCollisionKeyProps() []string {
|
func eventCollisionKeyProps() []string {
|
||||||
return idAnd("subject", "type", "start", "end", "attendees", "recurrence")
|
// Do not use attendees here. We slice out attendees from the
|
||||||
|
// restored event so that they do not receive an email for every
|
||||||
|
// restoration item. Attendees will guarantee non-overlapping keys.
|
||||||
|
return idAnd(
|
||||||
|
"subject",
|
||||||
|
"type",
|
||||||
|
"start",
|
||||||
|
"end",
|
||||||
|
recurrence,
|
||||||
|
isCancelled,
|
||||||
|
isDraft)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EventCollisionKey constructs a key from the eventable's creation time, subject, and organizer.
|
// EventCollisionKey constructs a key from the eventable's creation time, subject, and organizer.
|
||||||
@ -713,37 +724,34 @@ func EventCollisionKey(item models.Eventable) string {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
subject = ptr.Val(item.GetSubject())
|
subject = ptr.Val(item.GetSubject())
|
||||||
attendees = item.GetAttendees()
|
oftype = ptr.Val(item.GetType()).String()
|
||||||
a string
|
startTime = item.GetStart()
|
||||||
oftype = ptr.Val(item.GetType())
|
start string
|
||||||
t = oftype.String()
|
endTime = item.GetEnd()
|
||||||
start = item.GetStart()
|
end string
|
||||||
s string
|
|
||||||
end = item.GetEnd()
|
|
||||||
e string
|
|
||||||
recurs = item.GetRecurrence()
|
recurs = item.GetRecurrence()
|
||||||
r string
|
recur string
|
||||||
|
cancelled = ptr.Val(item.GetIsCancelled())
|
||||||
|
draft = ptr.Val(item.GetIsDraft())
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, att := range attendees {
|
if startTime != nil {
|
||||||
if att.GetEmailAddress() != nil {
|
start = ptr.Val(startTime.GetDateTime())
|
||||||
a += ptr.Val(att.GetEmailAddress().GetAddress())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if start != nil {
|
if endTime != nil {
|
||||||
s = ptr.Val(start.GetDateTime())
|
end = ptr.Val(endTime.GetDateTime())
|
||||||
}
|
|
||||||
|
|
||||||
if end != nil {
|
|
||||||
e = ptr.Val(end.GetDateTime())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if recurs != nil && recurs.GetPattern() != nil {
|
if recurs != nil && recurs.GetPattern() != nil {
|
||||||
r = ptr.Val(recurs.GetPattern().GetOdataType())
|
recur = ptr.Val(recurs.GetPattern().GetOdataType())
|
||||||
}
|
}
|
||||||
|
|
||||||
// this result gets hashed to ensure that an enormous list of attendees
|
// this result gets hashed to ensure that an enormous list of attendees
|
||||||
// doesn't generate a multi-kb collision key.
|
// doesn't generate a multi-kb collision key.
|
||||||
return clues.ConcealWith(clues.SHA256, subject+a+t+s+e+r)
|
return subject +
|
||||||
|
oftype +
|
||||||
|
start + end + recur +
|
||||||
|
strconv.FormatBool(draft) +
|
||||||
|
strconv.FormatBool(cancelled)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user