github.com/anacrolix/torrent@v1.61.0/fs/torrentfs.go (about)

     1  //go:build !windows
     2  
     3  package torrentfs
     4  
     5  import (
     6  	"context"
     7  	"expvar"
     8  	"os"
     9  	"strings"
    10  	"sync"
    11  
    12  	"github.com/anacrolix/fuse"
    13  	fusefs "github.com/anacrolix/fuse/fs"
    14  
    15  	"github.com/anacrolix/torrent"
    16  	"github.com/anacrolix/torrent/metainfo"
    17  )
    18  
    19  const (
    20  	defaultMode = 0o555
    21  )
    22  
    23  var torrentfsReadRequests = expvar.NewInt("torrentfsReadRequests")
    24  
    25  type TorrentFS struct {
    26  	Client       *torrent.Client
    27  	destroyed    chan struct{}
    28  	mu           sync.Mutex
    29  	blockedReads int
    30  	event        sync.Cond
    31  }
    32  
    33  var (
    34  	_ fusefs.FSDestroyer = &TorrentFS{}
    35  
    36  	_ fusefs.NodeForgetter      = rootNode{}
    37  	_ fusefs.HandleReadDirAller = rootNode{}
    38  	_ fusefs.HandleReadDirAller = dirNode{}
    39  )
    40  
    41  // Is a directory node that lists all torrents and handles destruction of the
    42  // filesystem.
    43  type rootNode struct {
    44  	fs *TorrentFS
    45  }
    46  
    47  type node struct {
    48  	path     string
    49  	metadata *metainfo.Info
    50  	FS       *TorrentFS
    51  	t        *torrent.Torrent
    52  }
    53  
    54  type dirNode struct {
    55  	node
    56  }
    57  
    58  var _ fusefs.HandleReadDirAller = dirNode{}
    59  
    60  func isSubPath(parent, child string) bool {
    61  	if parent == "" {
    62  		return len(child) > 0
    63  	}
    64  	if !strings.HasPrefix(child, parent) {
    65  		return false
    66  	}
    67  	extra := child[len(parent):]
    68  	if extra == "" {
    69  		return false
    70  	}
    71  	// Not just a file with more stuff on the end.
    72  	return extra[0] == '/'
    73  }
    74  
    75  func (dn dirNode) ReadDirAll(ctx context.Context) (des []fuse.Dirent, err error) {
    76  	names := map[string]bool{}
    77  	for _, fi := range dn.metadata.UpvertedFiles() {
    78  		filePathname := strings.Join(fi.BestPath(), "/")
    79  		if !isSubPath(dn.path, filePathname) {
    80  			continue
    81  		}
    82  		var name string
    83  		if dn.path == "" {
    84  			name = fi.BestPath()[0]
    85  		} else {
    86  			dirPathname := strings.Split(dn.path, "/")
    87  			name = fi.BestPath()[len(dirPathname)]
    88  		}
    89  		if names[name] {
    90  			continue
    91  		}
    92  		names[name] = true
    93  		de := fuse.Dirent{
    94  			Name: name,
    95  		}
    96  		if len(fi.BestPath()) == len(dn.path)+1 {
    97  			de.Type = fuse.DT_File
    98  		} else {
    99  			de.Type = fuse.DT_Dir
   100  		}
   101  		des = append(des, de)
   102  	}
   103  	return
   104  }
   105  
   106  func (dn dirNode) Lookup(_ context.Context, name string) (fusefs.Node, error) {
   107  	dir := false
   108  	var file *torrent.File
   109  	var fullPath string
   110  	if dn.path != "" {
   111  		fullPath = dn.path + "/" + name
   112  	} else {
   113  		fullPath = name
   114  	}
   115  	for _, f := range dn.t.Files() {
   116  		if f.DisplayPath() == fullPath {
   117  			file = f
   118  		}
   119  		if isSubPath(fullPath, f.DisplayPath()) {
   120  			dir = true
   121  		}
   122  	}
   123  	n := dn.node
   124  	n.path = fullPath
   125  	if dir && file != nil {
   126  		panic("both dir and file")
   127  	}
   128  	if file != nil {
   129  		return fileNode{n, file}, nil
   130  	}
   131  	if dir {
   132  		return dirNode{n}, nil
   133  	}
   134  	return nil, fuse.ENOENT
   135  }
   136  
   137  func (dn dirNode) Attr(ctx context.Context, attr *fuse.Attr) error {
   138  	attr.Mode = os.ModeDir | defaultMode
   139  	return nil
   140  }
   141  
   142  func (rn rootNode) Lookup(ctx context.Context, name string) (_node fusefs.Node, err error) {
   143  	for _, t := range rn.fs.Client.Torrents() {
   144  		info := t.Info()
   145  		if t.Name() != name || info == nil {
   146  			continue
   147  		}
   148  		__node := node{
   149  			metadata: info,
   150  			FS:       rn.fs,
   151  			t:        t,
   152  		}
   153  		if !info.IsDir() {
   154  			_node = fileNode{__node, t.Files()[0]}
   155  		} else {
   156  			_node = dirNode{__node}
   157  		}
   158  		break
   159  	}
   160  	if _node == nil {
   161  		err = fuse.ENOENT
   162  	}
   163  	return
   164  }
   165  
   166  func (rn rootNode) ReadDirAll(ctx context.Context) (dirents []fuse.Dirent, err error) {
   167  	for _, t := range rn.fs.Client.Torrents() {
   168  		info := t.Info()
   169  		if info == nil {
   170  			continue
   171  		}
   172  		dirents = append(dirents, fuse.Dirent{
   173  			Name: info.BestName(),
   174  			Type: func() fuse.DirentType {
   175  				if !info.IsDir() {
   176  					return fuse.DT_File
   177  				} else {
   178  					return fuse.DT_Dir
   179  				}
   180  			}(),
   181  		})
   182  	}
   183  	return
   184  }
   185  
   186  func (rn rootNode) Attr(ctx context.Context, attr *fuse.Attr) error {
   187  	attr.Mode = os.ModeDir | defaultMode
   188  	return nil
   189  }
   190  
   191  // TODO(anacrolix): Why should rootNode implement this?
   192  func (rn rootNode) Forget() {
   193  	rn.fs.Destroy()
   194  }
   195  
   196  func (tfs *TorrentFS) Root() (fusefs.Node, error) {
   197  	return rootNode{tfs}, nil
   198  }
   199  
   200  func (tfs *TorrentFS) Destroy() {
   201  	tfs.mu.Lock()
   202  	select {
   203  	case <-tfs.destroyed:
   204  	default:
   205  		close(tfs.destroyed)
   206  	}
   207  	tfs.mu.Unlock()
   208  }
   209  
   210  func New(cl *torrent.Client) *TorrentFS {
   211  	fs := &TorrentFS{
   212  		Client:    cl,
   213  		destroyed: make(chan struct{}),
   214  	}
   215  	fs.event.L = &fs.mu
   216  	return fs
   217  }