]> Sergey Matveev's repositories - btrtrc.git/blob - cmd/torrentfs/main.go
Also run Go CI test job on 1.19
[btrtrc.git] / cmd / torrentfs / main.go
1 // Mounts a FUSE filesystem backed by torrents and magnet links.
2 package main
3
4 import (
5         "fmt"
6         "net"
7         "net/http"
8         _ "net/http/pprof"
9         "os"
10         "os/signal"
11         "os/user"
12         "path/filepath"
13         "syscall"
14         "time"
15
16         "github.com/anacrolix/envpprof"
17         _ "github.com/anacrolix/envpprof"
18         "github.com/anacrolix/fuse"
19         fusefs "github.com/anacrolix/fuse/fs"
20         "github.com/anacrolix/log"
21         "github.com/anacrolix/tagflag"
22
23         "github.com/anacrolix/torrent"
24         torrentfs "github.com/anacrolix/torrent/fs"
25         "github.com/anacrolix/torrent/util/dirwatch"
26 )
27
28 var args = struct {
29         MetainfoDir string `help:"torrent files in this location describe the contents of the mounted filesystem"`
30         DownloadDir string `help:"location to save torrent data"`
31         MountDir    string `help:"location the torrent contents are made available"`
32
33         DisableTrackers bool
34         TestPeer        *net.TCPAddr
35         ReadaheadBytes  tagflag.Bytes
36         ListenAddr      *net.TCPAddr
37 }{
38         MetainfoDir: func() string {
39                 _user, err := user.Current()
40                 if err != nil {
41                         panic(err)
42                 }
43                 return filepath.Join(_user.HomeDir, ".config/transmission/torrents")
44         }(),
45         ReadaheadBytes: 10 << 20,
46         ListenAddr:     &net.TCPAddr{},
47 }
48
49 func exitSignalHandlers(fs *torrentfs.TorrentFS) {
50         c := make(chan os.Signal, 1)
51         signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
52         for {
53                 <-c
54                 fs.Destroy()
55                 err := fuse.Unmount(args.MountDir)
56                 if err != nil {
57                         log.Print(err)
58                 }
59         }
60 }
61
62 func addTestPeer(client *torrent.Client) {
63         for _, t := range client.Torrents() {
64                 t.AddPeers([]torrent.PeerInfo{{
65                         Addr: args.TestPeer,
66                 }})
67         }
68 }
69
70 func main() {
71         defer envpprof.Stop()
72         err := mainErr()
73         if err != nil {
74                 log.Printf("error in main: %v", err)
75                 os.Exit(1)
76         }
77 }
78
79 func mainErr() error {
80         tagflag.Parse(&args)
81         if args.MountDir == "" {
82                 os.Stderr.WriteString("y u no specify mountpoint?\n")
83                 os.Exit(2)
84         }
85         conn, err := fuse.Mount(args.MountDir)
86         if err != nil {
87                 return fmt.Errorf("mounting: %w", err)
88         }
89         defer fuse.Unmount(args.MountDir)
90         // TODO: Think about the ramifications of exiting not due to a signal.
91         defer conn.Close()
92         cfg := torrent.NewDefaultClientConfig()
93         cfg.DataDir = args.DownloadDir
94         cfg.DisableTrackers = args.DisableTrackers
95         cfg.NoUpload = true // Ensure that downloads are responsive.
96         cfg.SetListenAddr(args.ListenAddr.String())
97         client, err := torrent.NewClient(cfg)
98         if err != nil {
99                 return fmt.Errorf("creating torrent client: %w", err)
100         }
101         // This is naturally exported via GOPPROF=http.
102         http.DefaultServeMux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
103                 client.WriteStatus(w)
104         })
105         dw, err := dirwatch.New(args.MetainfoDir)
106         if err != nil {
107                 return fmt.Errorf("watching torrent dir: %w", err)
108         }
109         dw.Logger = dw.Logger.FilterLevel(log.Info)
110         go func() {
111                 for ev := range dw.Events {
112                         switch ev.Change {
113                         case dirwatch.Added:
114                                 if ev.TorrentFilePath != "" {
115                                         _, err := client.AddTorrentFromFile(ev.TorrentFilePath)
116                                         if err != nil {
117                                                 log.Printf("error adding torrent from file %q to client: %v", ev.TorrentFilePath, err)
118                                         }
119                                 } else if ev.MagnetURI != "" {
120                                         _, err := client.AddMagnet(ev.MagnetURI)
121                                         if err != nil {
122                                                 log.Printf("error adding magnet: %s", err)
123                                         }
124                                 }
125                         case dirwatch.Removed:
126                                 T, ok := client.Torrent(ev.InfoHash)
127                                 if !ok {
128                                         break
129                                 }
130                                 T.Drop()
131                         }
132                 }
133         }()
134         fs := torrentfs.New(client)
135         go exitSignalHandlers(fs)
136
137         if args.TestPeer != nil {
138                 go func() {
139                         for {
140                                 addTestPeer(client)
141                                 time.Sleep(10 * time.Second)
142                         }
143                 }()
144         }
145
146         if err := fusefs.Serve(conn, fs); err != nil {
147                 return fmt.Errorf("serving fuse fs: %w", err)
148         }
149         <-conn.Ready
150         if err := conn.MountError; err != nil {
151                 return fmt.Errorf("mount error: %w", err)
152         }
153         return nil
154 }