16 "github.com/anacrolix/log"
17 "github.com/anacrolix/missinggo/v2"
18 "github.com/anacrolix/tagflag"
19 "github.com/anacrolix/torrent"
20 "github.com/anacrolix/torrent/iplist"
21 "github.com/anacrolix/torrent/metainfo"
22 pp "github.com/anacrolix/torrent/peer_protocol"
23 "github.com/anacrolix/torrent/storage"
24 "github.com/davecgh/go-spew/spew"
25 "github.com/dustin/go-humanize"
26 "golang.org/x/time/rate"
29 func torrentBar(t *torrent.Torrent, pieceStates bool) {
33 fmt.Printf("%v: getting torrent info for %q\n", time.Since(start), t.Name())
36 lastStats := t.Stats()
38 interval := 3 * time.Second
39 for range time.Tick(interval) {
40 var completedPieces, partialPieces int
41 psrs := t.PieceStateRuns()
42 for _, r := range psrs {
44 completedPieces += r.Length
47 partialPieces += r.Length
51 byteRate := int64(time.Second)
52 byteRate *= stats.BytesReadUsefulData.Int64() - lastStats.BytesReadUsefulData.Int64()
53 byteRate /= int64(interval)
55 "%v: downloading %q: %s/%s, %d/%d pieces completed (%d partial): %v/s\n",
58 humanize.Bytes(uint64(t.BytesCompleted())),
59 humanize.Bytes(uint64(t.Length())),
63 humanize.Bytes(uint64(byteRate)),
67 os.Stdout.WriteString(line)
77 type stringAddr string
79 func (stringAddr) Network() string { return "" }
80 func (me stringAddr) String() string { return string(me) }
82 func resolveTestPeers(addrs []string) (ret []torrent.PeerInfo) {
83 for _, ta := range addrs {
84 ret = append(ret, torrent.PeerInfo{
91 func addTorrents(client *torrent.Client, flags downloadFlags) error {
92 testPeers := resolveTestPeers(flags.TestPeer)
93 for _, arg := range flags.Torrent {
94 t, err := func() (*torrent.Torrent, error) {
95 if strings.HasPrefix(arg, "magnet:") {
96 t, err := client.AddMagnet(arg)
98 return nil, fmt.Errorf("error adding magnet: %w", err)
101 } else if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") {
102 response, err := http.Get(arg)
104 return nil, fmt.Errorf("Error downloading torrent file: %s", err)
107 metaInfo, err := metainfo.Load(response.Body)
108 defer response.Body.Close()
110 return nil, fmt.Errorf("error loading torrent file %q: %s\n", arg, err)
112 t, err := client.AddTorrent(metaInfo)
114 return nil, fmt.Errorf("adding torrent: %w", err)
117 } else if strings.HasPrefix(arg, "infohash:") {
118 t, _ := client.AddTorrentInfoHash(metainfo.NewHashFromHex(strings.TrimPrefix(arg, "infohash:")))
121 metaInfo, err := metainfo.LoadFromFile(arg)
123 return nil, fmt.Errorf("error loading torrent file %q: %s\n", arg, err)
125 t, err := client.AddTorrent(metaInfo)
127 return nil, fmt.Errorf("adding torrent: %w", err)
133 return fmt.Errorf("adding torrent for %q: %w", arg, err)
136 torrentBar(t, flags.PieceStates)
138 t.AddPeers(testPeers)
141 if len(flags.File) == 0 {
144 for _, f := range t.Files() {
145 for _, fileArg := range flags.File {
146 if f.DisplayPath() == fileArg {
157 type downloadFlags struct {
162 type DownloadCmd struct {
163 Mmap bool `help:"memory-map torrent data"`
164 TestPeer []string `help:"addresses of some starting peers"`
165 Seed bool `help:"seed after download is complete"`
166 Addr string `help:"network listen addr"`
167 MaxUnverifiedBytes tagflag.Bytes `help:"maximum number bytes to have pending verification"`
168 UploadRate *tagflag.Bytes `help:"max piece bytes to send per second"`
169 DownloadRate *tagflag.Bytes `help:"max bytes per second down from peers"`
170 PackedBlocklist string
172 Progress bool `default:"true"`
174 Quiet bool `help:"discard client logging"`
175 Stats bool `help:"print stats at termination"`
176 Dht bool `default:"true"`
178 TcpPeers bool `default:"true"`
179 UtpPeers bool `default:"true"`
180 Webtorrent bool `default:"true"`
182 // Don't progress past handshake for peer connections where the peer doesn't offer the fast
184 RequireFastExtension bool
186 Ipv4 bool `default:"true"`
187 Ipv6 bool `default:"true"`
188 Pex bool `default:"true"`
191 Torrent []string `arity:"+" help:"torrent file path or magnet uri" arg:"positional"`
194 func statsEnabled(flags downloadFlags) bool {
198 func exitSignalHandlers(notify *missinggo.SynchronizedEvent) {
199 c := make(chan os.Signal, 1)
200 signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
202 log.Printf("close signal received: %+v", <-c)
207 func downloadErr(flags downloadFlags) error {
208 clientConfig := torrent.NewDefaultClientConfig()
209 clientConfig.DisableWebseeds = flags.DisableWebseeds
210 clientConfig.DisableTCP = !flags.TcpPeers
211 clientConfig.DisableUTP = !flags.UtpPeers
212 clientConfig.DisableIPv4 = !flags.Ipv4
213 clientConfig.DisableIPv6 = !flags.Ipv6
214 clientConfig.DisableAcceptRateLimiting = true
215 clientConfig.NoDHT = !flags.Dht
216 clientConfig.Debug = flags.Debug
217 clientConfig.Seed = flags.Seed
218 clientConfig.PublicIp4 = flags.PublicIP
219 clientConfig.PublicIp6 = flags.PublicIP
220 clientConfig.DisablePEX = !flags.Pex
221 clientConfig.DisableWebtorrent = !flags.Webtorrent
222 if flags.PackedBlocklist != "" {
223 blocklist, err := iplist.MMapPackedFile(flags.PackedBlocklist)
225 return fmt.Errorf("loading blocklist: %v", err)
227 defer blocklist.Close()
228 clientConfig.IPBlocklist = blocklist
231 clientConfig.DefaultStorage = storage.NewMMap("")
233 if flags.Addr != "" {
234 clientConfig.SetListenAddr(flags.Addr)
236 if flags.UploadRate != nil {
237 clientConfig.UploadRateLimiter = rate.NewLimiter(rate.Limit(*flags.UploadRate), 256<<10)
239 if flags.DownloadRate != nil {
240 clientConfig.DownloadRateLimiter = rate.NewLimiter(rate.Limit(*flags.DownloadRate), 1<<20)
243 clientConfig.Logger = log.Discard
245 if flags.RequireFastExtension {
246 clientConfig.MinPeerExtensions.SetBit(pp.ExtensionBitFast, true)
248 clientConfig.MaxUnverifiedBytes = flags.MaxUnverifiedBytes.Int64()
250 var stop missinggo.SynchronizedEvent
255 client, err := torrent.NewClient(clientConfig)
257 return fmt.Errorf("creating client: %w", err)
259 var clientClose sync.Once // In certain situations, close was being called more than once.
260 defer clientClose.Do(func() { client.Close() })
261 go exitSignalHandlers(&stop)
264 clientClose.Do(func() { client.Close() })
267 // Write status on the root path on the default HTTP muxer. This will be bound to localhost
268 // somewhere if GOPPROF is set, thanks to the envpprof import.
269 http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
270 client.WriteStatus(w)
272 err = addTorrents(client, flags)
273 started := time.Now()
275 return fmt.Errorf("adding torrents: %w", err)
277 defer outputStats(client, flags)
278 if client.WaitAll() {
279 log.Print("downloaded ALL the torrents")
281 err = errors.New("y u no complete torrents?!")
283 clientConnStats := client.ConnStats()
284 log.Printf("average download rate: %v",
285 humanize.Bytes(uint64(
287 clientConnStats.BytesReadUsefulData.Int64(),
288 )*time.Second/time.Since(started),
291 if len(client.Torrents()) == 0 {
292 log.Print("no torrents to seed")
294 outputStats(client, flags)
298 spew.Dump(expvar.Get("torrent").(*expvar.Map).Get("chunks received"))
299 spew.Dump(client.ConnStats())
300 clStats := client.ConnStats()
301 sentOverhead := clStats.BytesWritten.Int64() - clStats.BytesWrittenData.Int64()
303 "client read %v, %.1f%% was useful data. sent %v non-data bytes",
304 humanize.Bytes(uint64(clStats.BytesRead.Int64())),
305 100*float64(clStats.BytesReadUsefulData.Int64())/float64(clStats.BytesRead.Int64()),
306 humanize.Bytes(uint64(sentOverhead)))
310 func outputStats(cl *torrent.Client, args downloadFlags) {
311 if !statsEnabled(args) {
314 expvar.Do(func(kv expvar.KeyValue) {
315 fmt.Printf("%s: %s\n", kv.Key, kv.Value)
317 cl.WriteStatus(os.Stdout)