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 }