gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/fuse.go (about)

     1  //go:build linux || darwin
     2  // +build linux darwin
     3  
     4  package renter
     5  
     6  import (
     7  	"context"
     8  	"io"
     9  	"math"
    10  	"sync"
    11  	"sync/atomic"
    12  	"syscall"
    13  
    14  	"github.com/hanwen/go-fuse/v2/fs"
    15  	"github.com/hanwen/go-fuse/v2/fuse"
    16  	"gitlab.com/NebulousLabs/errors"
    17  	"gitlab.com/SkynetLabs/skyd/skymodules"
    18  	"gitlab.com/SkynetLabs/skyd/skymodules/renter/filesystem"
    19  )
    20  
    21  // fuseDirnode is a fuse node for the fs package that covers a siadir.
    22  //
    23  // NOTE: The fuseDirnode is _very_hot_ in that it gets hit rapidly and
    24  // concurrently and generally consumes a lot of CPU. If adding a mutex to
    25  // fuseDirnode, make as many variables static as possible and ensure that only
    26  // the non-hot functions need to use the mutex.
    27  //
    28  // In particular, the Lookup function should be computationally efficient.
    29  type fuseDirnode struct {
    30  	atomicClosed uint32
    31  
    32  	fs.Inode
    33  	staticDirNode    *filesystem.DirNode
    34  	staticFilesystem *fuseFS
    35  }
    36  
    37  // Ensure the dir nodes satisfy the required interfaces.
    38  //
    39  // NodeAccesser is necessary for telling certain programs that it is okay to
    40  // access the file.
    41  //
    42  // NodeFlusher is necessary for cleaning up resources such as the filesystem
    43  // node.
    44  //
    45  // NodeGetattrer provides details about the folder. This one may not be
    46  // strictly necessary, I'm not sure what exact value it adds.
    47  //
    48  // NodeLookuper is necessary to have files added to the filesystem tree.
    49  //
    50  // NodeReaddirer is necessary to list the files in a directory.
    51  //
    52  // NodeStatfser is necessary to provide information about the filesystem that
    53  // contains the directory.
    54  var _ = (fs.NodeAccesser)((*fuseDirnode)(nil))
    55  var _ = (fs.NodeFlusher)((*fuseDirnode)(nil))
    56  var _ = (fs.NodeGetattrer)((*fuseDirnode)(nil))
    57  var _ = (fs.NodeLookuper)((*fuseDirnode)(nil))
    58  var _ = (fs.NodeReaddirer)((*fuseDirnode)(nil))
    59  var _ = (fs.NodeStatfser)((*fuseDirnode)(nil))
    60  
    61  // fuseFilenode is a fuse node for the fs package that covers a siafile.
    62  //
    63  // Data is fetched using a download streamer. This download streamer needs to be
    64  // closed when the filehandle is released.
    65  type fuseFilenode struct {
    66  	atomicClosed uint32
    67  
    68  	fs.Inode
    69  	staticFilesystem *fuseFS
    70  	staticFileNode   *filesystem.FileNode
    71  	stream           skymodules.Streamer
    72  	mu               sync.Mutex
    73  }
    74  
    75  // Ensure the file nodes satisfy the required interfaces.
    76  //
    77  // NodeAccesser is necessary for telling certain programs that it is okay to
    78  // access the file.
    79  //
    80  // NodeFlusher is necessary for cleaning up resources such as the download
    81  // streamer.
    82  //
    83  // NodeGetattrer is necessary for providing the filesize to file browsers.
    84  //
    85  // NodeOpener is necessary for opening files to be read.
    86  //
    87  // NodeReader is necessary for reading files.
    88  //
    89  // NodeStatfser is necessary to provide information about the filesystem that
    90  // contains the file.
    91  var _ = (fs.NodeAccesser)((*fuseFilenode)(nil))
    92  var _ = (fs.NodeFlusher)((*fuseFilenode)(nil))
    93  var _ = (fs.NodeGetattrer)((*fuseFilenode)(nil))
    94  var _ = (fs.NodeOpener)((*fuseFilenode)(nil))
    95  var _ = (fs.NodeReader)((*fuseFilenode)(nil))
    96  var _ = (fs.NodeStatfser)((*fuseFilenode)(nil))
    97  
    98  // fuseRoot is the root directory for a mounted fuse filesystem.
    99  type fuseFS struct {
   100  	options skymodules.MountOptions
   101  	root    *fuseDirnode
   102  
   103  	renter *Renter
   104  	server *fuse.Server
   105  }
   106  
   107  // errToStatus converts an error to a syscall.Errno
   108  func errToStatus(err error) syscall.Errno {
   109  	if err == nil {
   110  		return syscall.F_OK
   111  	} else if errors.IsOSNotExist(err) {
   112  		return syscall.ENOENT
   113  	}
   114  	return syscall.EIO
   115  }
   116  
   117  // Access reports whether a directory can be accessed by the caller.
   118  func (fdn *fuseDirnode) Access(ctx context.Context, mask uint32) syscall.Errno {
   119  	// TODO: parse the mask and return a more correct value instead of always
   120  	// granting permission.
   121  	return syscall.F_OK
   122  }
   123  
   124  // Access reports whether a file can be accessed by the caller.
   125  func (ffn *fuseFilenode) Access(ctx context.Context, mask uint32) syscall.Errno {
   126  	// TODO: parse the mask and return a more correct value instead of always
   127  	// granting permission.
   128  	return syscall.F_OK
   129  }
   130  
   131  // Flush is called when a directory is being closed.
   132  func (fdn *fuseDirnode) Flush(ctx context.Context, fh fs.FileHandle) syscall.Errno {
   133  	var err error
   134  	notYetClosed := atomic.CompareAndSwapUint32(&fdn.atomicClosed, 0, 1)
   135  	if notYetClosed {
   136  		err = fdn.staticDirNode.Close()
   137  	}
   138  	return errToStatus(err)
   139  }
   140  
   141  // Flush is called when a file is being closed.
   142  func (ffn *fuseFilenode) Flush(ctx context.Context, fh fs.FileHandle) syscall.Errno {
   143  	swapped := atomic.CompareAndSwapUint32(&ffn.atomicClosed, 0, 1)
   144  	if !swapped {
   145  		return errToStatus(nil)
   146  	}
   147  	ffn.mu.Lock()
   148  	defer ffn.mu.Unlock()
   149  
   150  	// If a stream was opened for the file, the stream must now be closed.
   151  	var streamErr error
   152  	if ffn.stream != nil {
   153  		// Need to 'nil' out the stream once 'Flush' has been called because it
   154  		// can be called multiple times.
   155  		streamErr = ffn.stream.Close()
   156  	}
   157  
   158  	// Check all of the errors.
   159  	closeErr := ffn.staticFileNode.Close()
   160  	err := errors.Compose(streamErr, closeErr)
   161  	if err != nil {
   162  		siaPath := ffn.staticFilesystem.renter.staticFileSystem.FileSiaPath(ffn.staticFileNode)
   163  		ffn.staticFilesystem.renter.staticLog.Printf("error when flushing fuse file %v: %v", siaPath, err)
   164  		return errToStatus(err)
   165  	}
   166  	return errToStatus(nil)
   167  }
   168  
   169  // Lookup is a directory call that returns the file in the directory associated
   170  // with the provided name. When a file browser is opening folders with lots of
   171  // files, this method can be called thousands of times concurrently in a single
   172  // second. It goes without saying that this method needs to be very fast.
   173  func (fdn *fuseDirnode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
   174  	fileNode, fileErr := fdn.staticDirNode.File(name)
   175  	if fileErr == nil {
   176  		fileInfo, err := fdn.staticFilesystem.renter.staticFileSystem.FileNodeInfo(fileNode)
   177  		if err != nil {
   178  			siaPath := fdn.staticFilesystem.renter.staticFileSystem.DirSiaPath(fdn.staticDirNode)
   179  			fdn.staticFilesystem.renter.staticLog.Printf("Unable to fetch fileinfo on file %v from dir %v: %v", name, siaPath, err)
   180  			return nil, errToStatus(err)
   181  		}
   182  		// Convert the file to an inode.
   183  		filenode := &fuseFilenode{
   184  			staticFilesystem: fdn.staticFilesystem,
   185  			staticFileNode:   fileNode,
   186  		}
   187  		attrs := fs.StableAttr{
   188  			Ino:  fileInfo.UID,
   189  			Mode: fuse.S_IFREG,
   190  		}
   191  
   192  		// Set the crticial entry out values.
   193  		//
   194  		// TODO: Set more of these, there are like 20 of them.
   195  		out.Ino = fileInfo.UID
   196  		out.Size = fileInfo.Filesize
   197  		out.Mode = uint32(fileInfo.Mode())
   198  
   199  		inode := fdn.NewInode(ctx, filenode, attrs)
   200  		return inode, errToStatus(nil)
   201  	}
   202  
   203  	childDir, dirErr := fdn.staticDirNode.Dir(name)
   204  	if dirErr != nil {
   205  		siaPath := fdn.staticFilesystem.renter.staticFileSystem.DirSiaPath(fdn.staticDirNode)
   206  		fdn.staticFilesystem.renter.staticLog.Printf("Unable to perform lookup on %v in dir %v; file err %v :: dir err %v", name, siaPath, fileErr, dirErr)
   207  		return nil, errToStatus(dirErr)
   208  	}
   209  	dirInfo, err := fdn.staticFilesystem.renter.staticFileSystem.DirNodeInfo(childDir)
   210  	if err != nil {
   211  		fdn.staticFilesystem.renter.staticLog.Printf("Unable to fetch info from childDir: %v", err)
   212  		return nil, errToStatus(err)
   213  	}
   214  
   215  	// We found the directory we want, convert to an inode.
   216  	dirnode := &fuseDirnode{
   217  		staticDirNode:    childDir,
   218  		staticFilesystem: fdn.staticFilesystem,
   219  	}
   220  	attrs := fs.StableAttr{
   221  		Ino:  dirInfo.UID,
   222  		Mode: fuse.S_IFDIR,
   223  	}
   224  	out.Ino = dirInfo.UID
   225  	out.Mode = uint32(dirInfo.Mode())
   226  	inode := fdn.NewInode(ctx, dirnode, attrs)
   227  	return inode, errToStatus(nil)
   228  }
   229  
   230  // Getattr returns the attributes of a fuse dir.
   231  func (fdn *fuseDirnode) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
   232  	dirInfo, err := fdn.staticFilesystem.renter.staticFileSystem.DirNodeInfo(fdn.staticDirNode)
   233  	if err != nil {
   234  		fdn.staticFilesystem.renter.staticLog.Printf("Unable to fetch info from directory: %v", err)
   235  		return errToStatus(err)
   236  	}
   237  	out.Mode = uint32(dirInfo.Mode())
   238  	out.Ino = dirInfo.UID
   239  	return errToStatus(nil)
   240  }
   241  
   242  // Getattr returns the attributes of a fuse file.
   243  //
   244  // NOTE: When ffmpeg is running on a video, it spams Getattr on the open file.
   245  // Getattr should try to minimize lock contention and should run very quickly if
   246  // possible.
   247  func (ffn *fuseFilenode) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
   248  	fileInfo, err := ffn.staticFilesystem.renter.staticFileSystem.FileNodeInfo(ffn.staticFileNode)
   249  	if err != nil {
   250  		ffn.staticFilesystem.renter.staticLog.Printf("Unable to fetch info from file: %v", err)
   251  	}
   252  
   253  	out.Size = fileInfo.Filesize
   254  	out.Mode = uint32(fileInfo.Mode()) | syscall.S_IFREG
   255  	out.Ino = fileInfo.UID
   256  	return errToStatus(nil)
   257  }
   258  
   259  // Open will open a streamer for the file.
   260  //
   261  // TODO: Currently 'Open' returns '0' for the fuseFlags. I was unable to figure
   262  // out from the documentation what the flags are supposed to represent. So far,
   263  // this has not seemed to cause problems.
   264  func (ffn *fuseFilenode) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) {
   265  	ffn.mu.Lock()
   266  	defer ffn.mu.Unlock()
   267  
   268  	stream, err := ffn.staticFilesystem.renter.StreamerByNode(ffn.staticFileNode, false)
   269  	if err != nil {
   270  		siaPath := ffn.staticFilesystem.renter.staticFileSystem.FileSiaPath(ffn.staticFileNode)
   271  		ffn.staticFilesystem.renter.staticLog.Printf("Unable to get stream for file %v: %v", siaPath, err)
   272  		return nil, 0, errToStatus(err)
   273  	}
   274  	ffn.stream = stream
   275  	return ffn, 0, errToStatus(nil)
   276  }
   277  
   278  // Read will read data from the file and place it in dest.
   279  func (ffn *fuseFilenode) Read(ctx context.Context, f fs.FileHandle, dest []byte, offset int64) (fuse.ReadResult, syscall.Errno) {
   280  	// TODO: Right now only one call to Read from a file can be in effect at
   281  	// once, based on the way the streamer and the read call has been
   282  	// implemented. As the streamer gets updated to more readily support
   283  	// multiple concurrrent streams at once, this method can be re-implemented
   284  	// to greatly increase speeds.
   285  	ffn.mu.Lock()
   286  	defer ffn.mu.Unlock()
   287  
   288  	_, err := ffn.stream.Seek(offset, io.SeekStart)
   289  	if err != nil {
   290  		siaPath := ffn.staticFilesystem.renter.staticFileSystem.FileSiaPath(ffn.staticFileNode)
   291  		ffn.staticFilesystem.renter.staticLog.Printf("Error seeking to offset %v during call to Read in file %s: %v", offset, siaPath.String(), err)
   292  		return nil, errToStatus(err)
   293  	}
   294  
   295  	// Ignore both EOF and ErrUnexpectedEOF when doing the ReadFull. If we
   296  	// return ErrUnexpectedEOF, the program will try to read again but with a
   297  	// smaller read size and be confused about how large the file actually is,
   298  	// often dropping parts of the tail of the file.
   299  	n, err := io.ReadFull(ffn.stream, dest)
   300  	if err != nil && !errors.Contains(err, io.EOF) && err != io.ErrUnexpectedEOF {
   301  		siaPath := ffn.staticFilesystem.renter.staticFileSystem.FileSiaPath(ffn.staticFileNode)
   302  		ffn.staticFilesystem.renter.staticLog.Printf("Error reading from offset %v during call to Read in file %s: %v", offset, siaPath.String(), err)
   303  		return nil, errToStatus(err)
   304  	}
   305  
   306  	// Format the data in a way fuse understands and return.
   307  	return fuse.ReadResultData(dest[:n]), errToStatus(nil)
   308  }
   309  
   310  // Readdir will return a dirstream that can be used to look at all of the files
   311  // in the directory.
   312  func (fdn *fuseDirnode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
   313  	fileinfos, dirinfos, err := fdn.staticFilesystem.renter.staticFileSystem.CachedListOnNode(fdn.staticDirNode)
   314  	if err != nil {
   315  		siaPath := fdn.staticFilesystem.renter.staticFileSystem.DirSiaPath(fdn.staticDirNode)
   316  		fdn.staticFilesystem.renter.staticLog.Printf("Unable to get file and directory list for fuse directory %v: %v", siaPath, err)
   317  		return nil, errToStatus(err)
   318  	}
   319  
   320  	// Convert the fileinfos and dirinfos to []fuse.DirEntry
   321  	dirEntries := make([]fuse.DirEntry, 0, len(fileinfos)+len(dirinfos))
   322  	for _, fi := range fileinfos {
   323  		dirEntries = append(dirEntries, fuse.DirEntry{
   324  			Mode: uint32(fi.Mode()) | fuse.S_IFREG,
   325  			Name: fi.Name(),
   326  		})
   327  	}
   328  	// Skip the first directory, as the first directory is always the self
   329  	// directory.
   330  	for _, di := range dirinfos[1:] {
   331  		dirEntries = append(dirEntries, fuse.DirEntry{
   332  			Mode: uint32(di.Mode()) | fuse.S_IFDIR,
   333  			Name: di.Name(),
   334  		})
   335  	}
   336  
   337  	// The fuse package has a helper to convert a []fuse.DirEntry to a
   338  	// fuse.DirStream, we will use that here.
   339  	return fs.NewListDirStream(dirEntries), errToStatus(nil)
   340  }
   341  
   342  // setStatfsOut is a method that will set the StatfsOut fields which are
   343  // consistent across the fuse filesystem.
   344  func (ffs *fuseFS) setStatfsOut(out *fuse.StatfsOut) error {
   345  	// Get the allowance for the renter. This can be used to determine the total
   346  	// amount of space available.
   347  	settings, err := ffs.renter.Settings()
   348  	if err != nil {
   349  		return errors.AddContext(err, "unable to fetch renter settings")
   350  	}
   351  	totalStorage := settings.Allowance.ExpectedStorage
   352  
   353  	// Get fileinfo for the root directory and use that to compute the amount of
   354  	// storage in use and the number of files in the filesystem.
   355  	dirs, err := ffs.renter.DirList(skymodules.RootSiaPath())
   356  	if err != nil {
   357  		return errors.AddContext(err, "unable to fetch root directory infos")
   358  	}
   359  	if len(dirs) < 1 {
   360  		return errors.New("calling DirList on root directory returned no results")
   361  	}
   362  	rootDir := dirs[0]
   363  	usedStorage := rootDir.AggregateSize
   364  	numFiles := uint64(rootDir.AggregateNumFiles)
   365  
   366  	// Compute the amount of storage that's available.
   367  	//
   368  	// TODO: Could be more accurate if this value is small based on the amount
   369  	// of money remaining in the allowance and in the contracts.
   370  	var availStorage uint64
   371  	if totalStorage > usedStorage {
   372  		availStorage = totalStorage - usedStorage
   373  	}
   374  
   375  	// TODO: This is just totally made up.
   376  	blockSize := uint32(1 << 16)
   377  
   378  	// Set all of the out fields.
   379  	out.Blocks = totalStorage / uint64(blockSize)
   380  	out.Bfree = availStorage / uint64(blockSize)
   381  	out.Bavail = availStorage / uint64(blockSize)
   382  	out.Files = numFiles
   383  	out.Ffree = 1e6 // TODO: Not really sure what to do here. Description is "free file nodes in filesystem".
   384  	out.Bsize = blockSize
   385  	out.NameLen = math.MaxUint32 // There is no actual limit.
   386  	out.Frsize = blockSize
   387  	return nil
   388  }
   389  
   390  // Statfs will return the statfs fields for this directory.
   391  func (fdn *fuseDirnode) Statfs(ctx context.Context, out *fuse.StatfsOut) syscall.Errno {
   392  	err := fdn.staticFilesystem.setStatfsOut(out)
   393  	if err != nil {
   394  		siaPath := fdn.staticFilesystem.renter.staticFileSystem.DirSiaPath(fdn.staticDirNode)
   395  		fdn.staticFilesystem.renter.staticLog.Printf("Error fetching statfs for fuse dir %v: %v", siaPath, err)
   396  		return errToStatus(err)
   397  	}
   398  	return errToStatus(nil)
   399  }
   400  
   401  // Statfs will return the statfs fields for this file.
   402  func (ffn *fuseFilenode) Statfs(ctx context.Context, out *fuse.StatfsOut) syscall.Errno {
   403  	err := ffn.staticFilesystem.setStatfsOut(out)
   404  	if err != nil {
   405  		siaPath := ffn.staticFilesystem.renter.staticFileSystem.FileSiaPath(ffn.staticFileNode)
   406  		ffn.staticFilesystem.renter.staticLog.Printf("Error fetching statfs for fuse file %v: %v", siaPath, err)
   407  		return errToStatus(err)
   408  	}
   409  	return errToStatus(nil)
   410  }