// Package dirwatch provides filesystem-notification based tracking of torrent // info files and magnet URIs in a directory. package dirwatch import ( "bufio" "os" "path/filepath" "strings" "github.com/anacrolix/log" "github.com/anacrolix/missinggo/v2" "github.com/anacrolix/torrent/metainfo" "github.com/fsnotify/fsnotify" ) type Change uint const ( Added Change = iota Removed ) type Event struct { MagnetURI string Change TorrentFilePath string InfoHash metainfo.Hash } type entity struct { metainfo.Hash MagnetURI string TorrentFilePath string } type Instance struct { w *fsnotify.Watcher dirName string Events chan Event dirState map[metainfo.Hash]entity Logger log.Logger } func (i *Instance) Close() { i.w.Close() } func (i *Instance) handleEvents() { defer close(i.Events) for e := range i.w.Events { i.Logger.WithDefaultLevel(log.Debug).Printf("event: %v", e) if e.Op == fsnotify.Write { // TODO: Special treatment as an existing torrent may have changed. } else { i.refresh() } } } func (i *Instance) handleErrors() { for err := range i.w.Errors { log.Printf("error in torrent directory watcher: %s", err) } } func torrentFileInfoHash(fileName string) (ih metainfo.Hash, ok bool) { mi, _ := metainfo.LoadFromFile(fileName) if mi == nil { return } ih = mi.HashInfoBytes() ok = true return } func scanDir(dirName string) (ee map[metainfo.Hash]entity) { d, err := os.Open(dirName) if err != nil { log.Print(err) return } defer d.Close() names, err := d.Readdirnames(-1) if err != nil { log.Print(err) return } ee = make(map[metainfo.Hash]entity, len(names)) addEntity := func(e entity) { e0, ok := ee[e.Hash] if ok { if e0.MagnetURI == "" || len(e.MagnetURI) < len(e0.MagnetURI) { return } } ee[e.Hash] = e } for _, n := range names { fullName := filepath.Join(dirName, n) switch filepath.Ext(n) { case ".torrent": ih, ok := torrentFileInfoHash(fullName) if !ok { break } e := entity{ TorrentFilePath: fullName, } missinggo.CopyExact(&e.Hash, ih) addEntity(e) case ".magnet": uris, err := magnetFileURIs(fullName) if err != nil { log.Print(err) break } for _, uri := range uris { m, err := metainfo.ParseMagnetUri(uri) if err != nil { log.Printf("error parsing %q in file %q: %s", uri, fullName, err) continue } addEntity(entity{ Hash: m.InfoHash, MagnetURI: uri, }) } } } return } func magnetFileURIs(name string) (uris []string, err error) { f, err := os.Open(name) if err != nil { return } defer f.Close() scanner := bufio.NewScanner(f) scanner.Split(bufio.ScanWords) for scanner.Scan() { // Allow magnet URIs to be "commented" out. if strings.HasPrefix(scanner.Text(), "#") { continue } uris = append(uris, scanner.Text()) } err = scanner.Err() return } func (i *Instance) torrentRemoved(ih metainfo.Hash) { i.Events <- Event{ InfoHash: ih, Change: Removed, } } func (i *Instance) torrentAdded(e entity) { i.Events <- Event{ InfoHash: e.Hash, Change: Added, MagnetURI: e.MagnetURI, TorrentFilePath: e.TorrentFilePath, } } func (i *Instance) refresh() { _new := scanDir(i.dirName) old := i.dirState for ih := range old { _, ok := _new[ih] if !ok { i.torrentRemoved(ih) } } for ih, newE := range _new { oldE, ok := old[ih] if ok { if newE == oldE { continue } i.torrentRemoved(ih) } i.torrentAdded(newE) } i.dirState = _new } func New(dirName string) (i *Instance, err error) { w, err := fsnotify.NewWatcher() if err != nil { return } err = w.Add(dirName) if err != nil { w.Close() return } i = &Instance{ w: w, dirName: dirName, Events: make(chan Event), dirState: make(map[metainfo.Hash]entity), Logger: log.Default, } go func() { i.refresh() go i.handleEvents() go i.handleErrors() }() return }