github.com/anacrolix/torrent@v1.61.0/util/dirwatch/dirwatch.go (about) 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 "os" 8 "path/filepath" 9 "strings" 10 11 "github.com/anacrolix/log" 12 "github.com/anacrolix/missinggo/v2" 13 "github.com/fsnotify/fsnotify" 14 15 "github.com/anacrolix/torrent/metainfo" 16 ) 17 18 type Change uint 19 20 const ( 21 Added Change = iota 22 Removed 23 ) 24 25 type Event struct { 26 MagnetURI string 27 Change 28 TorrentFilePath string 29 InfoHash metainfo.Hash 30 } 31 32 type entity struct { 33 metainfo.Hash 34 MagnetURI string 35 TorrentFilePath string 36 } 37 38 type Instance struct { 39 w *fsnotify.Watcher 40 dirName string 41 Events chan Event 42 dirState map[metainfo.Hash]entity 43 Logger log.Logger 44 } 45 46 func (i *Instance) Close() { 47 i.w.Close() 48 } 49 50 func (i *Instance) handleEvents() { 51 defer close(i.Events) 52 for e := range i.w.Events { 53 i.Logger.WithDefaultLevel(log.Debug).Printf("event: %v", e) 54 if e.Op == fsnotify.Write { 55 // TODO: Special treatment as an existing torrent may have changed. 56 } else { 57 i.refresh() 58 } 59 } 60 } 61 62 func (i *Instance) handleErrors() { 63 for err := range i.w.Errors { 64 log.Printf("error in torrent directory watcher: %s", err) 65 } 66 } 67 68 func torrentFileInfoHash(fileName string) (ih metainfo.Hash, ok bool) { 69 mi, _ := metainfo.LoadFromFile(fileName) 70 if mi == nil { 71 return 72 } 73 ih = mi.HashInfoBytes() 74 ok = true 75 return 76 } 77 78 func scanDir(dirName string) (ee map[metainfo.Hash]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[metainfo.Hash]entity, len(names)) 91 addEntity := func(e entity) { 92 e0, ok := ee[e.Hash] 93 if ok { 94 if e0.MagnetURI == "" || len(e.MagnetURI) < len(e0.MagnetURI) { 95 return 96 } 97 } 98 ee[e.Hash] = 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 missinggo.CopyExact(&e.Hash, 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 := metainfo.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 Hash: 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 (i *Instance) torrentRemoved(ih metainfo.Hash) { 155 i.Events <- Event{ 156 InfoHash: ih, 157 Change: Removed, 158 } 159 } 160 161 func (i *Instance) torrentAdded(e entity) { 162 i.Events <- Event{ 163 InfoHash: e.Hash, 164 Change: Added, 165 MagnetURI: e.MagnetURI, 166 TorrentFilePath: e.TorrentFilePath, 167 } 168 } 169 170 func (i *Instance) refresh() { 171 _new := scanDir(i.dirName) 172 old := i.dirState 173 for ih := range old { 174 _, ok := _new[ih] 175 if !ok { 176 i.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 i.torrentRemoved(ih) 186 } 187 i.torrentAdded(newE) 188 } 189 i.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[metainfo.Hash]entity), 207 Logger: log.Default, 208 } 209 go func() { 210 i.refresh() 211 go i.handleEvents() 212 go i.handleErrors() 213 }() 214 return 215 }