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