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  }