github.com/jstaf/onedriver@v0.14.2-0.20240420231225-f07678f9e6ef/fs/delta.go (about) 1 package fs 2 3 import ( 4 "encoding/json" 5 "errors" 6 "strings" 7 "time" 8 9 "github.com/jstaf/onedriver/fs/graph" 10 "github.com/rs/zerolog/log" 11 bolt "go.etcd.io/bbolt" 12 ) 13 14 // DeltaLoop creates a new thread to poll the server for changes and should be 15 // called as a goroutine 16 func (f *Filesystem) DeltaLoop(interval time.Duration) { 17 log.Trace().Msg("Starting delta goroutine.") 18 for { // eva 19 // get deltas 20 log.Trace().Msg("Fetching deltas from server.") 21 pollSuccess := false 22 deltas := make(map[string]*graph.DriveItem) 23 for { 24 incoming, cont, err := f.pollDeltas(f.auth) 25 if err != nil { 26 // the only thing that should be able to bring the FS out 27 // of a read-only state is a successful delta call 28 log.Error().Err(err). 29 Msg("Error during delta fetch, marking fs as offline.") 30 f.Lock() 31 f.offline = true 32 f.Unlock() 33 break 34 } 35 36 for _, delta := range incoming { 37 // As per the API docs, the last delta received from the server 38 // for an item is the one we should use. 39 deltas[delta.ID] = delta 40 } 41 if !cont { 42 log.Info().Msgf("Fetched %d deltas.", len(deltas)) 43 pollSuccess = true 44 break 45 } 46 } 47 48 // now apply deltas 49 secondPass := make([]string, 0) 50 for _, delta := range deltas { 51 err := f.applyDelta(delta) 52 // retry deletion of non-empty directories after all other deltas applied 53 if err != nil && err.Error() == "directory is non-empty" { 54 secondPass = append(secondPass, delta.ID) 55 } 56 } 57 for _, id := range secondPass { 58 // failures should explicitly be ignored the second time around as per docs 59 f.applyDelta(deltas[id]) 60 } 61 62 if !f.IsOffline() { 63 f.SerializeAll() 64 } 65 66 if pollSuccess { 67 f.Lock() 68 if f.offline { 69 log.Info().Msg("Delta fetch success, marking fs as online.") 70 } 71 f.offline = false 72 f.Unlock() 73 74 f.db.Batch(func(tx *bolt.Tx) error { 75 return tx.Bucket(bucketDelta).Put([]byte("deltaLink"), []byte(f.deltaLink)) 76 }) 77 78 // wait until next interval 79 time.Sleep(interval) 80 } else { 81 // shortened duration while offline 82 time.Sleep(2 * time.Second) 83 } 84 } 85 } 86 87 type deltaResponse struct { 88 NextLink string `json:"@odata.nextLink,omitempty"` 89 DeltaLink string `json:"@odata.deltaLink,omitempty"` 90 Values []*graph.DriveItem `json:"value,omitempty"` 91 } 92 93 // Polls the delta endpoint and return deltas + whether or not to continue 94 // polling. Does not perform deduplication. Note that changes from the local 95 // client will actually appear as deltas from the server (there is no 96 // distinction between local and remote changes from the server's perspective, 97 // everything is a delta, regardless of where it came from). 98 func (f *Filesystem) pollDeltas(auth *graph.Auth) ([]*graph.DriveItem, bool, error) { 99 resp, err := graph.Get(f.deltaLink, auth) 100 if err != nil { 101 return make([]*graph.DriveItem, 0), false, err 102 } 103 104 page := deltaResponse{} 105 json.Unmarshal(resp, &page) 106 107 // If the server does not provide a `@odata.nextLink` item, it means we've 108 // reached the end of this polling cycle and should not continue until the 109 // next poll interval. 110 if page.NextLink != "" { 111 f.deltaLink = strings.TrimPrefix(page.NextLink, graph.GraphURL) 112 return page.Values, true, nil 113 } 114 f.deltaLink = strings.TrimPrefix(page.DeltaLink, graph.GraphURL) 115 return page.Values, false, nil 116 } 117 118 // applyDelta diagnoses and applies a server-side change to our local state. 119 // Things we care about (present in the local cache): 120 // * Deleted items 121 // * Changed content remotely, but not locally 122 // * New items in a folder we have locally 123 func (f *Filesystem) applyDelta(delta *graph.DriveItem) error { 124 id := delta.ID 125 name := delta.Name 126 parentID := delta.Parent.ID 127 ctx := log.With(). 128 Str("id", id). 129 Str("parentID", parentID). 130 Str("name", name). 131 Logger() 132 ctx.Debug().Msg("Applying delta") 133 134 // diagnose and act on what type of delta we're dealing with 135 136 // do we have it at all? 137 if parent := f.GetID(parentID); parent == nil { 138 // Nothing needs to be applied, item not in cache, so latest copy will 139 // be pulled down next time it's accessed. 140 ctx.Trace(). 141 Str("delta", "skip"). 142 Msg("Skipping delta, item's parent not in cache.") 143 return nil 144 } 145 146 local := f.GetID(id) 147 148 // was it deleted? 149 if delta.Deleted != nil { 150 if delta.IsDir() && local != nil && local.HasChildren() { 151 // from docs: you should only delete a folder locally if it is empty 152 // after syncing all the changes. 153 ctx.Warn().Str("delta", "delete"). 154 Msg("Refusing delta deletion of non-empty folder as per API docs.") 155 return errors.New("directory is non-empty") 156 } 157 ctx.Info().Str("delta", "delete"). 158 Msg("Applying server-side deletion of item.") 159 f.DeleteID(id) 160 return nil 161 } 162 163 // does the item exist locally? if not, add the delta to the cache under the 164 // appropriate parent 165 if local == nil { 166 // check if we don't have it here first 167 local, _ = f.GetChild(parentID, name, nil) 168 if local != nil { 169 localID := local.ID() 170 ctx.Info(). 171 Str("localID", localID). 172 Msg("Local item already exists under different ID.") 173 if isLocalID(localID) { 174 if err := f.MoveID(localID, id); err != nil { 175 ctx.Error(). 176 Str("localID", localID). 177 Err(err). 178 Msg("Could not move item to new, nonlocal ID!") 179 } 180 } 181 } else { 182 ctx.Info().Str("delta", "create"). 183 Msg("Creating inode from delta.") 184 f.InsertChild(parentID, NewInodeDriveItem(delta)) 185 return nil 186 } 187 } 188 189 // was the item moved? 190 localName := local.Name() 191 if local.ParentID() != parentID || local.Name() != name { 192 log.Info(). 193 Str("parent", local.ParentID()). 194 Str("name", localName). 195 Str("newParent", parentID). 196 Str("newName", name). 197 Str("id", id). 198 Str("delta", "rename"). 199 Msg("Applying server-side rename") 200 oldParentID := local.ParentID() 201 // local rename only 202 f.MovePath(oldParentID, parentID, localName, name, f.auth) 203 // do not return, there may be additional changes 204 } 205 206 // Finally, check if the content/metadata of the remote has changed. 207 // "Interesting" changes must be synced back to our local state without 208 // data loss or corruption. Currently the only thing the local filesystem 209 // actually modifies remotely is the actual file data, so we simply accept 210 // the remote metadata changes that do not deal with the file's content 211 // changing. 212 if delta.ModTimeUnix() > local.ModTime() && !delta.ETagIsMatch(local.ETag) { 213 sameContent := false 214 if !delta.IsDir() && delta.File != nil { 215 local.RLock() 216 sameContent = local.VerifyChecksum(delta.File.Hashes.QuickXorHash) 217 local.RUnlock() 218 } 219 220 if !sameContent { 221 //TODO check if local has changes and rename the server copy if so 222 ctx.Info().Str("delta", "overwrite"). 223 Msg("Overwriting local item, no local changes to preserve.") 224 // update modtime, hashes, purge any local content in memory 225 local.Lock() 226 defer local.Unlock() 227 local.DriveItem.ModTime = delta.ModTime 228 local.DriveItem.Size = delta.Size 229 local.DriveItem.ETag = delta.ETag 230 // the rest of these are harmless when this is a directory 231 // as they will be null anyways 232 local.DriveItem.File = delta.File 233 local.hasChanges = false 234 return nil 235 } 236 } 237 238 ctx.Trace().Str("delta", "skip").Msg("Skipping, no changes relative to local state.") 239 return nil 240 }