17 "github.com/anacrolix/log"
18 "github.com/anacrolix/tagflag"
19 "github.com/davecgh/go-spew/spew"
20 "github.com/dustin/go-humanize"
21 "golang.org/x/time/rate"
23 "github.com/anacrolix/torrent"
24 "github.com/anacrolix/torrent/iplist"
25 "github.com/anacrolix/torrent/metainfo"
26 pp "github.com/anacrolix/torrent/peer_protocol"
27 "github.com/anacrolix/torrent/storage"
30 func torrentBar(t *torrent.Torrent, pieceStates bool) {
34 fmt.Printf("%v: getting torrent info for %q\n", time.Since(start), t.Name())
37 lastStats := t.Stats()
39 interval := 3 * time.Second
40 for range time.Tick(interval) {
41 var completedPieces, partialPieces int
42 psrs := t.PieceStateRuns()
43 for _, r := range psrs {
45 completedPieces += r.Length
48 partialPieces += r.Length
52 byteRate := int64(time.Second)
53 byteRate *= stats.BytesReadUsefulData.Int64() - lastStats.BytesReadUsefulData.Int64()
54 byteRate /= int64(interval)
56 "%v: downloading %q: %s/%s, %d/%d pieces completed (%d partial): %v/s\n",
59 humanize.Bytes(uint64(t.BytesCompleted())),
60 humanize.Bytes(uint64(t.Length())),
64 humanize.Bytes(uint64(byteRate)),
68 os.Stdout.WriteString(line)
78 type stringAddr string
80 func (stringAddr) Network() string { return "" }
81 func (me stringAddr) String() string { return string(me) }
83 func resolveTestPeers(addrs []string) (ret []torrent.PeerInfo) {
84 for _, ta := range addrs {
85 ret = append(ret, torrent.PeerInfo{
92 func addTorrents(ctx context.Context, client *torrent.Client, flags downloadFlags, wg *sync.WaitGroup) error {
93 testPeers := resolveTestPeers(flags.TestPeer)
94 for _, arg := range flags.Torrent {
95 t, err := func() (*torrent.Torrent, error) {
96 if strings.HasPrefix(arg, "magnet:") {
97 t, err := client.AddMagnet(arg)
99 return nil, fmt.Errorf("error adding magnet: %w", err)
102 } else if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") {
103 response, err := http.Get(arg)
105 return nil, fmt.Errorf("Error downloading torrent file: %s", err)
108 metaInfo, err := metainfo.Load(response.Body)
109 defer response.Body.Close()
111 return nil, fmt.Errorf("error loading torrent file %q: %s\n", arg, err)
113 t, err := client.AddTorrent(metaInfo)
115 return nil, fmt.Errorf("adding torrent: %w", err)
118 } else if strings.HasPrefix(arg, "infohash:") {
119 t, _ := client.AddTorrentInfoHash(metainfo.NewHashFromHex(strings.TrimPrefix(arg, "infohash:")))
122 metaInfo, err := metainfo.LoadFromFile(arg)
124 return nil, fmt.Errorf("error loading torrent file %q: %s\n", arg, err)
126 t, err := client.AddTorrent(metaInfo)
128 return nil, fmt.Errorf("adding torrent: %w", err)
134 return fmt.Errorf("adding torrent for %q: %w", arg, err)
137 torrentBar(t, flags.PieceStates)
139 t.AddPeers(testPeers)
148 if flags.SaveMetainfos {
149 path := fmt.Sprintf("%v.torrent", t.InfoHash().HexString())
150 err := writeMetainfoToFile(t.Metainfo(), path)
152 log.Printf("wrote %q", path)
154 log.Printf("error writing %q: %v", path, err)
157 if len(flags.File) == 0 {
162 waitForPieces(ctx, t, 0, t.NumPieces())
164 if flags.LinearDiscard {
166 io.Copy(io.Discard, r)
170 for _, f := range t.Files() {
171 for _, fileArg := range flags.File {
172 if f.DisplayPath() == fileArg {
176 waitForPieces(ctx, t, f.BeginPieceIndex(), f.EndPieceIndex())
179 if flags.LinearDiscard {
183 io.Copy(io.Discard, r)
195 func waitForPieces(ctx context.Context, t *torrent.Torrent, beginIndex, endIndex int) {
196 sub := t.SubscribePieceStateChanges()
198 expected := storage.Completion{
202 pending := make(map[int]struct{})
203 for i := beginIndex; i < endIndex; i++ {
204 if t.Piece(i).State().Completion != expected {
205 pending[i] = struct{}{}
209 if len(pending) == 0 {
213 case ev := <-sub.Values:
214 if ev.Completion == expected {
215 delete(pending, ev.Index)
223 func writeMetainfoToFile(mi metainfo.MetaInfo, path string) error {
224 f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o640)
236 type downloadFlags struct {
241 type DownloadCmd struct {
243 Mmap bool `help:"memory-map torrent data"`
244 Seed bool `help:"seed after download is complete"`
245 Addr string `help:"network listen addr"`
246 MaxUnverifiedBytes *tagflag.Bytes `help:"maximum number bytes to have pending verification"`
247 UploadRate *tagflag.Bytes `help:"max piece bytes to send per second"`
248 DownloadRate *tagflag.Bytes `help:"max bytes per second down from peers"`
249 PackedBlocklist string
251 Progress bool `default:"true"`
252 PieceStates bool `help:"Output piece state runs at progress intervals."`
253 Quiet bool `help:"discard client logging"`
254 Stats bool `help:"print stats at termination"`
255 Dht bool `default:"true"`
256 PortForward bool `default:"true"`
258 TcpPeers bool `default:"true"`
259 UtpPeers bool `default:"true"`
260 Webtorrent bool `default:"true"`
262 // Don't progress past handshake for peer connections where the peer doesn't offer the fast
264 RequireFastExtension bool
266 Ipv4 bool `default:"true"`
267 Ipv6 bool `default:"true"`
268 Pex bool `default:"true"`
270 LinearDiscard bool `help:"Read and discard selected regions from start to finish. Useful for testing simultaneous Reader and static file prioritization."`
271 TestPeer []string `help:"addresses of some starting peers"`
274 Torrent []string `arity:"+" help:"torrent file path or magnet uri" arg:"positional"`
277 func statsEnabled(flags downloadFlags) bool {
281 func downloadErr(flags downloadFlags) error {
282 clientConfig := torrent.NewDefaultClientConfig()
283 clientConfig.DisableWebseeds = flags.DisableWebseeds
284 clientConfig.DisableTCP = !flags.TcpPeers
285 clientConfig.DisableUTP = !flags.UtpPeers
286 clientConfig.DisableIPv4 = !flags.Ipv4
287 clientConfig.DisableIPv6 = !flags.Ipv6
288 clientConfig.DisableAcceptRateLimiting = true
289 clientConfig.NoDHT = !flags.Dht
290 clientConfig.Debug = flags.Debug
291 clientConfig.Seed = flags.Seed
292 clientConfig.PublicIp4 = flags.PublicIP.To4()
293 clientConfig.PublicIp6 = flags.PublicIP
294 clientConfig.DisablePEX = !flags.Pex
295 clientConfig.DisableWebtorrent = !flags.Webtorrent
296 clientConfig.NoDefaultPortForwarding = !flags.PortForward
297 if flags.PackedBlocklist != "" {
298 blocklist, err := iplist.MMapPackedFile(flags.PackedBlocklist)
300 return fmt.Errorf("loading blocklist: %v", err)
302 defer blocklist.Close()
303 clientConfig.IPBlocklist = blocklist
306 clientConfig.DefaultStorage = storage.NewMMap("")
308 if flags.Addr != "" {
309 clientConfig.SetListenAddr(flags.Addr)
311 if flags.UploadRate != nil {
312 // TODO: I think the upload rate limit could be much lower.
313 clientConfig.UploadRateLimiter = rate.NewLimiter(rate.Limit(*flags.UploadRate), 256<<10)
315 if flags.DownloadRate != nil {
316 clientConfig.DownloadRateLimiter = rate.NewLimiter(rate.Limit(*flags.DownloadRate), 1<<16)
319 logger := log.Default.WithNames("main", "client")
321 logger = logger.FilterLevel(log.Critical)
323 clientConfig.Logger = logger
325 if flags.RequireFastExtension {
326 clientConfig.MinPeerExtensions.SetBit(pp.ExtensionBitFast, true)
328 if flags.MaxUnverifiedBytes != nil {
329 clientConfig.MaxUnverifiedBytes = flags.MaxUnverifiedBytes.Int64()
332 ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
335 client, err := torrent.NewClient(clientConfig)
337 return fmt.Errorf("creating client: %w", err)
341 // Write status on the root path on the default HTTP muxer. This will be bound to localhost
342 // somewhere if GOPPROF is set, thanks to the envpprof import.
343 http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
344 client.WriteStatus(w)
346 var wg sync.WaitGroup
347 err = addTorrents(ctx, client, flags, &wg)
349 return fmt.Errorf("adding torrents: %w", err)
351 started := time.Now()
352 defer outputStats(client, flags)
354 if ctx.Err() == nil {
355 log.Print("downloaded ALL the torrents")
359 clientConnStats := client.ConnStats()
361 "average download rate: %v/s",
362 humanize.Bytes(uint64(float64(
363 clientConnStats.BytesReadUsefulData.Int64(),
364 )/time.Since(started).Seconds())),
367 if len(client.Torrents()) == 0 {
368 log.Print("no torrents to seed")
370 outputStats(client, flags)
374 spew.Dump(expvar.Get("torrent").(*expvar.Map).Get("chunks received"))
375 spew.Dump(client.ConnStats())
376 clStats := client.ConnStats()
377 sentOverhead := clStats.BytesWritten.Int64() - clStats.BytesWrittenData.Int64()
379 "client read %v, %.1f%% was useful data. sent %v non-data bytes",
380 humanize.Bytes(uint64(clStats.BytesRead.Int64())),
381 100*float64(clStats.BytesReadUsefulData.Int64())/float64(clStats.BytesRead.Int64()),
382 humanize.Bytes(uint64(sentOverhead)))
386 func outputStats(cl *torrent.Client, args downloadFlags) {
387 if !statsEnabled(args) {
390 expvar.Do(func(kv expvar.KeyValue) {
391 fmt.Printf("%s: %s\n", kv.Key, kv.Value)
393 cl.WriteStatus(os.Stdout)