]> Sergey Matveev's repositories - btrtrc.git/blob - cmd/torrent/main.go
cmd/torrent: Use anacrolix/args and merge several other cmds in as subcommands
[btrtrc.git] / cmd / torrent / main.go
1 // Downloads torrents from the command-line.
2 package main
3
4 import (
5         "errors"
6         "expvar"
7         "fmt"
8         "io"
9         stdLog "log"
10         "net"
11         "net/http"
12         "os"
13         "os/signal"
14         "strings"
15         "sync"
16         "syscall"
17         "time"
18
19         "github.com/anacrolix/args"
20         "github.com/anacrolix/envpprof"
21         "github.com/anacrolix/log"
22         "github.com/anacrolix/missinggo/v2"
23         "github.com/anacrolix/tagflag"
24         "github.com/anacrolix/torrent/bencode"
25         "github.com/anacrolix/torrent/version"
26         "github.com/davecgh/go-spew/spew"
27         "github.com/dustin/go-humanize"
28         "golang.org/x/time/rate"
29
30         "github.com/anacrolix/torrent"
31         "github.com/anacrolix/torrent/iplist"
32         "github.com/anacrolix/torrent/metainfo"
33         "github.com/anacrolix/torrent/storage"
34 )
35
36 func torrentBar(t *torrent.Torrent, pieceStates bool) {
37         go func() {
38                 start := time.Now()
39                 if t.Info() == nil {
40                         fmt.Printf("%v: getting torrent info for %q\n", time.Since(start), t.Name())
41                         <-t.GotInfo()
42                 }
43                 lastStats := t.Stats()
44                 var lastLine string
45                 for range time.Tick(time.Second) {
46                         var completedPieces, partialPieces int
47                         psrs := t.PieceStateRuns()
48                         for _, r := range psrs {
49                                 if r.Complete {
50                                         completedPieces += r.Length
51                                 }
52                                 if r.Partial {
53                                         partialPieces += r.Length
54                                 }
55                         }
56                         stats := t.Stats()
57                         line := fmt.Sprintf(
58                                 "%v: downloading %q: %s/%s, %d/%d pieces completed (%d partial): %v/s\n",
59                                 time.Since(start),
60                                 t.Name(),
61                                 humanize.Bytes(uint64(t.BytesCompleted())),
62                                 humanize.Bytes(uint64(t.Length())),
63                                 completedPieces,
64                                 t.NumPieces(),
65                                 partialPieces,
66                                 humanize.Bytes(uint64(stats.BytesReadUsefulData.Int64()-lastStats.BytesReadUsefulData.Int64())),
67                         )
68                         if line != lastLine {
69                                 lastLine = line
70                                 os.Stdout.WriteString(line)
71                         }
72                         if pieceStates {
73                                 fmt.Println(psrs)
74                         }
75                         lastStats = stats
76                 }
77         }()
78 }
79
80 type stringAddr string
81
82 func (stringAddr) Network() string   { return "" }
83 func (me stringAddr) String() string { return string(me) }
84
85 func resolveTestPeers(addrs []string) (ret []torrent.PeerInfo) {
86         for _, ta := range addrs {
87                 ret = append(ret, torrent.PeerInfo{
88                         Addr: stringAddr(ta),
89                 })
90         }
91         return
92 }
93
94 func addTorrents(client *torrent.Client, flags downloadFlags) error {
95         testPeers := resolveTestPeers(flags.TestPeer)
96         for _, arg := range flags.Torrent {
97                 t, err := func() (*torrent.Torrent, error) {
98                         if strings.HasPrefix(arg, "magnet:") {
99                                 t, err := client.AddMagnet(arg)
100                                 if err != nil {
101                                         return nil, fmt.Errorf("error adding magnet: %w", err)
102                                 }
103                                 return t, nil
104                         } else if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") {
105                                 response, err := http.Get(arg)
106                                 if err != nil {
107                                         return nil, fmt.Errorf("Error downloading torrent file: %s", err)
108                                 }
109
110                                 metaInfo, err := metainfo.Load(response.Body)
111                                 defer response.Body.Close()
112                                 if err != nil {
113                                         return nil, fmt.Errorf("error loading torrent file %q: %s\n", arg, err)
114                                 }
115                                 t, err := client.AddTorrent(metaInfo)
116                                 if err != nil {
117                                         return nil, fmt.Errorf("adding torrent: %w", err)
118                                 }
119                                 return t, nil
120                         } else if strings.HasPrefix(arg, "infohash:") {
121                                 t, _ := client.AddTorrentInfoHash(metainfo.NewHashFromHex(strings.TrimPrefix(arg, "infohash:")))
122                                 return t, nil
123                         } else {
124                                 metaInfo, err := metainfo.LoadFromFile(arg)
125                                 if err != nil {
126                                         return nil, fmt.Errorf("error loading torrent file %q: %s\n", arg, err)
127                                 }
128                                 t, err := client.AddTorrent(metaInfo)
129                                 if err != nil {
130                                         return nil, fmt.Errorf("adding torrent: %w", err)
131                                 }
132                                 return t, nil
133                         }
134                 }()
135                 if err != nil {
136                         return fmt.Errorf("adding torrent for %q: %w", arg, err)
137                 }
138                 if flags.Progress {
139                         torrentBar(t, flags.PieceStates)
140                 }
141                 t.AddPeers(testPeers)
142                 go func() {
143                         <-t.GotInfo()
144                         if len(flags.File) == 0 {
145                                 t.DownloadAll()
146                         } else {
147                                 for _, f := range t.Files() {
148                                         for _, fileArg := range flags.File {
149                                                 if f.DisplayPath() == fileArg {
150                                                         f.Download()
151                                                 }
152                                         }
153                                 }
154                         }
155                 }()
156         }
157         return nil
158 }
159
160 type downloadFlags struct {
161         Debug bool
162         DownloadCmd
163 }
164
165 type DownloadCmd struct {
166         Mmap               bool           `help:"memory-map torrent data"`
167         TestPeer           []string       `help:"addresses of some starting peers"`
168         Seed               bool           `help:"seed after download is complete"`
169         Addr               string         `help:"network listen addr"`
170         MaxUnverifiedBytes tagflag.Bytes  `help:"maximum number bytes to have pending verification"`
171         UploadRate         *tagflag.Bytes `help:"max piece bytes to send per second"`
172         DownloadRate       *tagflag.Bytes `help:"max bytes per second down from peers"`
173         PackedBlocklist    string
174         PublicIP           net.IP
175         Progress           bool `default:"true"`
176         PieceStates        bool
177         Quiet              bool  `help:"discard client logging"`
178         Stats              *bool `help:"print stats at termination"`
179         Dht                bool  `default:"true"`
180
181         TcpPeers        bool `default:"true"`
182         UtpPeers        bool `default:"true"`
183         Webtorrent      bool `default:"true"`
184         DisableWebseeds bool
185
186         Ipv4 bool `default:"true"`
187         Ipv6 bool `default:"true"`
188         Pex  bool `default:"true"`
189
190         File    []string
191         Torrent []string `arity:"+" help:"torrent file path or magnet uri" arg:"positional"`
192 }
193
194 func stdoutAndStderrAreSameFile() bool {
195         fi1, _ := os.Stdout.Stat()
196         fi2, _ := os.Stderr.Stat()
197         return os.SameFile(fi1, fi2)
198 }
199
200 func statsEnabled(flags downloadFlags) bool {
201         if flags.Stats == nil {
202                 return flags.Debug
203         }
204         return *flags.Stats
205 }
206
207 func exitSignalHandlers(notify *missinggo.SynchronizedEvent) {
208         c := make(chan os.Signal, 1)
209         signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
210         for {
211                 log.Printf("close signal received: %+v", <-c)
212                 notify.Set()
213         }
214 }
215
216 func main() {
217         defer envpprof.Stop()
218         if err := mainErr(); err != nil {
219                 log.Printf("error in main: %v", err)
220                 os.Exit(1)
221         }
222 }
223
224 func mainErr() error {
225         stdLog.SetFlags(stdLog.Flags() | stdLog.Lshortfile)
226         debug := args.Flag(args.FlagOpt{Long: "debug"})
227         p := args.ParseMain(
228                 debug,
229                 args.Subcommand("metainfo", metainfoCmd),
230                 args.Subcommand("announce", func(p args.SubCmdCtx) error {
231                         var cmd AnnounceCmd
232                         err := p.NewParser().AddParams(
233                                 args.Pos("tracker", &cmd.Tracker),
234                                 args.Pos("infohash", &cmd.InfoHash)).Parse()
235                         if err != nil {
236                                 return err
237                         }
238                         return announceErr(cmd)
239                 }),
240                 args.Subcommand("download", func(p args.SubCmdCtx) error {
241                         var dlf DownloadCmd
242                         err := p.NewParser().AddParams(
243                                 append(args.FromStruct(&dlf), debug)...,
244                         ).Parse()
245                         if err != nil {
246                                 return err
247                         }
248                         return downloadErr(downloadFlags{
249                                 Debug:       debug.Bool(),
250                                 DownloadCmd: dlf,
251                         })
252                 }),
253                 args.Subcommand(
254                         "spew-bencoding",
255                         func(p args.SubCmdCtx) error {
256                                 d := bencode.NewDecoder(os.Stdin)
257                                 for i := 0; ; i++ {
258                                         var v interface{}
259                                         err := d.Decode(&v)
260                                         if err == io.EOF {
261                                                 break
262                                         }
263                                         if err != nil {
264                                                 return fmt.Errorf("decoding message index %d: %w", i, err)
265                                         }
266                                         spew.Dump(v)
267                                 }
268                                 return nil
269                         },
270                         args.Help("reads bencoding from stdin into Go native types and spews the result"),
271                 ),
272                 args.Subcommand("version", func(p args.SubCmdCtx) error {
273                         fmt.Printf("HTTP User-Agent: %q\n", version.DefaultHttpUserAgent)
274                         fmt.Printf("Torrent client version: %q\n", version.DefaultExtendedHandshakeClientVersion)
275                         fmt.Printf("Torrent version prefix: %q\n", version.DefaultBep20Prefix)
276                         return nil
277                 }),
278         )
279         if p.Err != nil {
280                 if errors.Is(p.Err, args.ErrHelped) {
281                         return nil
282                 }
283                 return p.Err
284         }
285         if !p.RanSubCmd {
286                 p.PrintChoices(os.Stderr)
287                 args.FatalUsage()
288         }
289         return nil
290 }
291
292 func downloadErr(flags downloadFlags) error {
293         clientConfig := torrent.NewDefaultClientConfig()
294         clientConfig.DisableWebseeds = flags.DisableWebseeds
295         clientConfig.DisableTCP = !flags.TcpPeers
296         clientConfig.DisableUTP = !flags.UtpPeers
297         clientConfig.DisableIPv4 = !flags.Ipv4
298         clientConfig.DisableIPv6 = !flags.Ipv6
299         clientConfig.DisableAcceptRateLimiting = true
300         clientConfig.NoDHT = !flags.Dht
301         clientConfig.Debug = flags.Debug
302         clientConfig.Seed = flags.Seed
303         clientConfig.PublicIp4 = flags.PublicIP
304         clientConfig.PublicIp6 = flags.PublicIP
305         clientConfig.DisablePEX = !flags.Pex
306         clientConfig.DisableWebtorrent = !flags.Webtorrent
307         if flags.PackedBlocklist != "" {
308                 blocklist, err := iplist.MMapPackedFile(flags.PackedBlocklist)
309                 if err != nil {
310                         return fmt.Errorf("loading blocklist: %v", err)
311                 }
312                 defer blocklist.Close()
313                 clientConfig.IPBlocklist = blocklist
314         }
315         if flags.Mmap {
316                 clientConfig.DefaultStorage = storage.NewMMap("")
317         }
318         if flags.Addr != "" {
319                 clientConfig.SetListenAddr(flags.Addr)
320         }
321         if flags.UploadRate != nil {
322                 clientConfig.UploadRateLimiter = rate.NewLimiter(rate.Limit(*flags.UploadRate), 256<<10)
323         }
324         if flags.DownloadRate != nil {
325                 clientConfig.DownloadRateLimiter = rate.NewLimiter(rate.Limit(*flags.DownloadRate), 1<<20)
326         }
327         if flags.Quiet {
328                 clientConfig.Logger = log.Discard
329         }
330         clientConfig.MaxUnverifiedBytes = flags.MaxUnverifiedBytes.Int64()
331
332         var stop missinggo.SynchronizedEvent
333         defer func() {
334                 stop.Set()
335         }()
336
337         client, err := torrent.NewClient(clientConfig)
338         if err != nil {
339                 return fmt.Errorf("creating client: %w", err)
340         }
341         var clientClose sync.Once //In certain situations, close was being called more than once.
342         defer clientClose.Do(client.Close)
343         go exitSignalHandlers(&stop)
344         go func() {
345                 <-stop.C()
346                 clientClose.Do(client.Close)
347         }()
348
349         // Write status on the root path on the default HTTP muxer. This will be bound to localhost
350         // somewhere if GOPPROF is set, thanks to the envpprof import.
351         http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
352                 client.WriteStatus(w)
353         })
354         err = addTorrents(client, flags)
355         if err != nil {
356                 return fmt.Errorf("adding torrents: %w", err)
357         }
358         defer outputStats(client, flags)
359         if client.WaitAll() {
360                 log.Print("downloaded ALL the torrents")
361         } else {
362                 return errors.New("y u no complete torrents?!")
363         }
364         if flags.Seed {
365                 if len(client.Torrents()) == 0 {
366                         log.Print("no torrents to seed")
367                 } else {
368                         outputStats(client, flags)
369                         <-stop.C()
370                 }
371         }
372         return nil
373 }
374
375 func outputStats(cl *torrent.Client, args downloadFlags) {
376         if !statsEnabled(args) {
377                 return
378         }
379         expvar.Do(func(kv expvar.KeyValue) {
380                 fmt.Printf("%s: %s\n", kv.Key, kv.Value)
381         })
382         cl.WriteStatus(os.Stdout)
383 }