introduce flat deletions to delta tree (#4719)
there are certain enumeration cases where tombstoning isn't the correct behavior, and we want to delete a folder from the tree entirely. This primarily occurrs when we have a create->delete pair of markers either within or across enumerations. --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [ ] 🌻 Feature #### Issue(s) * #4689 #### Test Plan - [x] ⚡ Unit test
This commit is contained in:
parent
53ce27a23b
commit
d9c42f790c
@ -172,7 +172,7 @@ func malwareItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func driveRootItem(id string) models.DriveItemable {
|
func driveRootItem(id string) models.DriveItemable {
|
||||||
name := "root"
|
name := rootName
|
||||||
item := models.NewDriveItem()
|
item := models.NewDriveItem()
|
||||||
item.SetName(&name)
|
item.SetName(&name)
|
||||||
item.SetId(&id)
|
item.SetId(&id)
|
||||||
@ -279,8 +279,8 @@ const (
|
|||||||
malware = "malware"
|
malware = "malware"
|
||||||
nav = "nav"
|
nav = "nav"
|
||||||
pkg = "package"
|
pkg = "package"
|
||||||
rootName = "root"
|
rootID = odConsts.RootID
|
||||||
rootID = "root_id"
|
rootName = odConsts.RootPathDir
|
||||||
subfolder = "subfolder"
|
subfolder = "subfolder"
|
||||||
tenant = "t"
|
tenant = "t"
|
||||||
user = "u"
|
user = "u"
|
||||||
@ -1692,22 +1692,29 @@ func (suite *CollectionsUnitSuite) TestGet_treeCannotBeUsedWhileIncomplete() {
|
|||||||
ctx, flush := tester.NewContext(t)
|
ctx, flush := tester.NewContext(t)
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
drive := models.NewDrive()
|
drv := models.NewDrive()
|
||||||
drive.SetId(ptr.To("id"))
|
drv.SetId(ptr.To("id"))
|
||||||
drive.SetName(ptr.To("name"))
|
drv.SetName(ptr.To("name"))
|
||||||
|
|
||||||
mbh := mock.DefaultOneDriveBH(user)
|
mbh := mock.DefaultOneDriveBH(user)
|
||||||
opts := control.DefaultOptions()
|
opts := control.DefaultOptions()
|
||||||
opts.ToggleFeatures.UseDeltaTree = true
|
opts.ToggleFeatures.UseDeltaTree = true
|
||||||
|
|
||||||
mockDrivePager := &apiMock.Pager[models.Driveable]{
|
mbh.DrivePagerV = pagerForDrives(drv)
|
||||||
ToReturn: []apiMock.PagerResult[models.Driveable]{
|
mbh.DriveItemEnumeration = mock.EnumerateItemsDeltaByDrive{
|
||||||
{Values: []models.Driveable{drive}},
|
DrivePagers: map[string]*mock.DriveItemsDeltaPager{
|
||||||
|
"id": {
|
||||||
|
Pages: []mock.NextPage{{
|
||||||
|
Items: []models.DriveItemable{
|
||||||
|
driveRootItem(rootID), // will be present, not needed
|
||||||
|
delItem(id(file), parent(1), rootID, isFile),
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
DeltaUpdate: pagers.DeltaUpdate{URL: id(delta)},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
mbh.DrivePagerV = mockDrivePager
|
|
||||||
|
|
||||||
c := collWithMBH(mbh)
|
c := collWithMBH(mbh)
|
||||||
c.ctrl = opts
|
c.ctrl = opts
|
||||||
|
|
||||||
|
|||||||
@ -9,15 +9,21 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
|
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
|
||||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
|
odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts"
|
||||||
bupMD "github.com/alcionai/corso/src/pkg/backup/metadata"
|
bupMD "github.com/alcionai/corso/src/pkg/backup/metadata"
|
||||||
"github.com/alcionai/corso/src/pkg/count"
|
"github.com/alcionai/corso/src/pkg/count"
|
||||||
"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/services/m365/api"
|
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/pagers"
|
"github.com/alcionai/corso/src/pkg/services/m365/api/pagers"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Processing
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// this file is used to separate the collections handling between the previous
|
// this file is used to separate the collections handling between the previous
|
||||||
// (list-based) design, and the in-progress (tree-based) redesign.
|
// (list-based) design, and the in-progress (tree-based) redesign.
|
||||||
// see: https://github.com/alcionai/corso/issues/4688
|
// see: https://github.com/alcionai/corso/issues/4688
|
||||||
@ -29,6 +35,13 @@ func (c *Collections) getTree(
|
|||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) ([]data.BackupCollection, bool, error) {
|
) ([]data.BackupCollection, bool, error) {
|
||||||
ctx = clues.AddTraceName(ctx, "GetTree")
|
ctx = clues.AddTraceName(ctx, "GetTree")
|
||||||
|
limiter := newPagerLimiter(c.ctrl)
|
||||||
|
|
||||||
|
logger.Ctx(ctx).Infow(
|
||||||
|
"running backup: getting collection data using tree structure",
|
||||||
|
"limits", c.ctrl.PreviewLimits,
|
||||||
|
"effective_limits", limiter.effectiveLimits(),
|
||||||
|
"preview_mode", limiter.enabled())
|
||||||
|
|
||||||
// extract the previous backup's metadata like: deltaToken urls and previousPath maps.
|
// extract the previous backup's metadata like: deltaToken urls and previousPath maps.
|
||||||
// We'll need these to reconstruct / ensure the correct state of the world, after
|
// We'll need these to reconstruct / ensure the correct state of the world, after
|
||||||
@ -97,8 +110,10 @@ func (c *Collections) getTree(
|
|||||||
ictx,
|
ictx,
|
||||||
drv,
|
drv,
|
||||||
prevPathsByDriveID[driveID],
|
prevPathsByDriveID[driveID],
|
||||||
|
deltasByDriveID[driveID],
|
||||||
|
limiter,
|
||||||
cl,
|
cl,
|
||||||
el.Local())
|
el)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
el.AddRecoverable(ictx, clues.Stack(err))
|
el.AddRecoverable(ictx, clues.Stack(err))
|
||||||
continue
|
continue
|
||||||
@ -140,40 +155,52 @@ func (c *Collections) getTree(
|
|||||||
return collections, canUsePrevBackup, nil
|
return collections, canUsePrevBackup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errTreeNotImplemented = clues.New("backup tree not implemented")
|
||||||
|
|
||||||
func (c *Collections) makeDriveCollections(
|
func (c *Collections) makeDriveCollections(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
d models.Driveable,
|
drv models.Driveable,
|
||||||
prevPaths map[string]string,
|
prevPaths map[string]string,
|
||||||
|
prevDeltaLink string,
|
||||||
|
limiter *pagerLimiter,
|
||||||
counter *count.Bus,
|
counter *count.Bus,
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) ([]data.BackupCollection, map[string]string, pagers.DeltaUpdate, error) {
|
) ([]data.BackupCollection, map[string]string, pagers.DeltaUpdate, error) {
|
||||||
cl := c.counter.Local()
|
ppfx, err := c.handler.PathPrefix(c.tenantID, ptr.Val(drv.GetId()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, pagers.DeltaUpdate{}, clues.Wrap(err, "generating backup tree prefix")
|
||||||
|
}
|
||||||
|
|
||||||
cl.Add(count.PrevPaths, int64(len(prevPaths)))
|
var (
|
||||||
logger.Ctx(ctx).Infow(
|
tree = newFolderyMcFolderFace(ppfx)
|
||||||
"previous metadata for drive",
|
stats = &driveEnumerationStats{}
|
||||||
"count_old_prev_paths", len(prevPaths))
|
)
|
||||||
|
|
||||||
// TODO(keepers): leaving this code around for now as a guide
|
counter.Add(count.PrevPaths, int64(len(prevPaths)))
|
||||||
// while implementation progresses.
|
|
||||||
|
|
||||||
// --- pager aggregation
|
// --- delta item aggregation
|
||||||
|
|
||||||
// du, newPrevPaths, err := c.PopulateDriveCollections(
|
du, err := c.populateTree(
|
||||||
// ctx,
|
ctx,
|
||||||
// d,
|
tree,
|
||||||
// tree,
|
limiter,
|
||||||
// cl.Local(),
|
stats,
|
||||||
// errs)
|
drv,
|
||||||
// if err != nil {
|
prevDeltaLink,
|
||||||
// return nil, false, clues.Stack(err)
|
counter,
|
||||||
// }
|
errs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, pagers.DeltaUpdate{}, clues.Stack(err)
|
||||||
|
}
|
||||||
|
|
||||||
// numDriveItems := c.NumItems - numPrevItems
|
// numDriveItems := c.NumItems - numPrevItems
|
||||||
// numPrevItems = c.NumItems
|
// numPrevItems = c.NumItems
|
||||||
|
|
||||||
// cl.Add(count.NewPrevPaths, int64(len(newPrevPaths)))
|
// cl.Add(count.NewPrevPaths, int64(len(newPrevPaths)))
|
||||||
|
|
||||||
|
// TODO(keepers): leaving this code around for now as a guide
|
||||||
|
// while implementation progresses.
|
||||||
|
|
||||||
// --- prev path incorporation
|
// --- prev path incorporation
|
||||||
|
|
||||||
// For both cases we don't need to do set difference on folder map if the
|
// For both cases we don't need to do set difference on folder map if the
|
||||||
@ -227,7 +254,285 @@ func (c *Collections) makeDriveCollections(
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return nil, nil, pagers.DeltaUpdate{}, clues.New("not yet implemented")
|
// this is a dumb hack to satisfy the linter.
|
||||||
|
if ctx == nil {
|
||||||
|
return nil, nil, du, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, du, errTreeNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// populateTree constructs a new tree and populates it with items
|
||||||
|
// retrieved by enumerating the delta query for the drive.
|
||||||
|
func (c *Collections) populateTree(
|
||||||
|
ctx context.Context,
|
||||||
|
tree *folderyMcFolderFace,
|
||||||
|
limiter *pagerLimiter,
|
||||||
|
stats *driveEnumerationStats,
|
||||||
|
drv models.Driveable,
|
||||||
|
prevDeltaLink string,
|
||||||
|
counter *count.Bus,
|
||||||
|
errs *fault.Bus,
|
||||||
|
) (pagers.DeltaUpdate, error) {
|
||||||
|
ctx = clues.Add(ctx, "invalid_prev_delta", len(prevDeltaLink) == 0)
|
||||||
|
|
||||||
|
var (
|
||||||
|
driveID = ptr.Val(drv.GetId())
|
||||||
|
el = errs.Local()
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO(keepers): to end in a correct state, we'll eventually need to run this
|
||||||
|
// query multiple times over, until it ends in an empty change set.
|
||||||
|
pager := c.handler.EnumerateDriveItemsDelta(
|
||||||
|
ctx,
|
||||||
|
driveID,
|
||||||
|
prevDeltaLink,
|
||||||
|
api.CallConfig{
|
||||||
|
Select: api.DefaultDriveItemProps(),
|
||||||
|
})
|
||||||
|
|
||||||
|
for page, reset, done := pager.NextPage(); !done; page, reset, done = pager.NextPage() {
|
||||||
|
if el.Failure() != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
counter.Inc(count.PagesEnumerated)
|
||||||
|
|
||||||
|
if reset {
|
||||||
|
counter.Inc(count.PagerResets)
|
||||||
|
tree.Reset()
|
||||||
|
c.resetStats()
|
||||||
|
|
||||||
|
*stats = driveEnumerationStats{}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.enumeratePageOfItems(
|
||||||
|
ctx,
|
||||||
|
tree,
|
||||||
|
limiter,
|
||||||
|
stats,
|
||||||
|
drv,
|
||||||
|
page,
|
||||||
|
counter,
|
||||||
|
errs)
|
||||||
|
if err != nil {
|
||||||
|
el.AddRecoverable(ctx, clues.Stack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop enumeration early if we've reached the item or page limit. Do this
|
||||||
|
// at the end of the loop so we don't request another page in the
|
||||||
|
// background.
|
||||||
|
//
|
||||||
|
// We don't want to break on just the container limit here because it's
|
||||||
|
// possible that there's more items in the current (final) container that
|
||||||
|
// we're processing. We need to see the next page to determine if we've
|
||||||
|
// reached the end of the container. Note that this doesn't take into
|
||||||
|
// account the number of items in the current container, so it's possible it
|
||||||
|
// will fetch more data when it doesn't really need to.
|
||||||
|
if limiter.atPageLimit(stats) || limiter.atItemLimit(stats) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always cancel the pager so that even if we exit early from the loop above
|
||||||
|
// we don't deadlock. Cancelling a pager that's already completed is
|
||||||
|
// essentially a noop.
|
||||||
|
pager.Cancel()
|
||||||
|
|
||||||
|
du, err := pager.Results()
|
||||||
|
if err != nil {
|
||||||
|
return du, clues.Stack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Ctx(ctx).Infow("enumerated collection delta", "stats", counter.Values())
|
||||||
|
|
||||||
|
return du, el.Failure()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Collections) enumeratePageOfItems(
|
||||||
|
ctx context.Context,
|
||||||
|
tree *folderyMcFolderFace,
|
||||||
|
limiter *pagerLimiter,
|
||||||
|
stats *driveEnumerationStats,
|
||||||
|
drv models.Driveable,
|
||||||
|
page []models.DriveItemable,
|
||||||
|
counter *count.Bus,
|
||||||
|
errs *fault.Bus,
|
||||||
|
) error {
|
||||||
|
ctx = clues.Add(ctx, "page_lenth", len(page))
|
||||||
|
el := errs.Local()
|
||||||
|
|
||||||
|
for i, item := range page {
|
||||||
|
if el.Failure() != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
isFolder = item.GetFolder() != nil || item.GetPackageEscaped() != nil
|
||||||
|
itemID = ptr.Val(item.GetId())
|
||||||
|
err error
|
||||||
|
skipped *fault.Skipped
|
||||||
|
)
|
||||||
|
|
||||||
|
ictx := clues.Add(
|
||||||
|
ctx,
|
||||||
|
"item_id", itemID,
|
||||||
|
"item_name", clues.Hide(ptr.Val(item.GetName())),
|
||||||
|
"item_index", i,
|
||||||
|
"item_is_folder", isFolder,
|
||||||
|
"item_is_package", item.GetPackageEscaped() != nil)
|
||||||
|
|
||||||
|
if isFolder {
|
||||||
|
// check if the preview needs to exit before adding each folder
|
||||||
|
if !tree.ContainsFolder(itemID) && limiter.atLimit(stats, len(tree.folderIDToNode)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
skipped, err = c.addFolderToTree(ictx, tree, drv, item, stats, counter)
|
||||||
|
} else {
|
||||||
|
skipped, err = c.addFileToTree(ictx, tree, drv, item, stats, counter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if skipped != nil {
|
||||||
|
el.AddSkip(ctx, skipped)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
el.AddRecoverable(ictx, clues.Wrap(err, "adding folder"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we reached the item or size limit while processing this page.
|
||||||
|
// The check after this loop will get us out of the pager.
|
||||||
|
// We don't want to check all limits because it's possible we've reached
|
||||||
|
// the container limit but haven't reached the item limit or really added
|
||||||
|
// items to the last container we found.
|
||||||
|
if limiter.atItemLimit(stats) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.numPages++
|
||||||
|
|
||||||
|
return clues.Stack(el.Failure()).OrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Collections) addFolderToTree(
|
||||||
|
ctx context.Context,
|
||||||
|
tree *folderyMcFolderFace,
|
||||||
|
drv models.Driveable,
|
||||||
|
folder models.DriveItemable,
|
||||||
|
stats *driveEnumerationStats,
|
||||||
|
counter *count.Bus,
|
||||||
|
) (*fault.Skipped, error) {
|
||||||
|
var (
|
||||||
|
driveID = ptr.Val(drv.GetId())
|
||||||
|
folderID = ptr.Val(folder.GetId())
|
||||||
|
folderName = ptr.Val(folder.GetName())
|
||||||
|
isDeleted = folder.GetDeleted() != nil
|
||||||
|
isMalware = folder.GetMalware() != nil
|
||||||
|
isPkg = folder.GetPackageEscaped() != nil
|
||||||
|
parent = folder.GetParentReference()
|
||||||
|
parentID string
|
||||||
|
notSelected bool
|
||||||
|
)
|
||||||
|
|
||||||
|
if parent != nil {
|
||||||
|
parentID = ptr.Val(parent.GetId())
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
switch {
|
||||||
|
case notSelected:
|
||||||
|
counter.Inc(count.TotalContainersSkipped)
|
||||||
|
case isMalware:
|
||||||
|
counter.Inc(count.TotalMalwareProcessed)
|
||||||
|
case isDeleted:
|
||||||
|
counter.Inc(count.TotalDeleteFoldersProcessed)
|
||||||
|
case isPkg:
|
||||||
|
counter.Inc(count.TotalPackagesProcessed)
|
||||||
|
default:
|
||||||
|
counter.Inc(count.TotalFoldersProcessed)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// FIXME(keepers): if we don't track this as previously visited,
|
||||||
|
// we could add a skip multiple times, every time we visit the
|
||||||
|
// folder again at the top of the page.
|
||||||
|
if isMalware {
|
||||||
|
skip := fault.ContainerSkip(
|
||||||
|
fault.SkipMalware,
|
||||||
|
driveID,
|
||||||
|
folderID,
|
||||||
|
folderName,
|
||||||
|
graph.ItemInfo(folder))
|
||||||
|
|
||||||
|
logger.Ctx(ctx).Infow("malware detected")
|
||||||
|
|
||||||
|
return skip, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDeleted {
|
||||||
|
err := tree.SetTombstone(ctx, folderID)
|
||||||
|
return nil, clues.Stack(err).OrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
collectionPath, err := c.makeFolderCollectionPath(ctx, driveID, folder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.Stack(err).Label(fault.LabelForceNoBackupCreation, count.BadCollPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip items that don't match the folder selectors we were given.
|
||||||
|
notSelected = shouldSkip(ctx, collectionPath, c.handler, ptr.Val(drv.GetName()))
|
||||||
|
if notSelected {
|
||||||
|
logger.Ctx(ctx).Debugw("path not selected", "skipped_path", collectionPath.String())
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tree.SetFolder(ctx, parentID, folderID, folderName, isPkg)
|
||||||
|
|
||||||
|
return nil, clues.Stack(err).OrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Collections) makeFolderCollectionPath(
|
||||||
|
ctx context.Context,
|
||||||
|
driveID string,
|
||||||
|
folder models.DriveItemable,
|
||||||
|
) (path.Path, error) {
|
||||||
|
if folder.GetRoot() != nil {
|
||||||
|
pb := odConsts.DriveFolderPrefixBuilder(driveID)
|
||||||
|
collectionPath, err := c.handler.CanonicalPath(pb, c.tenantID)
|
||||||
|
|
||||||
|
return collectionPath, clues.WrapWC(ctx, err, "making canonical root path").OrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
if folder.GetParentReference() == nil || folder.GetParentReference().GetPath() == nil {
|
||||||
|
return nil, clues.NewWC(ctx, "no parent reference in folder").Label(count.MissingParent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append folder name to path since we want the path for the collection, not
|
||||||
|
// the path for the parent of the collection.
|
||||||
|
name := ptr.Val(folder.GetName())
|
||||||
|
if len(name) == 0 {
|
||||||
|
return nil, clues.NewWC(ctx, "missing folder name")
|
||||||
|
}
|
||||||
|
|
||||||
|
folderPath := path.Split(ptr.Val(folder.GetParentReference().GetPath()))
|
||||||
|
folderPath = append(folderPath, name)
|
||||||
|
pb := path.Builder{}.Append(folderPath...)
|
||||||
|
collectionPath, err := c.handler.CanonicalPath(pb, c.tenantID)
|
||||||
|
|
||||||
|
return collectionPath, clues.WrapWC(ctx, err, "making folder collection path").OrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Collections) addFileToTree(
|
||||||
|
ctx context.Context,
|
||||||
|
tree *folderyMcFolderFace,
|
||||||
|
drv models.Driveable,
|
||||||
|
item models.DriveItemable,
|
||||||
|
stats *driveEnumerationStats,
|
||||||
|
counter *count.Bus,
|
||||||
|
) (*fault.Skipped, error) {
|
||||||
|
return nil, clues.New("not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
// quality-of-life wrapper that transforms each tombstone in the map
|
// quality-of-life wrapper that transforms each tombstone in the map
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -36,6 +36,9 @@ type folderyMcFolderFace struct {
|
|||||||
// it's just a sensible place to store the data, since we're
|
// it's just a sensible place to store the data, since we're
|
||||||
// already pushing file additions through the api.
|
// already pushing file additions through the api.
|
||||||
excludeFileIDs map[string]struct{}
|
excludeFileIDs map[string]struct{}
|
||||||
|
|
||||||
|
// true if Reset() was called
|
||||||
|
hadReset bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFolderyMcFolderFace(
|
func newFolderyMcFolderFace(
|
||||||
@ -49,6 +52,18 @@ func newFolderyMcFolderFace(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset erases all data contained in the tree. This is intended for
|
||||||
|
// tracking a delta enumeration reset, not for tree re-use, and will
|
||||||
|
// cause the tree to flag itself as dirty in order to appropriately
|
||||||
|
// post-process the data.
|
||||||
|
func (face *folderyMcFolderFace) Reset() {
|
||||||
|
face.hadReset = true
|
||||||
|
face.root = nil
|
||||||
|
face.folderIDToNode = map[string]*nodeyMcNodeFace{}
|
||||||
|
face.tombstones = map[string]*nodeyMcNodeFace{}
|
||||||
|
face.excludeFileIDs = map[string]struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
type nodeyMcNodeFace struct {
|
type nodeyMcNodeFace struct {
|
||||||
// required for mid-enumeration folder moves, else we have to walk
|
// required for mid-enumeration folder moves, else we have to walk
|
||||||
// the tree completely to remove the node from its old parent.
|
// the tree completely to remove the node from its old parent.
|
||||||
@ -71,14 +86,12 @@ type nodeyMcNodeFace struct {
|
|||||||
func newNodeyMcNodeFace(
|
func newNodeyMcNodeFace(
|
||||||
parent *nodeyMcNodeFace,
|
parent *nodeyMcNodeFace,
|
||||||
id, name string,
|
id, name string,
|
||||||
prev path.Elements,
|
|
||||||
isPackage bool,
|
isPackage bool,
|
||||||
) *nodeyMcNodeFace {
|
) *nodeyMcNodeFace {
|
||||||
return &nodeyMcNodeFace{
|
return &nodeyMcNodeFace{
|
||||||
parent: parent,
|
parent: parent,
|
||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
prev: prev,
|
|
||||||
children: map[string]*nodeyMcNodeFace{},
|
children: map[string]*nodeyMcNodeFace{},
|
||||||
items: map[string]time.Time{},
|
items: map[string]time.Time{},
|
||||||
isPackage: isPackage,
|
isPackage: isPackage,
|
||||||
@ -89,6 +102,21 @@ func newNodeyMcNodeFace(
|
|||||||
// folder handling
|
// folder handling
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ContainsFolder returns true if the given folder id is present as either
|
||||||
|
// a live node or a tombstone.
|
||||||
|
func (face *folderyMcFolderFace) ContainsFolder(id string) bool {
|
||||||
|
_, stillKicking := face.folderIDToNode[id]
|
||||||
|
_, alreadyBuried := face.tombstones[id]
|
||||||
|
|
||||||
|
return stillKicking || alreadyBuried
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountNodes returns a count that is the sum of live folders and
|
||||||
|
// tombstones recorded in the tree.
|
||||||
|
func (face *folderyMcFolderFace) CountFolders() int {
|
||||||
|
return len(face.tombstones) + len(face.folderIDToNode)
|
||||||
|
}
|
||||||
|
|
||||||
// SetFolder adds a node with the following details to the tree.
|
// SetFolder adds a node with the following details to the tree.
|
||||||
// If the node already exists with the given ID, the name and parent
|
// If the node already exists with the given ID, the name and parent
|
||||||
// values are updated to match (isPackage is assumed not to change).
|
// values are updated to match (isPackage is assumed not to change).
|
||||||
@ -118,7 +146,7 @@ func (face *folderyMcFolderFace) SetFolder(
|
|||||||
// only set the root node once.
|
// only set the root node once.
|
||||||
if name == odConsts.RootPathDir {
|
if name == odConsts.RootPathDir {
|
||||||
if face.root == nil {
|
if face.root == nil {
|
||||||
root := newNodeyMcNodeFace(nil, id, name, nil, isPackage)
|
root := newNodeyMcNodeFace(nil, id, name, isPackage)
|
||||||
face.root = root
|
face.root = root
|
||||||
face.folderIDToNode[id] = root
|
face.folderIDToNode[id] = root
|
||||||
}
|
}
|
||||||
@ -178,9 +206,7 @@ func (face *folderyMcFolderFace) SetFolder(
|
|||||||
nodey.parent = parent
|
nodey.parent = parent
|
||||||
} else {
|
} else {
|
||||||
// change type 1: new addition
|
// change type 1: new addition
|
||||||
// the previous location is always nil, since previous path additions get their
|
nodey = newNodeyMcNodeFace(parent, id, name, isPackage)
|
||||||
// own setter func.
|
|
||||||
nodey = newNodeyMcNodeFace(parent, id, name, nil, isPackage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure the parent points to this node, and that the node is registered
|
// ensure the parent points to this node, and that the node is registered
|
||||||
@ -194,16 +220,11 @@ func (face *folderyMcFolderFace) SetFolder(
|
|||||||
func (face *folderyMcFolderFace) SetTombstone(
|
func (face *folderyMcFolderFace) SetTombstone(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id string,
|
id string,
|
||||||
loc path.Elements,
|
|
||||||
) error {
|
) error {
|
||||||
if len(id) == 0 {
|
if len(id) == 0 {
|
||||||
return clues.NewWC(ctx, "missing tombstone folder ID")
|
return clues.NewWC(ctx, "missing tombstone folder ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(loc) == 0 {
|
|
||||||
return clues.NewWC(ctx, "missing tombstone location")
|
|
||||||
}
|
|
||||||
|
|
||||||
// since we run mutiple enumerations, it's possible to see a folder added on the
|
// since we run mutiple enumerations, it's possible to see a folder added on the
|
||||||
// first enumeration that then gets deleted on the next. This means that the folder
|
// first enumeration that then gets deleted on the next. This means that the folder
|
||||||
// was deleted while the first enumeration was in flight, and will show up even if
|
// was deleted while the first enumeration was in flight, and will show up even if
|
||||||
@ -225,29 +246,8 @@ func (face *folderyMcFolderFace) SetTombstone(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
zombey, alreadyBuried := face.tombstones[id]
|
if _, alreadyBuried := face.tombstones[id]; !alreadyBuried {
|
||||||
if alreadyBuried {
|
face.tombstones[id] = newNodeyMcNodeFace(nil, id, "", false)
|
||||||
if zombey.prev.String() != loc.String() {
|
|
||||||
// logging for sanity
|
|
||||||
logger.Ctx(ctx).Infow(
|
|
||||||
"attempted to tombstone two paths with the same ID",
|
|
||||||
"first_tombstone_path", zombey.prev,
|
|
||||||
"second_tombstone_path", loc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// since we're storing drive data by folder name in kopia, not id, we need
|
|
||||||
// to make sure to preserve the original tombstone location. If we get a
|
|
||||||
// conflicting set of locations in the same delta enumeration, we can always
|
|
||||||
// treat the original one as the canonical one. IE: what we're deleting is
|
|
||||||
// the original location as it exists in kopia. So even if we get a newer
|
|
||||||
// location in the drive enumeration, the original location is the one that
|
|
||||||
// kopia uses, and the one we need to tombstone.
|
|
||||||
//
|
|
||||||
// this should also be asserted in the second step, where we compare the delta
|
|
||||||
// changes to the backup previous paths metadata.
|
|
||||||
face.tombstones[id] = zombey
|
|
||||||
} else {
|
|
||||||
face.tombstones[id] = newNodeyMcNodeFace(nil, id, "", loc, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -8,11 +8,49 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts"
|
|
||||||
"github.com/alcionai/corso/src/internal/tester"
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
var loc = path.NewElements("root:/foo/bar/baz/qux/fnords/smarf/voi/zumba/bangles/howdyhowdyhowdy")
|
||||||
|
|
||||||
|
func treeWithRoot() *folderyMcFolderFace {
|
||||||
|
tree := newFolderyMcFolderFace(nil)
|
||||||
|
rootey := newNodeyMcNodeFace(nil, rootID, rootName, false)
|
||||||
|
tree.root = rootey
|
||||||
|
tree.folderIDToNode[rootID] = rootey
|
||||||
|
|
||||||
|
return tree
|
||||||
|
}
|
||||||
|
|
||||||
|
func treeWithTombstone() *folderyMcFolderFace {
|
||||||
|
tree := treeWithRoot()
|
||||||
|
tree.tombstones[id(folder)] = newNodeyMcNodeFace(nil, id(folder), "", false)
|
||||||
|
|
||||||
|
return tree
|
||||||
|
}
|
||||||
|
|
||||||
|
func treeWithFolders() *folderyMcFolderFace {
|
||||||
|
tree := treeWithRoot()
|
||||||
|
|
||||||
|
o := newNodeyMcNodeFace(tree.root, idx(folder, "parent"), namex(folder, "parent"), true)
|
||||||
|
tree.folderIDToNode[o.id] = o
|
||||||
|
|
||||||
|
f := newNodeyMcNodeFace(o, id(folder), name(folder), false)
|
||||||
|
tree.folderIDToNode[f.id] = f
|
||||||
|
o.children[f.id] = f
|
||||||
|
|
||||||
|
return tree
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
type DeltaTreeUnitSuite struct {
|
type DeltaTreeUnitSuite struct {
|
||||||
tester.Suite
|
tester.Suite
|
||||||
}
|
}
|
||||||
@ -24,7 +62,7 @@ func TestDeltaTreeUnitSuite(t *testing.T) {
|
|||||||
func (suite *DeltaTreeUnitSuite) TestNewFolderyMcFolderFace() {
|
func (suite *DeltaTreeUnitSuite) TestNewFolderyMcFolderFace() {
|
||||||
var (
|
var (
|
||||||
t = suite.T()
|
t = suite.T()
|
||||||
p, err = path.BuildPrefix("t", "r", path.OneDriveService, path.FilesCategory)
|
p, err = path.BuildPrefix(tenant, user, path.OneDriveService, path.FilesCategory)
|
||||||
)
|
)
|
||||||
|
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
@ -41,14 +79,13 @@ func (suite *DeltaTreeUnitSuite) TestNewNodeyMcNodeFace() {
|
|||||||
var (
|
var (
|
||||||
t = suite.T()
|
t = suite.T()
|
||||||
parent = &nodeyMcNodeFace{}
|
parent = &nodeyMcNodeFace{}
|
||||||
loc = path.NewElements("root:/foo/bar/baz/qux/fnords/smarf/voi/zumba/bangles/howdyhowdyhowdy")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
nodeFace := newNodeyMcNodeFace(parent, "id", "name", loc, true)
|
nodeFace := newNodeyMcNodeFace(parent, "id", "name", true)
|
||||||
assert.Equal(t, parent, nodeFace.parent)
|
assert.Equal(t, parent, nodeFace.parent)
|
||||||
assert.Equal(t, "id", nodeFace.id)
|
assert.Equal(t, "id", nodeFace.id)
|
||||||
assert.Equal(t, "name", nodeFace.name)
|
assert.Equal(t, "name", nodeFace.name)
|
||||||
assert.Equal(t, loc, nodeFace.prev)
|
assert.NotEqual(t, loc, nodeFace.prev)
|
||||||
assert.True(t, nodeFace.isPackage)
|
assert.True(t, nodeFace.isPackage)
|
||||||
assert.NotNil(t, nodeFace.children)
|
assert.NotNil(t, nodeFace.children)
|
||||||
assert.NotNil(t, nodeFace.items)
|
assert.NotNil(t, nodeFace.items)
|
||||||
@ -57,35 +94,6 @@ func (suite *DeltaTreeUnitSuite) TestNewNodeyMcNodeFace() {
|
|||||||
// note that this test is focused on the SetFolder function,
|
// note that this test is focused on the SetFolder function,
|
||||||
// and intentionally does not verify the resulting node tree
|
// and intentionally does not verify the resulting node tree
|
||||||
func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_SetFolder() {
|
func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_SetFolder() {
|
||||||
treeWithRoot := func() *folderyMcFolderFace {
|
|
||||||
rootey := newNodeyMcNodeFace(nil, odConsts.RootID, odConsts.RootPathDir, nil, false)
|
|
||||||
tree := newFolderyMcFolderFace(nil)
|
|
||||||
tree.root = rootey
|
|
||||||
tree.folderIDToNode[odConsts.RootID] = rootey
|
|
||||||
|
|
||||||
return tree
|
|
||||||
}
|
|
||||||
|
|
||||||
treeWithTombstone := func() *folderyMcFolderFace {
|
|
||||||
tree := treeWithRoot()
|
|
||||||
tree.tombstones["folder"] = newNodeyMcNodeFace(nil, "folder", "", path.NewElements(""), false)
|
|
||||||
|
|
||||||
return tree
|
|
||||||
}
|
|
||||||
|
|
||||||
treeWithFolders := func() *folderyMcFolderFace {
|
|
||||||
tree := treeWithRoot()
|
|
||||||
|
|
||||||
o := newNodeyMcNodeFace(tree.root, "other", "o", nil, true)
|
|
||||||
tree.folderIDToNode[o.id] = o
|
|
||||||
|
|
||||||
f := newNodeyMcNodeFace(o, "folder", "f", nil, false)
|
|
||||||
tree.folderIDToNode[f.id] = f
|
|
||||||
o.children[f.id] = f
|
|
||||||
|
|
||||||
return tree
|
|
||||||
}
|
|
||||||
|
|
||||||
table := []struct {
|
table := []struct {
|
||||||
tname string
|
tname string
|
||||||
tree *folderyMcFolderFace
|
tree *folderyMcFolderFace
|
||||||
@ -94,102 +102,91 @@ func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_SetFolder() {
|
|||||||
name string
|
name string
|
||||||
isPackage bool
|
isPackage bool
|
||||||
expectErr assert.ErrorAssertionFunc
|
expectErr assert.ErrorAssertionFunc
|
||||||
expectPrev assert.ValueAssertionFunc
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
tname: "add root",
|
tname: "add root",
|
||||||
tree: &folderyMcFolderFace{
|
tree: &folderyMcFolderFace{
|
||||||
folderIDToNode: map[string]*nodeyMcNodeFace{},
|
folderIDToNode: map[string]*nodeyMcNodeFace{},
|
||||||
},
|
},
|
||||||
id: odConsts.RootID,
|
id: rootID,
|
||||||
name: odConsts.RootPathDir,
|
name: rootName,
|
||||||
isPackage: true,
|
isPackage: true,
|
||||||
expectErr: assert.NoError,
|
expectErr: assert.NoError,
|
||||||
expectPrev: assert.Nil,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tname: "root already exists",
|
tname: "root already exists",
|
||||||
tree: treeWithRoot(),
|
tree: treeWithRoot(),
|
||||||
id: odConsts.RootID,
|
id: rootID,
|
||||||
name: odConsts.RootPathDir,
|
name: rootName,
|
||||||
expectErr: assert.NoError,
|
expectErr: assert.NoError,
|
||||||
expectPrev: assert.Nil,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tname: "add folder",
|
tname: "add folder",
|
||||||
tree: treeWithRoot(),
|
tree: treeWithRoot(),
|
||||||
parentID: odConsts.RootID,
|
parentID: rootID,
|
||||||
id: "folder",
|
id: id(folder),
|
||||||
name: "nameyMcNameFace",
|
name: name(folder),
|
||||||
expectErr: assert.NoError,
|
expectErr: assert.NoError,
|
||||||
expectPrev: assert.Nil,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tname: "add package",
|
tname: "add package",
|
||||||
tree: treeWithRoot(),
|
tree: treeWithRoot(),
|
||||||
parentID: odConsts.RootID,
|
parentID: rootID,
|
||||||
id: "folder",
|
id: id(folder),
|
||||||
name: "nameyMcNameFace",
|
name: name(folder),
|
||||||
isPackage: true,
|
isPackage: true,
|
||||||
expectErr: assert.NoError,
|
expectErr: assert.NoError,
|
||||||
expectPrev: assert.Nil,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tname: "missing ID",
|
tname: "missing ID",
|
||||||
tree: treeWithRoot(),
|
tree: treeWithRoot(),
|
||||||
parentID: odConsts.RootID,
|
parentID: rootID,
|
||||||
name: "nameyMcNameFace",
|
name: name(folder),
|
||||||
isPackage: true,
|
isPackage: true,
|
||||||
expectErr: assert.Error,
|
expectErr: assert.Error,
|
||||||
expectPrev: assert.Nil,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tname: "missing name",
|
tname: "missing name",
|
||||||
tree: treeWithRoot(),
|
tree: treeWithRoot(),
|
||||||
parentID: odConsts.RootID,
|
parentID: rootID,
|
||||||
id: "folder",
|
id: id(folder),
|
||||||
isPackage: true,
|
isPackage: true,
|
||||||
expectErr: assert.Error,
|
expectErr: assert.Error,
|
||||||
expectPrev: assert.Nil,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tname: "missing parentID",
|
tname: "missing parentID",
|
||||||
tree: treeWithRoot(),
|
tree: treeWithRoot(),
|
||||||
id: "folder",
|
id: id(folder),
|
||||||
name: "nameyMcNameFace",
|
name: name(folder),
|
||||||
isPackage: true,
|
isPackage: true,
|
||||||
expectErr: assert.Error,
|
expectErr: assert.Error,
|
||||||
expectPrev: assert.Nil,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tname: "already tombstoned",
|
tname: "already tombstoned",
|
||||||
tree: treeWithTombstone(),
|
tree: treeWithTombstone(),
|
||||||
parentID: odConsts.RootID,
|
parentID: rootID,
|
||||||
id: "folder",
|
id: id(folder),
|
||||||
name: "nameyMcNameFace",
|
name: name(folder),
|
||||||
expectErr: assert.NoError,
|
expectErr: assert.NoError,
|
||||||
expectPrev: assert.NotNil,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tname: "add folder before parent",
|
tname: "add folder before parent",
|
||||||
tree: &folderyMcFolderFace{
|
tree: &folderyMcFolderFace{
|
||||||
folderIDToNode: map[string]*nodeyMcNodeFace{},
|
folderIDToNode: map[string]*nodeyMcNodeFace{},
|
||||||
},
|
},
|
||||||
parentID: odConsts.RootID,
|
parentID: rootID,
|
||||||
id: "folder",
|
id: id(folder),
|
||||||
name: "nameyMcNameFace",
|
name: name(folder),
|
||||||
isPackage: true,
|
isPackage: true,
|
||||||
expectErr: assert.Error,
|
expectErr: assert.Error,
|
||||||
expectPrev: assert.Nil,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tname: "folder already exists",
|
tname: "folder already exists",
|
||||||
tree: treeWithFolders(),
|
tree: treeWithFolders(),
|
||||||
parentID: "other",
|
parentID: idx(folder, "parent"),
|
||||||
id: "folder",
|
id: id(folder),
|
||||||
name: "nameyMcNameFace",
|
name: name(folder),
|
||||||
expectErr: assert.NoError,
|
expectErr: assert.NoError,
|
||||||
expectPrev: assert.Nil,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
@ -213,7 +210,6 @@ func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_SetFolder() {
|
|||||||
|
|
||||||
result := test.tree.folderIDToNode[test.id]
|
result := test.tree.folderIDToNode[test.id]
|
||||||
require.NotNil(t, result)
|
require.NotNil(t, result)
|
||||||
test.expectPrev(t, result.prev)
|
|
||||||
assert.Equal(t, test.id, result.id)
|
assert.Equal(t, test.id, result.id)
|
||||||
assert.Equal(t, test.name, result.name)
|
assert.Equal(t, test.name, result.name)
|
||||||
assert.Equal(t, test.isPackage, result.isPackage)
|
assert.Equal(t, test.isPackage, result.isPackage)
|
||||||
@ -230,65 +226,38 @@ func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_SetFolder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_AddTombstone() {
|
func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_AddTombstone() {
|
||||||
loc := path.NewElements("root:/foo/bar/baz/qux/fnords/smarf/voi/zumba/bangles/howdyhowdyhowdy")
|
|
||||||
treeWithTombstone := func() *folderyMcFolderFace {
|
|
||||||
tree := newFolderyMcFolderFace(nil)
|
|
||||||
tree.tombstones["id"] = newNodeyMcNodeFace(nil, "id", "", loc, false)
|
|
||||||
|
|
||||||
return tree
|
|
||||||
}
|
|
||||||
|
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
id string
|
id string
|
||||||
loc path.Elements
|
|
||||||
tree *folderyMcFolderFace
|
tree *folderyMcFolderFace
|
||||||
expectErr assert.ErrorAssertionFunc
|
expectErr assert.ErrorAssertionFunc
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "add tombstone",
|
name: "add tombstone",
|
||||||
id: "id",
|
id: id(folder),
|
||||||
loc: loc,
|
|
||||||
tree: newFolderyMcFolderFace(nil),
|
tree: newFolderyMcFolderFace(nil),
|
||||||
expectErr: assert.NoError,
|
expectErr: assert.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "duplicate tombstone",
|
name: "duplicate tombstone",
|
||||||
id: "id",
|
id: id(folder),
|
||||||
loc: loc,
|
|
||||||
tree: treeWithTombstone(),
|
tree: treeWithTombstone(),
|
||||||
expectErr: assert.NoError,
|
expectErr: assert.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing ID",
|
name: "missing ID",
|
||||||
loc: loc,
|
|
||||||
tree: newFolderyMcFolderFace(nil),
|
|
||||||
expectErr: assert.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing loc",
|
|
||||||
id: "id",
|
|
||||||
tree: newFolderyMcFolderFace(nil),
|
|
||||||
expectErr: assert.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty loc",
|
|
||||||
id: "id",
|
|
||||||
loc: path.Elements{},
|
|
||||||
tree: newFolderyMcFolderFace(nil),
|
tree: newFolderyMcFolderFace(nil),
|
||||||
expectErr: assert.Error,
|
expectErr: assert.Error,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "conflict: folder alive",
|
name: "conflict: folder alive",
|
||||||
id: "id",
|
id: id(folder),
|
||||||
loc: loc,
|
|
||||||
tree: treeWithTombstone(),
|
tree: treeWithTombstone(),
|
||||||
expectErr: assert.NoError,
|
expectErr: assert.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "already tombstoned with different path",
|
name: "already tombstoned",
|
||||||
id: "id",
|
id: id(folder),
|
||||||
loc: append(path.Elements{"foo"}, loc...),
|
|
||||||
tree: treeWithTombstone(),
|
tree: treeWithTombstone(),
|
||||||
expectErr: assert.NoError,
|
expectErr: assert.NoError,
|
||||||
},
|
},
|
||||||
@ -300,7 +269,7 @@ func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_AddTombstone() {
|
|||||||
ctx, flush := tester.NewContext(t)
|
ctx, flush := tester.NewContext(t)
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
err := test.tree.SetTombstone(ctx, test.id, test.loc)
|
err := test.tree.SetTombstone(ctx, test.id)
|
||||||
test.expectErr(t, err, clues.ToCore(err))
|
test.expectErr(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -309,8 +278,6 @@ func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_AddTombstone() {
|
|||||||
|
|
||||||
result := test.tree.tombstones[test.id]
|
result := test.tree.tombstones[test.id]
|
||||||
require.NotNil(t, result)
|
require.NotNil(t, result)
|
||||||
require.NotEmpty(t, result.prev)
|
|
||||||
assert.Equal(t, loc, result.prev)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -431,22 +398,17 @@ func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_SetFolder_correctTree()
|
|||||||
ctx, flush := tester.NewContext(t)
|
ctx, flush := tester.NewContext(t)
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
tree := newFolderyMcFolderFace(nil)
|
tree := treeWithRoot()
|
||||||
rootID := odConsts.RootID
|
|
||||||
|
|
||||||
set := func(
|
set := func(
|
||||||
parentID, id, name string,
|
parentID, fid, fname string,
|
||||||
isPackage bool,
|
isPackage bool,
|
||||||
) {
|
) {
|
||||||
err := tree.SetFolder(ctx, parentID, id, name, isPackage)
|
err := tree.SetFolder(ctx, parentID, fid, fname, isPackage)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Nil(t, tree.root)
|
// assert the root exists
|
||||||
assert.Empty(t, tree.folderIDToNode)
|
|
||||||
|
|
||||||
// add the root
|
|
||||||
set("", rootID, odConsts.RootPathDir, false)
|
|
||||||
|
|
||||||
assert.NotNil(t, tree.root)
|
assert.NotNil(t, tree.root)
|
||||||
assert.Equal(t, rootID, tree.root.id)
|
assert.Equal(t, rootID, tree.root.id)
|
||||||
@ -455,18 +417,18 @@ func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_SetFolder_correctTree()
|
|||||||
an(tree.root).compare(t, tree, true)
|
an(tree.root).compare(t, tree, true)
|
||||||
|
|
||||||
// add a child at the root
|
// add a child at the root
|
||||||
set(tree.root.id, "lefty", "l", false)
|
set(rootID, id("lefty"), name("l"), false)
|
||||||
|
|
||||||
lefty := tree.folderIDToNode["lefty"]
|
lefty := tree.folderIDToNode[id("lefty")]
|
||||||
an(
|
an(
|
||||||
tree.root,
|
tree.root,
|
||||||
an(lefty)).
|
an(lefty)).
|
||||||
compare(t, tree, true)
|
compare(t, tree, true)
|
||||||
|
|
||||||
// add another child at the root
|
// add another child at the root
|
||||||
set(tree.root.id, "righty", "r", false)
|
set(rootID, id("righty"), name("r"), false)
|
||||||
|
|
||||||
righty := tree.folderIDToNode["righty"]
|
righty := tree.folderIDToNode[id("righty")]
|
||||||
an(
|
an(
|
||||||
tree.root,
|
tree.root,
|
||||||
an(lefty),
|
an(lefty),
|
||||||
@ -474,9 +436,9 @@ func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_SetFolder_correctTree()
|
|||||||
compare(t, tree, true)
|
compare(t, tree, true)
|
||||||
|
|
||||||
// add a child to lefty
|
// add a child to lefty
|
||||||
set(lefty.id, "bloaty", "bl", false)
|
set(lefty.id, id("bloaty"), name("bl"), false)
|
||||||
|
|
||||||
bloaty := tree.folderIDToNode["bloaty"]
|
bloaty := tree.folderIDToNode[id("bloaty")]
|
||||||
an(
|
an(
|
||||||
tree.root,
|
tree.root,
|
||||||
an(lefty, an(bloaty)),
|
an(lefty, an(bloaty)),
|
||||||
@ -484,9 +446,9 @@ func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_SetFolder_correctTree()
|
|||||||
compare(t, tree, true)
|
compare(t, tree, true)
|
||||||
|
|
||||||
// add another child to lefty
|
// add another child to lefty
|
||||||
set(lefty.id, "brightly", "br", false)
|
set(lefty.id, id("brightly"), name("br"), false)
|
||||||
|
|
||||||
brightly := tree.folderIDToNode["brightly"]
|
brightly := tree.folderIDToNode[id("brightly")]
|
||||||
an(
|
an(
|
||||||
tree.root,
|
tree.root,
|
||||||
an(lefty, an(bloaty), an(brightly)),
|
an(lefty, an(bloaty), an(brightly)),
|
||||||
@ -522,40 +484,34 @@ func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_SetFolder_correctTombst
|
|||||||
ctx, flush := tester.NewContext(t)
|
ctx, flush := tester.NewContext(t)
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
tree := newFolderyMcFolderFace(nil)
|
tree := treeWithRoot()
|
||||||
rootID := odConsts.RootID
|
|
||||||
|
|
||||||
set := func(
|
set := func(
|
||||||
parentID, id, name string,
|
parentID, fid, fname string,
|
||||||
isPackage bool,
|
isPackage bool,
|
||||||
) {
|
) {
|
||||||
err := tree.SetFolder(ctx, parentID, id, name, isPackage)
|
err := tree.SetFolder(ctx, parentID, fid, fname, isPackage)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
tomb := func(
|
tomb := func(
|
||||||
id string,
|
tid string,
|
||||||
loc path.Elements,
|
loc path.Elements,
|
||||||
) {
|
) {
|
||||||
err := tree.SetTombstone(ctx, id, loc)
|
err := tree.SetTombstone(ctx, tid)
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Nil(t, tree.root)
|
|
||||||
assert.Empty(t, tree.folderIDToNode)
|
|
||||||
|
|
||||||
// create a simple tree
|
// create a simple tree
|
||||||
// root > branchy > [leafy, bob]
|
// root > branchy > [leafy, bob]
|
||||||
set("", rootID, odConsts.RootPathDir, false)
|
set(tree.root.id, id("branchy"), name("br"), false)
|
||||||
|
branchy := tree.folderIDToNode[id("branchy")]
|
||||||
|
|
||||||
set(tree.root.id, "branchy", "br", false)
|
set(branchy.id, id("leafy"), name("l"), false)
|
||||||
branchy := tree.folderIDToNode["branchy"]
|
set(branchy.id, id("bob"), name("bobbers"), false)
|
||||||
|
|
||||||
set(branchy.id, "leafy", "l", false)
|
leafy := tree.folderIDToNode[id("leafy")]
|
||||||
set(branchy.id, "bob", "bobbers", false)
|
bob := tree.folderIDToNode[id("bob")]
|
||||||
|
|
||||||
leafy := tree.folderIDToNode["leafy"]
|
|
||||||
bob := tree.folderIDToNode["bob"]
|
|
||||||
|
|
||||||
an(
|
an(
|
||||||
tree.root,
|
tree.root,
|
||||||
|
|||||||
@ -26,6 +26,26 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/pagers"
|
"github.com/alcionai/corso/src/pkg/services/m365/api/pagers"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func minimumLimitOpts() control.Options {
|
||||||
|
minLimitOpts := control.DefaultOptions()
|
||||||
|
minLimitOpts.PreviewLimits.Enabled = true
|
||||||
|
minLimitOpts.PreviewLimits.MaxBytes = 1
|
||||||
|
minLimitOpts.PreviewLimits.MaxContainers = 1
|
||||||
|
minLimitOpts.PreviewLimits.MaxItems = 1
|
||||||
|
minLimitOpts.PreviewLimits.MaxItemsPerContainer = 1
|
||||||
|
minLimitOpts.PreviewLimits.MaxPages = 1
|
||||||
|
|
||||||
|
return minLimitOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
type LimiterUnitSuite struct {
|
type LimiterUnitSuite struct {
|
||||||
tester.Suite
|
tester.Suite
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,10 +9,8 @@ const (
|
|||||||
ThrottledAPICalls Key = "throttled-api-calls"
|
ThrottledAPICalls Key = "throttled-api-calls"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tracked during backup
|
// backup amounts reported by kopia
|
||||||
const (
|
const (
|
||||||
// amounts reported by kopia
|
|
||||||
|
|
||||||
PersistedCachedFiles Key = "persisted-cached-files"
|
PersistedCachedFiles Key = "persisted-cached-files"
|
||||||
PersistedDirectories Key = "persisted-directories"
|
PersistedDirectories Key = "persisted-directories"
|
||||||
PersistedFiles Key = "persisted-files"
|
PersistedFiles Key = "persisted-files"
|
||||||
@ -24,9 +22,10 @@ const (
|
|||||||
PersistenceErrors Key = "persistence-errors"
|
PersistenceErrors Key = "persistence-errors"
|
||||||
PersistenceExpectedErrors Key = "persistence-expected-errors"
|
PersistenceExpectedErrors Key = "persistence-expected-errors"
|
||||||
PersistenceIgnoredErrors Key = "persistence-ignored-errors"
|
PersistenceIgnoredErrors Key = "persistence-ignored-errors"
|
||||||
|
)
|
||||||
|
|
||||||
// amounts reported by data providers
|
// backup amounts reported by data providers
|
||||||
|
const (
|
||||||
Channels Key = "channels"
|
Channels Key = "channels"
|
||||||
CollectionMoved Key = "collection-moved"
|
CollectionMoved Key = "collection-moved"
|
||||||
CollectionNew Key = "collection-state-new"
|
CollectionNew Key = "collection-state-new"
|
||||||
@ -66,10 +65,27 @@ const (
|
|||||||
StreamItemsErrored Key = "stream-items-errored"
|
StreamItemsErrored Key = "stream-items-errored"
|
||||||
StreamItemsFound Key = "stream-items-found"
|
StreamItemsFound Key = "stream-items-found"
|
||||||
StreamItemsRemoved Key = "stream-items-removed"
|
StreamItemsRemoved Key = "stream-items-removed"
|
||||||
|
TotalContainersSkipped Key = "total-containers-skipped"
|
||||||
URLCacheMiss Key = "url-cache-miss"
|
URLCacheMiss Key = "url-cache-miss"
|
||||||
URLCacheRefresh Key = "url-cache-refresh"
|
URLCacheRefresh Key = "url-cache-refresh"
|
||||||
|
)
|
||||||
|
|
||||||
// miscellaneous
|
// Total___Processed counts are used to track raw processing numbers
|
||||||
|
// for values that may have a similar, but different, end result count.
|
||||||
|
// For example: a delta query may add the same folder to many different pages.
|
||||||
|
// instead of adding logic to catch folder duplications and only count new
|
||||||
|
// entries, we can increment TotalFoldersProcessed for every duplication,
|
||||||
|
// and use a separate Key (Folders) for the end count of folders produced
|
||||||
|
// at the end of the delta enumeration.
|
||||||
|
const (
|
||||||
|
TotalDeleteFoldersProcessed Key = "total-delete-folders-processed"
|
||||||
|
TotalFoldersProcessed Key = "total-folders-processed"
|
||||||
|
TotalMalwareProcessed Key = "total-malware-processed"
|
||||||
|
TotalPackagesProcessed Key = "total-packages-processed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// miscellaneous
|
||||||
|
const (
|
||||||
RequiresUserPnToIDMigration Key = "requires-user-pn-to-id-migration"
|
RequiresUserPnToIDMigration Key = "requires-user-pn-to-id-migration"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -491,6 +491,7 @@ func CtxStack(ctx context.Context, skip int) *zap.SugaredLogger {
|
|||||||
// CtxErr retrieves the logger embedded in the context
|
// CtxErr retrieves the logger embedded in the context
|
||||||
// and packs all of the structured data in the error inside it.
|
// and packs all of the structured data in the error inside it.
|
||||||
func CtxErr(ctx context.Context, err error) *zap.SugaredLogger {
|
func CtxErr(ctx context.Context, err error) *zap.SugaredLogger {
|
||||||
|
// TODO(keepers): only log the err values, not the ctx.
|
||||||
return Ctx(ctx).
|
return Ctx(ctx).
|
||||||
With(
|
With(
|
||||||
"error", err,
|
"error", err,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user