github.com/jstaf/onedriver@v0.14.2-0.20240420231225-f07678f9e6ef/fs/cache.go (about) 1 package fs 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strings" 9 "sync" 10 "time" 11 12 "github.com/hanwen/go-fuse/v2/fuse" 13 "github.com/jstaf/onedriver/fs/graph" 14 "github.com/rs/zerolog/log" 15 bolt "go.etcd.io/bbolt" 16 ) 17 18 // Filesystem is the actual FUSE filesystem and uses the go analogy of the 19 // "low-level" FUSE API here: 20 // https://github.com/libfuse/libfuse/blob/master/include/fuse_lowlevel.h 21 type Filesystem struct { 22 fuse.RawFileSystem 23 24 metadata sync.Map 25 db *bolt.DB 26 content *LoopbackCache 27 auth *graph.Auth 28 root string // the id of the filesystem's root item 29 deltaLink string 30 uploads *UploadManager 31 32 sync.RWMutex 33 offline bool 34 lastNodeID uint64 35 inodes []string 36 37 // tracks currently open directories 38 opendirsM sync.RWMutex 39 opendirs map[uint64][]*Inode 40 } 41 42 // boltdb buckets 43 var ( 44 bucketContent = []byte("content") 45 bucketMetadata = []byte("metadata") 46 bucketDelta = []byte("delta") 47 bucketVersion = []byte("version") 48 ) 49 50 // so we can tell what format the db has 51 const fsVersion = "1" 52 53 // NewFilesystem creates a new filesystem 54 func NewFilesystem(auth *graph.Auth, cacheDir string) *Filesystem { 55 // prepare cache directory 56 if _, err := os.Stat(cacheDir); err != nil { 57 if err = os.Mkdir(cacheDir, 0700); err != nil { 58 log.Fatal().Err(err).Msg("Could not create cache directory.") 59 } 60 } 61 db, err := bolt.Open( 62 filepath.Join(cacheDir, "onedriver.db"), 63 0600, 64 &bolt.Options{Timeout: time.Second * 5}, 65 ) 66 if err != nil { 67 log.Fatal().Err(err).Msg("Could not open DB. Is it already in use by another mount?") 68 } 69 70 content := NewLoopbackCache(filepath.Join(cacheDir, "content")) 71 db.Update(func(tx *bolt.Tx) error { 72 tx.CreateBucketIfNotExists(bucketMetadata) 73 tx.CreateBucketIfNotExists(bucketDelta) 74 versionBucket, _ := tx.CreateBucketIfNotExists(bucketVersion) 75 76 // migrate old content bucket to the local filesystem 77 b := tx.Bucket(bucketContent) 78 if b != nil { 79 oldVersion := "0" 80 log.Info(). 81 Str("oldVersion", oldVersion). 82 Str("version", fsVersion). 83 Msg("Migrating to new db format.") 84 err := b.ForEach(func(k []byte, v []byte) error { 85 log.Info().Bytes("key", k).Msg("Migrating file content.") 86 if err := content.Insert(string(k), v); err != nil { 87 return err 88 } 89 return b.Delete(k) 90 }) 91 if err != nil { 92 log.Error().Err(err).Msg("Migration failed.") 93 } 94 tx.DeleteBucket(bucketContent) 95 log.Info(). 96 Str("oldVersion", oldVersion). 97 Str("version", fsVersion). 98 Msg("Migrations complete.") 99 } 100 return versionBucket.Put([]byte("version"), []byte(fsVersion)) 101 }) 102 103 // ok, ready to start fs 104 fs := &Filesystem{ 105 RawFileSystem: fuse.NewDefaultRawFileSystem(), 106 content: content, 107 db: db, 108 auth: auth, 109 opendirs: make(map[uint64][]*Inode), 110 } 111 112 rootItem, err := graph.GetItem("root", auth) 113 root := NewInodeDriveItem(rootItem) 114 if err != nil { 115 if graph.IsOffline(err) { 116 // no network, load from db if possible and go to read-only state 117 fs.Lock() 118 fs.offline = true 119 fs.Unlock() 120 if root = fs.GetID("root"); root == nil { 121 log.Fatal().Msg( 122 "We are offline and could not fetch the filesystem root item from disk.", 123 ) 124 } 125 // when offline, we load the cache deltaLink from disk 126 fs.db.View(func(tx *bolt.Tx) error { 127 if link := tx.Bucket(bucketDelta).Get([]byte("deltaLink")); link != nil { 128 fs.deltaLink = string(link) 129 } else { 130 // Only reached if a previous online session never survived 131 // long enough to save its delta link. We explicitly disallow these 132 // types of startups as it's possible for things to get out of sync 133 // this way. 134 log.Fatal().Msg("Cannot perform an offline startup without a valid " + 135 "delta link from a previous session.") 136 } 137 return nil 138 }) 139 } else { 140 log.Fatal().Err(err).Msg("Could not fetch root item of filesystem!") 141 } 142 } 143 // root inode is inode 1 144 fs.root = root.ID() 145 fs.InsertID(fs.root, root) 146 147 fs.uploads = NewUploadManager(2*time.Second, db, fs, auth) 148 149 if !fs.IsOffline() { 150 // .Trash-UID is used by "gio trash" for user trash, create it if it 151 // does not exist 152 trash := fmt.Sprintf(".Trash-%d", os.Getuid()) 153 if child, _ := fs.GetChild(fs.root, trash, auth); child == nil { 154 item, err := graph.Mkdir(trash, fs.root, auth) 155 if err != nil { 156 log.Error().Err(err). 157 Msg("Could not create trash folder. " + 158 "Trashing items through the file browser may result in errors.") 159 } else { 160 fs.InsertID(item.ID, NewInodeDriveItem(item)) 161 } 162 } 163 164 // using token=latest because we don't care about existing items - they'll 165 // be downloaded on-demand by the cache 166 fs.deltaLink = "/me/drive/root/delta?token=latest" 167 } 168 169 // deltaloop is started manually 170 return fs 171 } 172 173 // IsOffline returns whether or not the cache thinks its offline. 174 func (f *Filesystem) IsOffline() bool { 175 f.RLock() 176 defer f.RUnlock() 177 return f.offline 178 } 179 180 // TranslateID returns the DriveItemID for a given NodeID 181 func (f *Filesystem) TranslateID(nodeID uint64) string { 182 f.RLock() 183 defer f.RUnlock() 184 if nodeID > f.lastNodeID || nodeID == 0 { 185 return "" 186 } 187 return f.inodes[nodeID-1] 188 } 189 190 // GetNodeID fetches the inode for a particular inode ID. 191 func (f *Filesystem) GetNodeID(nodeID uint64) *Inode { 192 id := f.TranslateID(nodeID) 193 if id == "" { 194 return nil 195 } 196 return f.GetID(id) 197 } 198 199 // InsertNodeID assigns a numeric inode ID used by the kernel if one is not 200 // already assigned. 201 func (f *Filesystem) InsertNodeID(inode *Inode) uint64 { 202 nodeID := inode.NodeID() 203 if nodeID == 0 { 204 // lock ordering is to satisfy deadlock detector 205 inode.Lock() 206 f.Lock() 207 208 f.lastNodeID++ 209 f.inodes = append(f.inodes, inode.DriveItem.ID) 210 nodeID = f.lastNodeID 211 inode.nodeID = nodeID 212 213 f.Unlock() 214 inode.Unlock() 215 } 216 return nodeID 217 } 218 219 // GetID gets an inode from the cache by ID. No API fetching is performed. 220 // Result is nil if no inode is found. 221 func (f *Filesystem) GetID(id string) *Inode { 222 entry, exists := f.metadata.Load(id) 223 if !exists { 224 // we allow fetching from disk as a fallback while offline (and it's also 225 // necessary while transitioning from offline->online) 226 var found *Inode 227 f.db.View(func(tx *bolt.Tx) error { 228 data := tx.Bucket(bucketMetadata).Get([]byte(id)) 229 var err error 230 if data != nil { 231 found, err = NewInodeJSON(data) 232 } 233 return err 234 }) 235 if found != nil { 236 f.InsertNodeID(found) 237 f.metadata.Store(id, found) // move to memory for next time 238 } 239 return found 240 } 241 return entry.(*Inode) 242 } 243 244 // InsertID inserts a single item into the filesystem by ID and sets its parent 245 // using the Inode.Parent.ID, if set. Must be called after DeleteID, if being 246 // used to rename/move an item. This is the main way new Inodes are added to the 247 // filesystem. Returns the Inode's numeric NodeID. 248 func (f *Filesystem) InsertID(id string, inode *Inode) uint64 { 249 f.metadata.Store(id, inode) 250 nodeID := f.InsertNodeID(inode) 251 252 if id != inode.ID() { 253 // we update the inode IDs here in case they do not match/changed 254 inode.Lock() 255 inode.DriveItem.ID = id 256 inode.Unlock() 257 258 f.Lock() 259 if nodeID <= f.lastNodeID { 260 f.inodes[nodeID-1] = id 261 } else { 262 log.Error(). 263 Uint64("nodeID", nodeID). 264 Uint64("lastNodeID", f.lastNodeID). 265 Msg("NodeID exceeded maximum node ID! Ignoring ID change.") 266 } 267 f.Unlock() 268 } 269 270 parentID := inode.ParentID() 271 if parentID == "" { 272 // root item, or parent not set 273 return nodeID 274 } 275 parent := f.GetID(parentID) 276 if parent == nil { 277 log.Error(). 278 Str("parentID", parentID). 279 Str("childID", id). 280 Str("childName", inode.Name()). 281 Msg("Parent item could not be found when setting parent.") 282 return nodeID 283 } 284 285 // check if the item has already been added to the parent 286 // Lock order is super key here, must go parent->child or the deadlock 287 // detector screams at us. 288 parent.Lock() 289 defer parent.Unlock() 290 for _, child := range parent.children { 291 if child == id { 292 // exit early, child cannot be added twice 293 return nodeID 294 } 295 } 296 297 // add to parent 298 if inode.IsDir() { 299 parent.subdir++ 300 } 301 parent.children = append(parent.children, id) 302 303 return nodeID 304 } 305 306 // InsertChild adds an item as a child of a specified parent ID. 307 func (f *Filesystem) InsertChild(parentID string, child *Inode) uint64 { 308 child.Lock() 309 // should already be set, just double-checking here. 310 child.DriveItem.Parent.ID = parentID 311 id := child.DriveItem.ID 312 child.Unlock() 313 return f.InsertID(id, child) 314 } 315 316 // DeleteID deletes an item from the cache, and removes it from its parent. Must 317 // be called before InsertID if being used to rename/move an item. 318 func (f *Filesystem) DeleteID(id string) { 319 if inode := f.GetID(id); inode != nil { 320 parent := f.GetID(inode.ParentID()) 321 parent.Lock() 322 for i, childID := range parent.children { 323 if childID == id { 324 parent.children = append(parent.children[:i], parent.children[i+1:]...) 325 if inode.IsDir() { 326 parent.subdir-- 327 } 328 break 329 } 330 } 331 parent.Unlock() 332 } 333 f.metadata.Delete(id) 334 f.uploads.CancelUpload(id) 335 } 336 337 // GetChild fetches a named child of an item. Wraps GetChildrenID. 338 func (f *Filesystem) GetChild(id string, name string, auth *graph.Auth) (*Inode, error) { 339 children, err := f.GetChildrenID(id, auth) 340 if err != nil { 341 return nil, err 342 } 343 for _, child := range children { 344 if strings.EqualFold(child.Name(), name) { 345 return child, nil 346 } 347 } 348 return nil, errors.New("child does not exist") 349 } 350 351 // GetChildrenID grabs all DriveItems that are the children of the given ID. If 352 // items are not found, they are fetched. 353 func (f *Filesystem) GetChildrenID(id string, auth *graph.Auth) (map[string]*Inode, error) { 354 // fetch item and catch common errors 355 inode := f.GetID(id) 356 children := make(map[string]*Inode) 357 if inode == nil { 358 log.Error().Str("id", id).Msg("Inode not found in cache") 359 return children, errors.New(id + " not found in cache") 360 } else if !inode.IsDir() { 361 // Normal files are treated as empty folders. This only gets called if 362 // we messed up and tried to get the children of a plain-old file. 363 log.Warn(). 364 Str("id", id). 365 Str("path", inode.Path()). 366 Msg("Attepted to get children of ordinary file") 367 return children, nil 368 } 369 370 // If item.children is not nil, it means we have the item's children 371 // already and can fetch them directly from the cache 372 inode.RLock() 373 if inode.children != nil { 374 // can potentially have out-of-date child metadata if started offline, but since 375 // changes are disallowed while offline, the children will be back in sync after 376 // the first successful delta fetch (which also brings the fs back online) 377 for _, childID := range inode.children { 378 child := f.GetID(childID) 379 if child == nil { 380 // will be nil if deleted or never existed 381 continue 382 } 383 children[strings.ToLower(child.Name())] = child 384 } 385 inode.RUnlock() 386 return children, nil 387 } 388 inode.RUnlock() 389 390 // We haven't fetched the children for this item yet, get them from the server. 391 fetched, err := graph.GetItemChildren(id, auth) 392 if err != nil { 393 if graph.IsOffline(err) { 394 log.Warn().Str("id", id). 395 Msg("We are offline, and no children found in cache. " + 396 "Pretending there are no children.") 397 return children, nil 398 } 399 // something else happened besides being offline 400 return nil, err 401 } 402 403 inode.Lock() 404 inode.children = make([]string, 0) 405 for _, item := range fetched { 406 // we will always have an id after fetching from the server 407 child := NewInodeDriveItem(item) 408 f.InsertNodeID(child) 409 f.metadata.Store(child.DriveItem.ID, child) 410 411 // store in result map 412 children[strings.ToLower(child.Name())] = child 413 414 // store id in parent item and increment parents subdirectory count 415 inode.children = append(inode.children, child.DriveItem.ID) 416 if child.IsDir() { 417 inode.subdir++ 418 } 419 } 420 inode.Unlock() 421 422 return children, nil 423 } 424 425 // GetChildrenPath grabs all DriveItems that are the children of the resource at 426 // the path. If items are not found, they are fetched. 427 func (f *Filesystem) GetChildrenPath(path string, auth *graph.Auth) (map[string]*Inode, error) { 428 inode, err := f.GetPath(path, auth) 429 if err != nil { 430 return make(map[string]*Inode), err 431 } 432 return f.GetChildrenID(inode.ID(), auth) 433 } 434 435 // GetPath fetches a given DriveItem in the cache, if any items along the way are 436 // not found, they are fetched. 437 func (f *Filesystem) GetPath(path string, auth *graph.Auth) (*Inode, error) { 438 lastID := f.root 439 if path == "/" { 440 return f.GetID(lastID), nil 441 } 442 443 // from the root directory, traverse the chain of items till we reach our 444 // target ID. 445 path = strings.TrimSuffix(strings.ToLower(path), "/") 446 split := strings.Split(path, "/")[1:] //omit leading "/" 447 var inode *Inode 448 for i := 0; i < len(split); i++ { 449 // fetches children 450 children, err := f.GetChildrenID(lastID, auth) 451 if err != nil { 452 return nil, err 453 } 454 455 var exists bool // if we use ":=", item is shadowed 456 inode, exists = children[split[i]] 457 if !exists { 458 // the item still doesn't exist after fetching from server. it 459 // doesn't exist 460 return nil, errors.New(strings.Join(split[:i+1], "/") + 461 " does not exist on server or in local cache") 462 } 463 lastID = inode.ID() 464 } 465 return inode, nil 466 } 467 468 // DeletePath an item from the cache by path. Must be called before Insert if 469 // being used to move/rename an item. 470 func (f *Filesystem) DeletePath(key string) { 471 inode, _ := f.GetPath(strings.ToLower(key), nil) 472 if inode != nil { 473 f.DeleteID(inode.ID()) 474 } 475 } 476 477 // InsertPath lets us manually insert an item to the cache (like if it was 478 // created locally). Overwrites a cached item if present. Must be called after 479 // delete if being used to move/rename an item. 480 func (f *Filesystem) InsertPath(key string, auth *graph.Auth, inode *Inode) (uint64, error) { 481 key = strings.ToLower(key) 482 483 // set the item.Parent.ID properly if the item hasn't been in the cache 484 // before or is being moved. 485 parent, err := f.GetPath(filepath.Dir(key), auth) 486 if err != nil { 487 return 0, err 488 } else if parent == nil { 489 const errMsg string = "parent of key was nil" 490 log.Error(). 491 Str("key", key). 492 Str("path", inode.Path()). 493 Msg(errMsg) 494 return 0, errors.New(errMsg) 495 } 496 497 // Coded this way to make sure locks are in the same order for the deadlock 498 // detector (lock ordering needs to be the same as InsertID: Parent->Child). 499 parentID := parent.ID() 500 inode.Lock() 501 inode.DriveItem.Parent.ID = parentID 502 id := inode.DriveItem.ID 503 inode.Unlock() 504 505 return f.InsertID(id, inode), nil 506 } 507 508 // MoveID moves an item to a new ID name. Also responsible for handling the 509 // actual overwrite of the item's IDInternal field 510 func (f *Filesystem) MoveID(oldID string, newID string) error { 511 inode := f.GetID(oldID) 512 if inode == nil { 513 // It may have already been renamed. This is not an error. We assume 514 // that IDs will never collide. Re-perform the op if this is the case. 515 if inode = f.GetID(newID); inode == nil { 516 // nope, it just doesn't exist 517 return errors.New("Could not get item: " + oldID) 518 } 519 } 520 521 // need to rename the child under the parent 522 parent := f.GetID(inode.ParentID()) 523 parent.Lock() 524 for i, child := range parent.children { 525 if child == oldID { 526 parent.children[i] = newID 527 break 528 } 529 } 530 parent.Unlock() 531 532 // now actually perform the metadata+content move 533 f.DeleteID(oldID) 534 f.InsertID(newID, inode) 535 if inode.IsDir() { 536 return nil 537 } 538 f.content.Move(oldID, newID) 539 return nil 540 } 541 542 // MovePath moves an item to a new position. 543 func (f *Filesystem) MovePath(oldParent, newParent, oldName, newName string, auth *graph.Auth) error { 544 inode, err := f.GetChild(oldParent, oldName, auth) 545 if err != nil { 546 return err 547 } 548 549 id := inode.ID() 550 f.DeleteID(id) 551 552 // this is the actual move op 553 inode.SetName(newName) 554 parent := f.GetID(newParent) 555 inode.Parent.ID = parent.DriveItem.ID 556 f.InsertID(id, inode) 557 return nil 558 } 559 560 // SerializeAll dumps all inode metadata currently in the cache to disk. This 561 // metadata is only used later if an item could not be found in memory AND the 562 // cache is offline. Old metadata is not removed, only overwritten (to avoid an 563 // offline session from wiping all metadata on a subsequent serialization). 564 func (f *Filesystem) SerializeAll() { 565 log.Debug().Msg("Serializing cache metadata to disk.") 566 567 allItems := make(map[string][]byte) 568 f.metadata.Range(func(k interface{}, v interface{}) bool { 569 // cannot occur within bolt transaction because acquiring the inode lock 570 // with AsJSON locks out other boltdb transactions 571 id := fmt.Sprint(k) 572 allItems[id] = v.(*Inode).AsJSON() 573 return true 574 }) 575 576 /* 577 One transaction to serialize them all, 578 One transaction to find them, 579 One transaction to bring them all 580 and in the darkness write them. 581 */ 582 f.db.Batch(func(tx *bolt.Tx) error { 583 b := tx.Bucket(bucketMetadata) 584 for k, v := range allItems { 585 b.Put([]byte(k), v) 586 if k == f.root { 587 // root item must be updated manually (since there's actually 588 // two copies) 589 b.Put([]byte("root"), v) 590 } 591 } 592 return nil 593 }) 594 }