package torrentfs import ( "context" "expvar" "os" "strings" "sync" "github.com/anacrolix/fuse" fusefs "github.com/anacrolix/fuse/fs" "github.com/anacrolix/torrent" "github.com/anacrolix/torrent/metainfo" ) const ( defaultMode = 0o555 ) var torrentfsReadRequests = expvar.NewInt("torrentfsReadRequests") type TorrentFS struct { Client *torrent.Client destroyed chan struct{} mu sync.Mutex blockedReads int event sync.Cond } var ( _ fusefs.FSDestroyer = &TorrentFS{} _ fusefs.NodeForgetter = rootNode{} _ fusefs.HandleReadDirAller = rootNode{} _ fusefs.HandleReadDirAller = dirNode{} ) // Is a directory node that lists all torrents and handles destruction of the // filesystem. type rootNode struct { fs *TorrentFS } type node struct { path string metadata *metainfo.Info FS *TorrentFS t *torrent.Torrent } type dirNode struct { node } var _ fusefs.HandleReadDirAller = dirNode{} func isSubPath(parent, child string) bool { if parent == "" { return len(child) > 0 } if !strings.HasPrefix(child, parent) { return false } extra := child[len(parent):] if extra == "" { return false } // Not just a file with more stuff on the end. return extra[0] == '/' } func (dn dirNode) ReadDirAll(ctx context.Context) (des []fuse.Dirent, err error) { names := map[string]bool{} for _, fi := range dn.metadata.Files { filePathname := strings.Join(fi.Path, "/") if !isSubPath(dn.path, filePathname) { continue } var name string if dn.path == "" { name = fi.Path[0] } else { dirPathname := strings.Split(dn.path, "/") name = fi.Path[len(dirPathname)] } if names[name] { continue } names[name] = true de := fuse.Dirent{ Name: name, } if len(fi.Path) == len(dn.path)+1 { de.Type = fuse.DT_File } else { de.Type = fuse.DT_Dir } des = append(des, de) } return } func (dn dirNode) Lookup(_ context.Context, name string) (fusefs.Node, error) { dir := false var file *torrent.File var fullPath string if dn.path != "" { fullPath = dn.path + "/" + name } else { fullPath = name } for _, f := range dn.t.Files() { if f.DisplayPath() == fullPath { file = f } if isSubPath(fullPath, f.DisplayPath()) { dir = true } } n := dn.node n.path = fullPath if dir && file != nil { panic("both dir and file") } if file != nil { return fileNode{n, file}, nil } if dir { return dirNode{n}, nil } return nil, fuse.ENOENT } func (dn dirNode) Attr(ctx context.Context, attr *fuse.Attr) error { attr.Mode = os.ModeDir | defaultMode return nil } func (rn rootNode) Lookup(ctx context.Context, name string) (_node fusefs.Node, err error) { for _, t := range rn.fs.Client.Torrents() { info := t.Info() if t.Name() != name || info == nil { continue } __node := node{ metadata: info, FS: rn.fs, t: t, } if !info.IsDir() { _node = fileNode{__node, t.Files()[0]} } else { _node = dirNode{__node} } break } if _node == nil { err = fuse.ENOENT } return } func (rn rootNode) ReadDirAll(ctx context.Context) (dirents []fuse.Dirent, err error) { for _, t := range rn.fs.Client.Torrents() { info := t.Info() if info == nil { continue } dirents = append(dirents, fuse.Dirent{ Name: info.Name, Type: func() fuse.DirentType { if !info.IsDir() { return fuse.DT_File } else { return fuse.DT_Dir } }(), }) } return } func (rn rootNode) Attr(ctx context.Context, attr *fuse.Attr) error { attr.Mode = os.ModeDir return nil } // TODO(anacrolix): Why should rootNode implement this? func (rn rootNode) Forget() { rn.fs.Destroy() } func (tfs *TorrentFS) Root() (fusefs.Node, error) { return rootNode{tfs}, nil } func (tfs *TorrentFS) Destroy() { tfs.mu.Lock() select { case <-tfs.destroyed: default: close(tfs.destroyed) } tfs.mu.Unlock() } func New(cl *torrent.Client) *TorrentFS { fs := &TorrentFS{ Client: cl, destroyed: make(chan struct{}), } fs.event.L = &fs.mu return fs }