// Mounts a FUSE filesystem backed by torrents and magnet links. package main import ( "fmt" "net" "net/http" _ "net/http/pprof" "os" "os/signal" "os/user" "path/filepath" "syscall" "time" "github.com/anacrolix/envpprof" _ "github.com/anacrolix/envpprof" "github.com/anacrolix/fuse" fusefs "github.com/anacrolix/fuse/fs" "github.com/anacrolix/log" "github.com/anacrolix/tagflag" "github.com/anacrolix/torrent" torrentfs "github.com/anacrolix/torrent/fs" "github.com/anacrolix/torrent/util/dirwatch" ) var args = struct { MetainfoDir string `help:"torrent files in this location describe the contents of the mounted filesystem"` DownloadDir string `help:"location to save torrent data"` MountDir string `help:"location the torrent contents are made available"` DisableTrackers bool TestPeer *net.TCPAddr ReadaheadBytes tagflag.Bytes ListenAddr *net.TCPAddr }{ MetainfoDir: func() string { _user, err := user.Current() if err != nil { panic(err) } return filepath.Join(_user.HomeDir, ".config/transmission/torrents") }(), ReadaheadBytes: 10 << 20, ListenAddr: &net.TCPAddr{}, } func exitSignalHandlers(fs *torrentfs.TorrentFS) { c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) for { <-c fs.Destroy() err := fuse.Unmount(args.MountDir) if err != nil { log.Print(err) } } } func addTestPeer(client *torrent.Client) { for _, t := range client.Torrents() { t.AddPeers([]torrent.PeerInfo{{ Addr: args.TestPeer, }}) } } func main() { defer envpprof.Stop() err := mainErr() if err != nil { log.Printf("error in main: %v", err) os.Exit(1) } } func mainErr() error { tagflag.Parse(&args) if args.MountDir == "" { os.Stderr.WriteString("y u no specify mountpoint?\n") os.Exit(2) } conn, err := fuse.Mount(args.MountDir) if err != nil { return fmt.Errorf("mounting: %w", err) } defer fuse.Unmount(args.MountDir) // TODO: Think about the ramifications of exiting not due to a signal. defer conn.Close() cfg := torrent.NewDefaultClientConfig() cfg.DataDir = args.DownloadDir cfg.DisableTrackers = args.DisableTrackers cfg.NoUpload = true // Ensure that downloads are responsive. cfg.SetListenAddr(args.ListenAddr.String()) client, err := torrent.NewClient(cfg) if err != nil { return fmt.Errorf("creating torrent client: %w", err) } // This is naturally exported via GOPPROF=http. http.DefaultServeMux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { client.WriteStatus(w) }) dw, err := dirwatch.New(args.MetainfoDir) if err != nil { return fmt.Errorf("watching torrent dir: %w", err) } dw.Logger = dw.Logger.FilterLevel(log.Info) go func() { for ev := range dw.Events { switch ev.Change { case dirwatch.Added: if ev.TorrentFilePath != "" { _, err := client.AddTorrentFromFile(ev.TorrentFilePath) if err != nil { log.Printf("error adding torrent from file %q to client: %v", ev.TorrentFilePath, err) } } else if ev.MagnetURI != "" { _, err := client.AddMagnet(ev.MagnetURI) if err != nil { log.Printf("error adding magnet: %s", err) } } case dirwatch.Removed: T, ok := client.Torrent(ev.InfoHash) if !ok { break } T.Drop() } } }() fs := torrentfs.New(client) go exitSignalHandlers(fs) if args.TestPeer != nil { go func() { for { addTestPeer(client) time.Sleep(10 * time.Second) } }() } if err := fusefs.Serve(conn, fs); err != nil { return fmt.Errorf("serving fuse fs: %w", err) } <-conn.Ready if err := conn.MountError; err != nil { return fmt.Errorf("mount error: %w", err) } return nil }