]> Sergey Matveev's repositories - btrtrc.git/blob - util/dirwatch/dirwatch.go
Clean up magnet URI parsing errors in dirwatch
[btrtrc.git] / util / dirwatch / dirwatch.go
1 // Package dirwatch provides filesystem-notification based tracking of torrent
2 // info files and magnet URIs in a directory.
3 package dirwatch
4
5 import (
6         "bufio"
7         "log"
8         "os"
9         "path/filepath"
10         "strings"
11
12         "bitbucket.org/anacrolix/go.torrent/util"
13
14         "bitbucket.org/anacrolix/go.torrent"
15         "github.com/anacrolix/libtorgo/metainfo"
16         "github.com/go-fsnotify/fsnotify"
17 )
18
19 type Change uint
20
21 const (
22         Added Change = iota
23         Removed
24 )
25
26 type Event struct {
27         MagnetURI string
28         Change
29         TorrentFilePath string
30         InfoHash        torrent.InfoHash
31 }
32
33 type entity struct {
34         torrent.InfoHash
35         MagnetURI       string
36         TorrentFilePath string
37 }
38
39 type Instance struct {
40         w        *fsnotify.Watcher
41         dirName  string
42         Events   chan Event
43         dirState map[torrent.InfoHash]entity
44 }
45
46 func (me *Instance) Close() {
47         me.w.Close()
48 }
49
50 func (me *Instance) handleEvents() {
51         defer close(me.Events)
52         for e := range me.w.Events {
53                 log.Printf("event: %s", e)
54                 if e.Op == fsnotify.Write {
55                         // TODO: Special treatment as an existing torrent may have changed.
56                 } else {
57                         me.refresh()
58                 }
59         }
60 }
61
62 func (me *Instance) handleErrors() {
63         for err := range me.w.Errors {
64                 log.Printf("error in torrent directory watcher: %s", err)
65         }
66 }
67
68 func torrentFileInfoHash(fileName string) (ih torrent.InfoHash, ok bool) {
69         mi, _ := metainfo.LoadFromFile(fileName)
70         if mi == nil {
71                 return
72         }
73         util.CopyExact(ih[:], mi.Info.Hash)
74         ok = true
75         return
76 }
77
78 func scanDir(dirName string) (ee map[torrent.InfoHash]entity) {
79         d, err := os.Open(dirName)
80         if err != nil {
81                 log.Print(err)
82                 return
83         }
84         defer d.Close()
85         names, err := d.Readdirnames(-1)
86         if err != nil {
87                 log.Print(err)
88                 return
89         }
90         ee = make(map[torrent.InfoHash]entity, len(names))
91         addEntity := func(e entity) {
92                 e0, ok := ee[e.InfoHash]
93                 if ok {
94                         if e0.MagnetURI == "" || len(e.MagnetURI) < len(e0.MagnetURI) {
95                                 return
96                         }
97                 }
98                 ee[e.InfoHash] = e
99         }
100         for _, n := range names {
101                 fullName := filepath.Join(dirName, n)
102                 switch filepath.Ext(n) {
103                 case ".torrent":
104                         ih, ok := torrentFileInfoHash(fullName)
105                         if !ok {
106                                 break
107                         }
108                         e := entity{
109                                 TorrentFilePath: fullName,
110                         }
111                         util.CopyExact(&e.InfoHash, ih)
112                         addEntity(e)
113                 case ".magnet":
114                         uris, err := magnetFileURIs(fullName)
115                         if err != nil {
116                                 log.Print(err)
117                                 break
118                         }
119                         for _, uri := range uris {
120                                 m, err := torrent.ParseMagnetURI(uri)
121                                 if err != nil {
122                                         log.Printf("error parsing %q in file %q: %s", uri, fullName, err)
123                                         continue
124                                 }
125                                 addEntity(entity{
126                                         InfoHash:  m.InfoHash,
127                                         MagnetURI: uri,
128                                 })
129                         }
130                 }
131         }
132         return
133 }
134
135 func magnetFileURIs(name string) (uris []string, err error) {
136         f, err := os.Open(name)
137         if err != nil {
138                 return
139         }
140         defer f.Close()
141         scanner := bufio.NewScanner(f)
142         scanner.Split(bufio.ScanWords)
143         for scanner.Scan() {
144                 // Allow magnet URIs to be "commented" out.
145                 if strings.HasPrefix(scanner.Text(), "#") {
146                         continue
147                 }
148                 uris = append(uris, scanner.Text())
149         }
150         err = scanner.Err()
151         return
152 }
153
154 func (me *Instance) torrentRemoved(ih torrent.InfoHash) {
155         me.Events <- Event{
156                 InfoHash: ih,
157                 Change:   Removed,
158         }
159 }
160
161 func (me *Instance) torrentAdded(e entity) {
162         me.Events <- Event{
163                 InfoHash:        e.InfoHash,
164                 Change:          Added,
165                 MagnetURI:       e.MagnetURI,
166                 TorrentFilePath: e.TorrentFilePath,
167         }
168 }
169
170 func (me *Instance) refresh() {
171         _new := scanDir(me.dirName)
172         old := me.dirState
173         for ih, _ := range old {
174                 _, ok := _new[ih]
175                 if !ok {
176                         me.torrentRemoved(ih)
177                 }
178         }
179         for ih, newE := range _new {
180                 oldE, ok := old[ih]
181                 if ok {
182                         if newE == oldE {
183                                 continue
184                         }
185                         me.torrentRemoved(ih)
186                 }
187                 me.torrentAdded(newE)
188         }
189         me.dirState = _new
190 }
191
192 func New(dirName string) (i *Instance, err error) {
193         w, err := fsnotify.NewWatcher()
194         if err != nil {
195                 return
196         }
197         err = w.Add(dirName)
198         if err != nil {
199                 w.Close()
200                 return
201         }
202         i = &Instance{
203                 w:        w,
204                 dirName:  dirName,
205                 Events:   make(chan Event),
206                 dirState: make(map[torrent.InfoHash]entity, 0),
207         }
208         go func() {
209                 i.refresh()
210                 go i.handleEvents()
211                 go i.handleErrors()
212         }()
213         return
214 }