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]()} }