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