github.com/jstaf/onedriver@v0.14.2-0.20240420231225-f07678f9e6ef/fs/fs.go (about)

     1  package fs
     2  
     3  import (
     4  	"io"
     5  	"math"
     6  	"os"
     7  	"path/filepath"
     8  	"regexp"
     9  	"strings"
    10  	"syscall"
    11  	"time"
    12  
    13  	"github.com/hanwen/go-fuse/v2/fuse"
    14  	"github.com/jstaf/onedriver/fs/graph"
    15  	"github.com/rs/zerolog/log"
    16  )
    17  
    18  const timeout = time.Second
    19  
    20  func (f *Filesystem) getInodeContent(i *Inode) *[]byte {
    21  	i.RLock()
    22  	defer i.RUnlock()
    23  	data := f.content.Get(i.DriveItem.ID)
    24  	return &data
    25  }
    26  
    27  // remoteID uploads a file to obtain a Onedrive ID if it doesn't already
    28  // have one. This is necessary to avoid race conditions against uploads if the
    29  // file has not already been uploaded.
    30  func (f *Filesystem) remoteID(i *Inode) (string, error) {
    31  	if i.IsDir() {
    32  		// Directories are always created with an ID. (And this method is only
    33  		// really used for files anyways...)
    34  		return i.ID(), nil
    35  	}
    36  
    37  	originalID := i.ID()
    38  	if isLocalID(originalID) && f.auth.AccessToken != "" {
    39  		// perform a blocking upload of the item
    40  		data := f.getInodeContent(i)
    41  		session, err := NewUploadSession(i, data)
    42  		if err != nil {
    43  			return originalID, err
    44  		}
    45  
    46  		i.Lock()
    47  		name := i.DriveItem.Name
    48  		err = session.Upload(f.auth)
    49  		if err != nil {
    50  			i.Unlock()
    51  
    52  			if strings.Contains(err.Error(), "nameAlreadyExists") {
    53  				// A file with this name already exists on the server, get its ID and
    54  				// use that. This is probably the same file, but just got uploaded
    55  				// earlier.
    56  				children, err := graph.GetItemChildren(i.ParentID(), f.auth)
    57  				if err != nil {
    58  					return originalID, err
    59  				}
    60  				for _, child := range children {
    61  					if child.Name == name {
    62  						log.Info().
    63  							Str("name", name).
    64  							Str("originalID", originalID).
    65  							Str("newID", child.ID).
    66  							Msg("Exchanged ID.")
    67  						return child.ID, f.MoveID(originalID, child.ID)
    68  					}
    69  				}
    70  			}
    71  			// failed to obtain an ID, return whatever it was beforehand
    72  			return originalID, err
    73  		}
    74  
    75  		// we just successfully uploaded a copy, no need to do it again
    76  		i.hasChanges = false
    77  		i.DriveItem.ETag = session.ETag
    78  		i.Unlock()
    79  
    80  		// this is all we really wanted from this transaction
    81  		err = f.MoveID(originalID, session.ID)
    82  		log.Info().
    83  			Str("name", name).
    84  			Str("originalID", originalID).
    85  			Str("newID", session.ID).
    86  			Msg("Exchanged ID.")
    87  		return session.ID, err
    88  	}
    89  	return originalID, nil
    90  }
    91  
    92  var disallowedRexp = regexp.MustCompile(`(?i)LPT[0-9]|COM[0-9]|_vti_|["*:<>?\/\\\|]`)
    93  
    94  // isNameRestricted returns true if the name is disallowed according to the doc here:
    95  // https://support.microsoft.com/en-us/office/restrictions-and-limitations-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa
    96  func isNameRestricted(name string) bool {
    97  	if strings.EqualFold(name, "CON") {
    98  		return true
    99  	}
   100  	if strings.EqualFold(name, "AUX") {
   101  		return true
   102  	}
   103  	if strings.EqualFold(name, "PRN") {
   104  		return true
   105  	}
   106  	if strings.EqualFold(name, "NUL") {
   107  		return true
   108  	}
   109  	if strings.EqualFold(name, ".lock") {
   110  		return true
   111  	}
   112  	if strings.EqualFold(name, "desktop.ini") {
   113  		return true
   114  	}
   115  	return disallowedRexp.FindStringIndex(name) != nil
   116  }
   117  
   118  // Statfs returns information about the filesystem. Mainly useful for checking
   119  // quotas and storage limits.
   120  func (f *Filesystem) StatFs(cancel <-chan struct{}, in *fuse.InHeader, out *fuse.StatfsOut) fuse.Status {
   121  	ctx := log.With().Str("op", "StatFs").Logger()
   122  	ctx.Debug().Msg("")
   123  	drive, err := graph.GetDrive(f.auth)
   124  	if err != nil {
   125  		return fuse.EREMOTEIO
   126  	}
   127  
   128  	if drive.DriveType == graph.DriveTypePersonal {
   129  		ctx.Warn().Msg("Personal OneDrive accounts do not show number of files, " +
   130  			"inode counts reported by onedriver will be bogus.")
   131  	} else if drive.Quota.Total == 0 { // <-- check for if microsoft ever fixes their API
   132  		ctx.Warn().Msg("OneDrive for Business accounts do not report quotas, " +
   133  			"pretending the quota is 5TB and it's all unused.")
   134  		drive.Quota.Total = 5 * uint64(math.Pow(1024, 4))
   135  		drive.Quota.Remaining = 5 * uint64(math.Pow(1024, 4))
   136  		drive.Quota.FileCount = 0
   137  	}
   138  
   139  	// limits are pasted from https://support.microsoft.com/en-us/help/3125202
   140  	const blkSize uint64 = 4096 // default ext4 block size
   141  	out.Bsize = uint32(blkSize)
   142  	out.Blocks = drive.Quota.Total / blkSize
   143  	out.Bfree = drive.Quota.Remaining / blkSize
   144  	out.Bavail = drive.Quota.Remaining / blkSize
   145  	out.Files = 100000
   146  	out.Ffree = 100000 - drive.Quota.FileCount
   147  	out.NameLen = 260
   148  	return fuse.OK
   149  }
   150  
   151  // Mkdir creates a directory.
   152  func (f *Filesystem) Mkdir(cancel <-chan struct{}, in *fuse.MkdirIn, name string, out *fuse.EntryOut) fuse.Status {
   153  	if isNameRestricted(name) {
   154  		return fuse.EINVAL
   155  	}
   156  
   157  	inode := f.GetNodeID(in.NodeId)
   158  	if inode == nil {
   159  		return fuse.ENOENT
   160  	}
   161  	id := inode.ID()
   162  	path := filepath.Join(inode.Path(), name)
   163  	ctx := log.With().
   164  		Str("op", "Mkdir").
   165  		Uint64("nodeID", in.NodeId).
   166  		Str("id", id).
   167  		Str("path", path).
   168  		Str("mode", Octal(in.Mode)).
   169  		Logger()
   170  	ctx.Debug().Msg("")
   171  
   172  	// create the new directory on the server
   173  	item, err := graph.Mkdir(name, id, f.auth)
   174  	if err != nil {
   175  		ctx.Error().Err(err).Msg("Could not create remote directory!")
   176  		return fuse.EREMOTEIO
   177  	}
   178  
   179  	newInode := NewInodeDriveItem(item)
   180  	newInode.mode = in.Mode | fuse.S_IFDIR
   181  
   182  	out.NodeId = f.InsertChild(id, newInode)
   183  	out.Attr = newInode.makeAttr()
   184  	out.SetAttrTimeout(timeout)
   185  	out.SetEntryTimeout(timeout)
   186  	return fuse.OK
   187  }
   188  
   189  // Rmdir removes a directory if it's empty.
   190  func (f *Filesystem) Rmdir(cancel <-chan struct{}, in *fuse.InHeader, name string) fuse.Status {
   191  	parentID := f.TranslateID(in.NodeId)
   192  	if parentID == "" {
   193  		return fuse.ENOENT
   194  	}
   195  	child, _ := f.GetChild(parentID, name, f.auth)
   196  	if child == nil {
   197  		return fuse.ENOENT
   198  	}
   199  	if child.HasChildren() {
   200  		return fuse.Status(syscall.ENOTEMPTY)
   201  	}
   202  	return f.Unlink(cancel, in, name)
   203  }
   204  
   205  // ReadDir provides a list of all the entries in the directory
   206  func (f *Filesystem) OpenDir(cancel <-chan struct{}, in *fuse.OpenIn, out *fuse.OpenOut) fuse.Status {
   207  	id := f.TranslateID(in.NodeId)
   208  	dir := f.GetID(id)
   209  	if dir == nil {
   210  		return fuse.ENOENT
   211  	}
   212  	if !dir.IsDir() {
   213  		return fuse.ENOTDIR
   214  	}
   215  	path := dir.Path()
   216  	ctx := log.With().
   217  		Str("op", "OpenDir").
   218  		Uint64("nodeID", in.NodeId).
   219  		Str("id", id).
   220  		Str("path", path).Logger()
   221  	ctx.Debug().Msg("")
   222  
   223  	children, err := f.GetChildrenID(id, f.auth)
   224  	if err != nil {
   225  		// not an item not found error (Lookup/Getattr will always be called
   226  		// before Readdir()), something has happened to our connection
   227  		ctx.Error().Err(err).Msg("Could not fetch children")
   228  		return fuse.EREMOTEIO
   229  	}
   230  
   231  	parent := f.GetID(dir.ParentID())
   232  	if parent == nil {
   233  		// This is the parent of the mountpoint. The FUSE kernel module discards
   234  		// this info, so what we put here doesn't actually matter.
   235  		parent = NewInode("..", 0755|fuse.S_IFDIR, nil)
   236  		parent.nodeID = math.MaxUint64
   237  	}
   238  
   239  	entries := make([]*Inode, 2)
   240  	entries[0] = dir
   241  	entries[1] = parent
   242  
   243  	for _, child := range children {
   244  		entries = append(entries, child)
   245  	}
   246  	f.opendirsM.Lock()
   247  	f.opendirs[in.NodeId] = entries
   248  	f.opendirsM.Unlock()
   249  
   250  	return fuse.OK
   251  }
   252  
   253  // ReleaseDir closes a directory and purges it from memory
   254  func (f *Filesystem) ReleaseDir(in *fuse.ReleaseIn) {
   255  	f.opendirsM.Lock()
   256  	delete(f.opendirs, in.NodeId)
   257  	f.opendirsM.Unlock()
   258  }
   259  
   260  // ReadDirPlus reads an individual directory entry AND does a lookup.
   261  func (f *Filesystem) ReadDirPlus(cancel <-chan struct{}, in *fuse.ReadIn, out *fuse.DirEntryList) fuse.Status {
   262  	f.opendirsM.RLock()
   263  	entries, ok := f.opendirs[in.NodeId]
   264  	f.opendirsM.RUnlock()
   265  	if !ok {
   266  		// readdir can sometimes arrive before the corresponding opendir, so we force it
   267  		f.OpenDir(cancel, &fuse.OpenIn{InHeader: in.InHeader}, nil)
   268  		f.opendirsM.RLock()
   269  		entries, ok = f.opendirs[in.NodeId]
   270  		f.opendirsM.RUnlock()
   271  		if !ok {
   272  			return fuse.EBADF
   273  		}
   274  	}
   275  
   276  	if in.Offset >= uint64(len(entries)) {
   277  		// just tried to seek past end of directory, we're all done!
   278  		return fuse.OK
   279  	}
   280  
   281  	inode := entries[in.Offset]
   282  	entry := fuse.DirEntry{
   283  		Ino:  inode.NodeID(),
   284  		Mode: inode.Mode(),
   285  	}
   286  	// first two entries will always be "." and ".."
   287  	switch in.Offset {
   288  	case 0:
   289  		entry.Name = "."
   290  	case 1:
   291  		entry.Name = ".."
   292  	default:
   293  		entry.Name = inode.Name()
   294  	}
   295  	entryOut := out.AddDirLookupEntry(entry)
   296  	if entryOut == nil {
   297  		//FIXME probably need to handle this better using the "overflow stuff"
   298  		log.Error().
   299  			Str("op", "ReadDirPlus").
   300  			Uint64("nodeID", in.NodeId).
   301  			Uint64("offset", in.Offset).
   302  			Str("entryName", entry.Name).
   303  			Uint64("entryNodeID", entry.Ino).
   304  			Msg("Exceeded DirLookupEntry bounds!")
   305  		return fuse.EIO
   306  	}
   307  	entryOut.NodeId = entry.Ino
   308  	entryOut.Attr = inode.makeAttr()
   309  	entryOut.SetAttrTimeout(timeout)
   310  	entryOut.SetEntryTimeout(timeout)
   311  	return fuse.OK
   312  }
   313  
   314  // ReadDir reads a directory entry. Usually doesn't get called (ReadDirPlus is
   315  // typically used).
   316  func (f *Filesystem) ReadDir(cancel <-chan struct{}, in *fuse.ReadIn, out *fuse.DirEntryList) fuse.Status {
   317  	f.opendirsM.RLock()
   318  	entries, ok := f.opendirs[in.NodeId]
   319  	f.opendirsM.RUnlock()
   320  	if !ok {
   321  		// readdir can sometimes arrive before the corresponding opendir, so we force it
   322  		f.OpenDir(cancel, &fuse.OpenIn{InHeader: in.InHeader}, nil)
   323  		f.opendirsM.RLock()
   324  		entries, ok = f.opendirs[in.NodeId]
   325  		f.opendirsM.RUnlock()
   326  		if !ok {
   327  			return fuse.EBADF
   328  		}
   329  	}
   330  
   331  	if in.Offset >= uint64(len(entries)) {
   332  		// just tried to seek past end of directory, we're all done!
   333  		return fuse.OK
   334  	}
   335  
   336  	inode := entries[in.Offset]
   337  	entry := fuse.DirEntry{
   338  		Ino:  inode.NodeID(),
   339  		Mode: inode.Mode(),
   340  	}
   341  	// first two entries will always be "." and ".."
   342  	switch in.Offset {
   343  	case 0:
   344  		entry.Name = "."
   345  	case 1:
   346  		entry.Name = ".."
   347  	default:
   348  		entry.Name = inode.Name()
   349  	}
   350  
   351  	out.AddDirEntry(entry)
   352  	return fuse.OK
   353  }
   354  
   355  // Lookup is called by the kernel when the VFS wants to know about a file inside
   356  // a directory.
   357  func (f *Filesystem) Lookup(cancel <-chan struct{}, in *fuse.InHeader, name string, out *fuse.EntryOut) fuse.Status {
   358  	id := f.TranslateID(in.NodeId)
   359  	log.Trace().
   360  		Str("op", "Lookup").
   361  		Uint64("nodeID", in.NodeId).
   362  		Str("id", id).
   363  		Str("name", name).
   364  		Msg("")
   365  
   366  	child, _ := f.GetChild(id, strings.ToLower(name), f.auth)
   367  	if child == nil {
   368  		return fuse.ENOENT
   369  	}
   370  
   371  	out.NodeId = child.NodeID()
   372  	out.Attr = child.makeAttr()
   373  	out.SetAttrTimeout(timeout)
   374  	out.SetEntryTimeout(timeout)
   375  	return fuse.OK
   376  }
   377  
   378  // Mknod creates a regular file. The server doesn't have this yet.
   379  func (f *Filesystem) Mknod(cancel <-chan struct{}, in *fuse.MknodIn, name string, out *fuse.EntryOut) fuse.Status {
   380  	if isNameRestricted(name) {
   381  		return fuse.EINVAL
   382  	}
   383  
   384  	parentID := f.TranslateID(in.NodeId)
   385  	if parentID == "" {
   386  		return fuse.EBADF
   387  	}
   388  
   389  	parent := f.GetID(parentID)
   390  	if parent == nil {
   391  		return fuse.ENOENT
   392  	}
   393  
   394  	path := filepath.Join(parent.Path(), name)
   395  	ctx := log.With().
   396  		Str("op", "Mknod").
   397  		Uint64("nodeID", in.NodeId).
   398  		Str("path", path).
   399  		Logger()
   400  	if f.IsOffline() {
   401  		ctx.Warn().Msg("We are offline. Refusing Mknod() to avoid data loss later.")
   402  		return fuse.EROFS
   403  	}
   404  
   405  	if child, _ := f.GetChild(parentID, name, f.auth); child != nil {
   406  		return fuse.Status(syscall.EEXIST)
   407  	}
   408  
   409  	inode := NewInode(name, in.Mode, parent)
   410  	ctx.Debug().
   411  		Str("childID", inode.ID()).
   412  		Str("mode", Octal(in.Mode)).
   413  		Msg("Creating inode.")
   414  	out.NodeId = f.InsertChild(parentID, inode)
   415  	out.Attr = inode.makeAttr()
   416  	out.SetAttrTimeout(timeout)
   417  	out.SetEntryTimeout(timeout)
   418  	return fuse.OK
   419  }
   420  
   421  // Create creates a regular file and opens it. The server doesn't have this yet.
   422  func (f *Filesystem) Create(cancel <-chan struct{}, in *fuse.CreateIn, name string, out *fuse.CreateOut) fuse.Status {
   423  	// we reuse mknod here
   424  	result := f.Mknod(
   425  		cancel,
   426  		// we don't actually use the umask or padding here, so they don't get passed
   427  		&fuse.MknodIn{
   428  			InHeader: in.InHeader,
   429  			Mode:     in.Mode,
   430  		},
   431  		name,
   432  		&out.EntryOut,
   433  	)
   434  	if result == fuse.Status(syscall.EEXIST) {
   435  		// if the inode already exists, we should truncate the existing file and
   436  		// return the existing file inode as per "man creat"
   437  		parentID := f.TranslateID(in.NodeId)
   438  		child, _ := f.GetChild(parentID, name, f.auth)
   439  		log.Debug().
   440  			Str("op", "Create").
   441  			Uint64("nodeID", in.NodeId).
   442  			Str("id", parentID).
   443  			Str("childID", child.ID()).
   444  			Str("path", child.Path()).
   445  			Str("mode", Octal(in.Mode)).
   446  			Msg("Child inode already exists, truncating.")
   447  		f.content.Delete(child.ID())
   448  		f.content.Open(child.ID())
   449  		child.DriveItem.Size = 0
   450  		child.hasChanges = true
   451  		return fuse.OK
   452  	}
   453  	// no further initialized required to open the file, it's empty
   454  	return result
   455  }
   456  
   457  // Open fetches a Inodes's content and initializes the .Data field with actual
   458  // data from the server.
   459  func (f *Filesystem) Open(cancel <-chan struct{}, in *fuse.OpenIn, out *fuse.OpenOut) fuse.Status {
   460  	id := f.TranslateID(in.NodeId)
   461  	inode := f.GetID(id)
   462  	if inode == nil {
   463  		return fuse.ENOENT
   464  	}
   465  
   466  	path := inode.Path()
   467  	ctx := log.With().
   468  		Str("op", "Open").
   469  		Uint64("nodeID", in.NodeId).
   470  		Str("id", id).
   471  		Str("path", path).
   472  		Logger()
   473  
   474  	flags := int(in.Flags)
   475  	if flags&os.O_RDWR+flags&os.O_WRONLY > 0 && f.IsOffline() {
   476  		ctx.Warn().
   477  			Bool("readWrite", flags&os.O_RDWR > 0).
   478  			Bool("writeOnly", flags&os.O_WRONLY > 0).
   479  			Msg("Refusing Open() with write flag, FS is offline.")
   480  		return fuse.EROFS
   481  	}
   482  
   483  	ctx.Debug().Msg("")
   484  
   485  	// try grabbing from disk
   486  	fd, err := f.content.Open(id)
   487  	if err != nil {
   488  		ctx.Error().Err(err).Msg("Could not create cache file.")
   489  		return fuse.EIO
   490  	}
   491  
   492  	if isLocalID(id) {
   493  		// just use whatever's present if we're the only ones who have it
   494  		return fuse.OK
   495  	}
   496  
   497  	// we have something on disk-
   498  	// verify content against what we're supposed to have
   499  	inode.Lock()
   500  	defer inode.Unlock()
   501  	// stay locked until end to prevent multiple Opens() from competing for
   502  	// downloads of the same file.
   503  
   504  	if inode.VerifyChecksum(graph.QuickXORHashStream(fd)) {
   505  		// disk content is only used if the checksums match
   506  		ctx.Info().Msg("Found content in cache.")
   507  
   508  		// we check size ourselves in case the API file sizes are WRONG (it happens)
   509  		st, _ := fd.Stat()
   510  		inode.DriveItem.Size = uint64(st.Size())
   511  		return fuse.OK
   512  	}
   513  
   514  	ctx.Info().Msg(
   515  		"Not using cached item due to file hash mismatch, fetching content from API.",
   516  	)
   517  
   518  	// write to tempfile first to ensure our download is good
   519  	tempID := "temp-" + id
   520  	temp, err := f.content.Open(tempID)
   521  	if err != nil {
   522  		ctx.Error().Err(err).Msg("Failed to create tempfile for download.")
   523  		return fuse.EIO
   524  	}
   525  	defer f.content.Delete(tempID)
   526  
   527  	// replace content only on a match
   528  	size, err := graph.GetItemContentStream(id, f.auth, temp)
   529  	if err != nil || !inode.VerifyChecksum(graph.QuickXORHashStream(temp)) {
   530  		ctx.Error().Err(err).Msg("Failed to fetch remote content.")
   531  		return fuse.EREMOTEIO
   532  	}
   533  	temp.Seek(0, 0) // being explicit, even though already done in hashstream func
   534  	fd.Seek(0, 0)
   535  	fd.Truncate(0)
   536  	io.Copy(fd, temp)
   537  	inode.DriveItem.Size = size
   538  	return fuse.OK
   539  }
   540  
   541  // Unlink deletes a child file.
   542  func (f *Filesystem) Unlink(cancel <-chan struct{}, in *fuse.InHeader, name string) fuse.Status {
   543  	parentID := f.TranslateID(in.NodeId)
   544  	child, _ := f.GetChild(parentID, name, nil)
   545  	if child == nil {
   546  		// the file we are unlinking never existed
   547  		return fuse.ENOENT
   548  	}
   549  	if f.IsOffline() {
   550  		return fuse.EROFS
   551  	}
   552  
   553  	id := child.ID()
   554  	path := child.Path()
   555  	ctx := log.With().
   556  		Str("op", "Unlink").
   557  		Uint64("nodeID", in.NodeId).
   558  		Str("id", parentID).
   559  		Str("childID", id).
   560  		Str("path", path).
   561  		Logger()
   562  	ctx.Debug().Msg("Unlinking inode.")
   563  
   564  	// if no ID, the item is local-only, and does not need to be deleted on the
   565  	// server
   566  	if !isLocalID(id) {
   567  		if err := graph.Remove(id, f.auth); err != nil {
   568  			ctx.Err(err).Msg("Failed to delete item on server. Aborting op.")
   569  			return fuse.EREMOTEIO
   570  		}
   571  	}
   572  
   573  	f.DeleteID(id)
   574  	f.content.Delete(id)
   575  	return fuse.OK
   576  }
   577  
   578  // Read an inode's data like a file.
   579  func (f *Filesystem) Read(cancel <-chan struct{}, in *fuse.ReadIn, buf []byte) (fuse.ReadResult, fuse.Status) {
   580  	inode := f.GetNodeID(in.NodeId)
   581  	if inode == nil {
   582  		return fuse.ReadResultData(make([]byte, 0)), fuse.EBADF
   583  	}
   584  
   585  	id := inode.ID()
   586  	path := inode.Path()
   587  	ctx := log.With().
   588  		Str("op", "Read").
   589  		Uint64("nodeID", in.NodeId).
   590  		Str("id", id).
   591  		Str("path", path).
   592  		Int("bufsize", len(buf)).
   593  		Logger()
   594  	ctx.Trace().Msg("")
   595  
   596  	fd, err := f.content.Open(id)
   597  	if err != nil {
   598  		ctx.Error().Err(err).Msg("Cache Open() failed.")
   599  		return fuse.ReadResultData(make([]byte, 0)), fuse.EIO
   600  	}
   601  
   602  	// we are locked for the remainder of this op
   603  	inode.RLock()
   604  	defer inode.RUnlock()
   605  	return fuse.ReadResultFd(fd.Fd(), int64(in.Offset), int(in.Size)), fuse.OK
   606  }
   607  
   608  // Write to an Inode like a file. Note that changes are 100% local until
   609  // Flush() is called. Returns the number of bytes written and the status of the
   610  // op.
   611  func (f *Filesystem) Write(cancel <-chan struct{}, in *fuse.WriteIn, data []byte) (uint32, fuse.Status) {
   612  	id := f.TranslateID(in.NodeId)
   613  	inode := f.GetID(id)
   614  	if inode == nil {
   615  		return 0, fuse.EBADF
   616  	}
   617  
   618  	nWrite := len(data)
   619  	offset := int(in.Offset)
   620  	ctx := log.With().
   621  		Str("op", "Write").
   622  		Str("id", id).
   623  		Uint64("nodeID", in.NodeId).
   624  		Str("path", inode.Path()).
   625  		Int("bufsize", nWrite).
   626  		Int("offset", offset).
   627  		Logger()
   628  	ctx.Trace().Msg("")
   629  
   630  	fd, err := f.content.Open(id)
   631  	if err != nil {
   632  		ctx.Error().Msg("Cache Open() failed.")
   633  		return 0, fuse.EIO
   634  	}
   635  
   636  	inode.Lock()
   637  	defer inode.Unlock()
   638  	n, err := fd.WriteAt(data, int64(offset))
   639  	if err != nil {
   640  		ctx.Error().Err(err).Msg("Error during write")
   641  		return uint32(n), fuse.EIO
   642  	}
   643  
   644  	st, _ := fd.Stat()
   645  	inode.DriveItem.Size = uint64(st.Size())
   646  	inode.hasChanges = true
   647  	return uint32(n), fuse.OK
   648  }
   649  
   650  // Fsync is a signal to ensure writes to the Inode are flushed to stable
   651  // storage. This method is used to trigger uploads of file content.
   652  func (f *Filesystem) Fsync(cancel <-chan struct{}, in *fuse.FsyncIn) fuse.Status {
   653  	id := f.TranslateID(in.NodeId)
   654  	inode := f.GetID(id)
   655  	if inode == nil {
   656  		return fuse.EBADF
   657  	}
   658  
   659  	ctx := log.With().
   660  		Str("op", "Fsync").
   661  		Str("id", id).
   662  		Uint64("nodeID", in.NodeId).
   663  		Str("path", inode.Path()).
   664  		Logger()
   665  	ctx.Debug().Msg("")
   666  	if inode.HasChanges() {
   667  		inode.Lock()
   668  		inode.hasChanges = false
   669  
   670  		// recompute hashes when saving new content
   671  		inode.DriveItem.File = &graph.File{}
   672  		fd, err := f.content.Open(id)
   673  		if err != nil {
   674  			ctx.Error().Err(err).Msg("Could not get fd.")
   675  		}
   676  		fd.Sync()
   677  		inode.DriveItem.File.Hashes.QuickXorHash = graph.QuickXORHashStream(fd)
   678  		inode.Unlock()
   679  
   680  		if err := f.uploads.QueueUpload(inode); err != nil {
   681  			ctx.Error().Err(err).Msg("Error creating upload session.")
   682  			return fuse.EREMOTEIO
   683  		}
   684  		return fuse.OK
   685  	}
   686  	return fuse.OK
   687  }
   688  
   689  // Flush is called when a file descriptor is closed. Uses Fsync() to perform file
   690  // uploads. (Release not implemented because all cleanup is already done here).
   691  func (f *Filesystem) Flush(cancel <-chan struct{}, in *fuse.FlushIn) fuse.Status {
   692  	inode := f.GetNodeID(in.NodeId)
   693  	if inode == nil {
   694  		return fuse.EBADF
   695  	}
   696  
   697  	id := inode.ID()
   698  	log.Trace().
   699  		Str("op", "Flush").
   700  		Str("id", id).
   701  		Str("path", inode.Path()).
   702  		Uint64("nodeID", in.NodeId).
   703  		Msg("")
   704  	f.Fsync(cancel, &fuse.FsyncIn{InHeader: in.InHeader})
   705  	f.content.Close(id)
   706  	return 0
   707  }
   708  
   709  // Getattr returns a the Inode as a UNIX stat. Holds the read mutex for all of
   710  // the "metadata fetch" operations.
   711  func (f *Filesystem) GetAttr(cancel <-chan struct{}, in *fuse.GetAttrIn, out *fuse.AttrOut) fuse.Status {
   712  	id := f.TranslateID(in.NodeId)
   713  	inode := f.GetID(id)
   714  	if inode == nil {
   715  		return fuse.ENOENT
   716  	}
   717  	log.Trace().
   718  		Str("op", "GetAttr").
   719  		Uint64("nodeID", in.NodeId).
   720  		Str("id", id).
   721  		Str("path", inode.Path()).
   722  		Msg("")
   723  
   724  	out.Attr = inode.makeAttr()
   725  	out.SetTimeout(timeout)
   726  	return fuse.OK
   727  }
   728  
   729  // Setattr is the workhorse for setting filesystem attributes. Does the work of
   730  // operations like utimens, chmod, chown (not implemented, FUSE is single-user),
   731  // and truncate.
   732  func (f *Filesystem) SetAttr(cancel <-chan struct{}, in *fuse.SetAttrIn, out *fuse.AttrOut) fuse.Status {
   733  	i := f.GetNodeID(in.NodeId)
   734  	if i == nil {
   735  		return fuse.ENOENT
   736  	}
   737  	path := i.Path()
   738  	isDir := i.IsDir() // holds an rlock
   739  	i.Lock()
   740  
   741  	ctx := log.With().
   742  		Str("op", "SetAttr").
   743  		Uint64("nodeID", in.NodeId).
   744  		Str("id", i.DriveItem.ID).
   745  		Str("path", path).
   746  		Logger()
   747  
   748  	// utimens
   749  	if mtime, valid := in.GetMTime(); valid {
   750  		ctx.Info().
   751  			Str("subop", "utimens").
   752  			Time("oldMtime", *i.DriveItem.ModTime).
   753  			Time("newMtime", *i.DriveItem.ModTime).
   754  			Msg("")
   755  		i.DriveItem.ModTime = &mtime
   756  	}
   757  
   758  	// chmod
   759  	if mode, valid := in.GetMode(); valid {
   760  		ctx.Info().
   761  			Str("subop", "chmod").
   762  			Str("oldMode", Octal(i.mode)).
   763  			Str("newMode", Octal(mode)).
   764  			Msg("")
   765  		if isDir {
   766  			i.mode = fuse.S_IFDIR | mode
   767  		} else {
   768  			i.mode = fuse.S_IFREG | mode
   769  		}
   770  	}
   771  
   772  	// truncate
   773  	if size, valid := in.GetSize(); valid {
   774  		ctx.Info().
   775  			Str("subop", "truncate").
   776  			Uint64("oldSize", i.DriveItem.Size).
   777  			Uint64("newSize", size).
   778  			Msg("")
   779  		fd, _ := f.content.Open(i.DriveItem.ID)
   780  		// the unix syscall does not update the seek position, so neither should we
   781  		fd.Truncate(int64(size))
   782  		i.DriveItem.Size = size
   783  		i.hasChanges = true
   784  	}
   785  
   786  	i.Unlock()
   787  	out.Attr = i.makeAttr()
   788  	out.SetTimeout(timeout)
   789  	return fuse.OK
   790  }
   791  
   792  // Rename renames and/or moves an inode.
   793  func (f *Filesystem) Rename(cancel <-chan struct{}, in *fuse.RenameIn, name string, newName string) fuse.Status {
   794  	if isNameRestricted(newName) {
   795  		return fuse.EINVAL
   796  	}
   797  
   798  	oldParentID := f.TranslateID(in.NodeId)
   799  	oldParentItem := f.GetNodeID(in.NodeId)
   800  	if oldParentID == "" || oldParentItem == nil {
   801  		return fuse.EBADF
   802  	}
   803  	path := filepath.Join(oldParentItem.Path(), name)
   804  
   805  	// we'll have the metadata for the dest inode already so it is not necessary
   806  	// to use GetPath() to prefetch it. In order for the fs to know about this
   807  	// inode, it has already fetched all of the inodes up to the new destination.
   808  	newParentItem := f.GetNodeID(in.Newdir)
   809  	if newParentItem == nil {
   810  		return fuse.ENOENT
   811  	}
   812  	dest := filepath.Join(newParentItem.Path(), newName)
   813  
   814  	inode, _ := f.GetChild(oldParentID, name, f.auth)
   815  	id, err := f.remoteID(inode)
   816  	newParentID := newParentItem.ID()
   817  
   818  	ctx := log.With().
   819  		Str("op", "Rename").
   820  		Str("id", id).
   821  		Str("parentID", newParentID).
   822  		Str("path", path).
   823  		Str("dest", dest).
   824  		Logger()
   825  	ctx.Info().
   826  		Uint64("srcNodeID", in.NodeId).
   827  		Uint64("dstNodeID", in.Newdir).
   828  		Msg("")
   829  
   830  	if isLocalID(id) || err != nil {
   831  		// uploads will fail without an id
   832  		ctx.Error().Err(err).
   833  			Msg("ID of item to move cannot be local and we failed to obtain an ID.")
   834  		return fuse.EREMOTEIO
   835  	}
   836  
   837  	// perform remote rename
   838  	if err = graph.Rename(id, newName, newParentID, f.auth); err != nil {
   839  		ctx.Error().Err(err).Msg("Failed to rename remote item.")
   840  		return fuse.EREMOTEIO
   841  	}
   842  
   843  	// now rename local copy
   844  	if err = f.MovePath(oldParentID, newParentID, name, newName, f.auth); err != nil {
   845  		ctx.Error().Err(err).Msg("Failed to rename local item.")
   846  		return fuse.EIO
   847  	}
   848  
   849  	// whew! item renamed
   850  	return fuse.OK
   851  }