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