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