corso/src/internal/kopia/merge_details.go
ashmrtn 975f15b42b
Use modTime in details merging (#3962)
Add modTime as one of the things that the
DetailsMergeInfoer knows how to check against,
wire it into details merging, and populate it
during item upload

This will help merge assist backup base items
by allowing us to do a direct comparison on
modTimes if we add them to the
DetailsMergeInfoer during upload

---

#### 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

- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* #<issue>

#### Test Plan

- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
2023-08-09 02:04:06 +00:00

159 lines
3.8 KiB
Go

package kopia
import (
"time"
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/path"
)
type DetailsMergeInfoer interface {
// ItemsToMerge returns the number of items that need to be merged.
ItemsToMerge() int
// GetNewPathRefs takes the old RepoRef and old LocationRef of an item and
// returns the new RepoRef and the new location of the item the item. If the
// item shouldn't be merged nils are returned.
GetNewPathRefs(
oldRef *path.Builder,
modTime time.Time,
oldLoc details.LocationIDer,
) (path.Path, *path.Builder, error)
}
type prevRef struct {
repoRef path.Path
locRef *path.Builder
modTime *time.Time
}
type mergeDetails struct {
repoRefs map[string]prevRef
locations *locationPrefixMatcher
}
func (m *mergeDetails) ItemsToMerge() int {
if m == nil {
return 0
}
return len(m.repoRefs)
}
// addRepoRef adds an entry in mergeDetails that can be looked up later. If
// modTime is non-nil then it's checked during lookup. If it is nil then the
// mod time provided during lookup is ignored.
func (m *mergeDetails) addRepoRef(
oldRef *path.Builder,
modTime *time.Time,
newRef path.Path,
newLocRef *path.Builder,
) error {
if newRef == nil {
return clues.New("nil RepoRef")
}
if _, ok := m.repoRefs[oldRef.ShortRef()]; ok {
return clues.New("duplicate RepoRef").With("repo_ref", oldRef.String())
}
pr := prevRef{
repoRef: newRef,
locRef: newLocRef,
modTime: modTime,
}
m.repoRefs[oldRef.ShortRef()] = pr
return nil
}
func (m *mergeDetails) GetNewPathRefs(
oldRef *path.Builder,
modTime time.Time,
oldLoc details.LocationIDer,
) (path.Path, *path.Builder, error) {
pr, ok := m.repoRefs[oldRef.ShortRef()]
if !ok {
return nil, nil, nil
}
// ModTimes don't match which means we're attempting to merge a different
// version of the item (i.e. an older version from an assist base). We
// shouldn't return a match because it could cause us to source out-of-date
// details for the item.
if pr.modTime != nil && !pr.modTime.Equal(modTime) {
return nil, nil, nil
}
// This was a location specified directly by a collection.
if pr.locRef != nil {
return pr.repoRef, pr.locRef, nil
} else if oldLoc == nil || oldLoc.ID() == nil || len(oldLoc.ID().Elements()) == 0 {
return nil, nil, clues.New("empty location key")
}
// This is a location that we need to do prefix matching on because we didn't
// see the new location of it in a collection. For example, it's a subfolder
// whose parent folder was moved.
prefixes := m.locations.longestPrefix(oldLoc)
newLoc := oldLoc.InDetails()
// Noop if prefix or newPrefix are nil. Them being nil means that the
// LocationRef hasn't changed.
newLoc.UpdateParent(prefixes.oldLoc, prefixes.newLoc)
return pr.repoRef, newLoc, nil
}
func (m *mergeDetails) addLocation(
oldRef details.LocationIDer,
newLoc *path.Builder,
) error {
return m.locations.add(oldRef, newLoc)
}
func newMergeDetails() *mergeDetails {
return &mergeDetails{
repoRefs: map[string]prevRef{},
locations: newLocationPrefixMatcher(),
}
}
type locRefs struct {
oldLoc *path.Builder
newLoc *path.Builder
}
type locationPrefixMatcher struct {
m prefixmatcher.Builder[locRefs]
}
func (m *locationPrefixMatcher) add(
oldRef details.LocationIDer,
newLoc *path.Builder,
) error {
key := oldRef.ID().String()
if _, ok := m.m.Get(key); ok {
return clues.New("RepoRef already in matcher").With("repo_ref", oldRef)
}
m.m.Add(key, locRefs{oldLoc: oldRef.InDetails(), newLoc: newLoc})
return nil
}
func (m *locationPrefixMatcher) longestPrefix(
oldRef details.LocationIDer,
) locRefs {
_, v, _ := m.m.LongestPrefix(oldRef.ID().String())
return v
}
func newLocationPrefixMatcher() *locationPrefixMatcher {
return &locationPrefixMatcher{m: prefixmatcher.NewMatcher[locRefs]()}
}