github.com/anacrolix/torrent@v1.61.0/cmd/torrent/download.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"expvar"
     7  	"fmt"
     8  	"io"
     9  	"log/slog"
    10  	"net"
    11  	"net/http"
    12  	"os"
    13  	"strings"
    14  	"sync"
    15  	"time"
    16  
    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"
    22  
    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"
    28  )
    29  
    30  func clientStatusWriter(ctx context.Context, cl *torrent.Client) {
    31  	start := time.Now()
    32  	lastStats := cl.Stats()
    33  	var lastLine string
    34  	var lastPrint time.Time
    35  	interval := 3 * time.Second
    36  	ticker := time.Tick(interval)
    37  	for {
    38  		select {
    39  		case <-ctx.Done():
    40  			return
    41  		case <-ticker:
    42  		}
    43  		stats := cl.Stats()
    44  		ts := cl.Torrents()
    45  		var completeBytes int64
    46  		var totalBytes int64
    47  		var infos int
    48  		for _, t := range ts {
    49  			if t.Info() != nil {
    50  				infos++
    51  				completeBytes += t.BytesCompleted()
    52  				totalBytes += t.Info().TotalLength()
    53  			}
    54  		}
    55  		getRate := func(a func(*torrent.ClientStats) *torrent.Count) int64 {
    56  			byteRate := int64(time.Second)
    57  			byteRate *= a(&stats).Int64() - a(&lastStats).Int64()
    58  			byteRate /= int64(interval)
    59  			return byteRate
    60  		}
    61  		uploadRate := getRate(func(s *torrent.ClientStats) *torrent.Count {
    62  			return &s.BytesWrittenData
    63  		})
    64  		downloadRate := getRate(func(s *torrent.ClientStats) *torrent.Count {
    65  			return &s.BytesReadUsefulData
    66  		})
    67  		line := fmt.Sprintf(
    68  			"%v torrents, %v infos, %s/%s ready, upload %s, download %s/s",
    69  			len(ts),
    70  			infos,
    71  			humanize.Bytes(uint64(completeBytes)),
    72  			humanize.Bytes(uint64(totalBytes)),
    73  			humanize.Bytes(uint64(uploadRate)),
    74  			humanize.Bytes(uint64(downloadRate)),
    75  		)
    76  		if line != lastLine || time.Since(lastPrint) >= time.Minute {
    77  			lastLine = line
    78  			lastPrint = time.Now()
    79  			fmt.Fprintf(os.Stdout, "%s: %s\n", time.Since(start), line)
    80  		}
    81  		lastStats = stats
    82  	}
    83  }
    84  
    85  // Keeping this for now for reference in case I do per-torrent deltas in client status updates.
    86  func torrentBar(t *torrent.Torrent, pieceStates bool) {
    87  	go func() {
    88  		start := time.Now()
    89  		if t.Info() == nil {
    90  			fmt.Printf("%v: getting torrent info for %q\n", time.Since(start), t.Name())
    91  			<-t.GotInfo()
    92  		}
    93  		lastStats := t.Stats()
    94  		var lastLine string
    95  		interval := 3 * time.Second
    96  		for range time.Tick(interval) {
    97  			var completedPieces, partialPieces int
    98  			psrs := t.PieceStateRuns()
    99  			for _, r := range psrs {
   100  				if r.Complete {
   101  					completedPieces += r.Length
   102  				}
   103  				if r.Partial {
   104  					partialPieces += r.Length
   105  				}
   106  			}
   107  			stats := t.Stats()
   108  			byteRate := int64(time.Second)
   109  			byteRate *= stats.BytesReadUsefulData.Int64() - lastStats.BytesReadUsefulData.Int64()
   110  			byteRate /= int64(interval)
   111  			line := fmt.Sprintf(
   112  				"%v: downloading %q: %s/%s, %d/%d pieces completed (%d partial): %v/s\n",
   113  				time.Since(start),
   114  				t.Name(),
   115  				humanize.Bytes(uint64(t.BytesCompleted())),
   116  				humanize.Bytes(uint64(t.Length())),
   117  				completedPieces,
   118  				t.NumPieces(),
   119  				partialPieces,
   120  				humanize.Bytes(uint64(byteRate)),
   121  			)
   122  			if line != lastLine {
   123  				lastLine = line
   124  				os.Stdout.WriteString(line)
   125  			}
   126  			if pieceStates {
   127  				fmt.Println(psrs)
   128  			}
   129  			lastStats = stats
   130  		}
   131  	}()
   132  }
   133  
   134  type stringAddr string
   135  
   136  func (stringAddr) Network() string   { return "" }
   137  func (me stringAddr) String() string { return string(me) }
   138  
   139  func resolveTestPeers(addrs []string) (ret []torrent.PeerInfo) {
   140  	for _, ta := range addrs {
   141  		ret = append(ret, torrent.PeerInfo{
   142  			Addr: stringAddr(ta),
   143  		})
   144  	}
   145  	return
   146  }
   147  
   148  func addTorrents(
   149  	ctx context.Context,
   150  	client *torrent.Client,
   151  	flags downloadFlags,
   152  	wg *sync.WaitGroup,
   153  	fatalErr func(err error),
   154  ) error {
   155  	testPeers := resolveTestPeers(flags.TestPeer)
   156  	for _, arg := range flags.Torrent {
   157  		if ctx.Err() != nil {
   158  			return ctx.Err()
   159  		}
   160  		t, err := func() (*torrent.Torrent, error) {
   161  			if strings.HasPrefix(arg, "magnet:") {
   162  				t, err := client.AddMagnet(arg)
   163  				if err != nil {
   164  					return nil, fmt.Errorf("error adding magnet: %w", err)
   165  				}
   166  				return t, nil
   167  			} else if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") {
   168  				response, err := http.Get(arg)
   169  				if err != nil {
   170  					return nil, fmt.Errorf("error downloading torrent file: %w", err)
   171  				}
   172  
   173  				metaInfo, err := metainfo.Load(response.Body)
   174  				defer response.Body.Close()
   175  				if err != nil {
   176  					return nil, fmt.Errorf("error loading torrent file %q: %s\n", arg, err)
   177  				}
   178  				t, err := client.AddTorrent(metaInfo)
   179  				if err != nil {
   180  					return nil, err
   181  				}
   182  				return t, nil
   183  			} else if strings.HasPrefix(arg, "infohash:") {
   184  				t, _ := client.AddTorrentInfoHash(metainfo.NewHashFromHex(strings.TrimPrefix(arg, "infohash:")))
   185  				return t, nil
   186  			} else {
   187  				metaInfo, err := metainfo.LoadFromFile(arg)
   188  				if err != nil {
   189  					return nil, fmt.Errorf("error loading torrent file %q: %s\n", arg, err)
   190  				}
   191  				t, err := client.AddTorrent(metaInfo)
   192  				if err != nil {
   193  					return nil, fmt.Errorf("adding torrent: %w", err)
   194  				}
   195  				return t, nil
   196  			}
   197  		}()
   198  		if err != nil {
   199  			return fmt.Errorf("adding torrent for %q: %w", arg, err)
   200  		}
   201  		t.SetOnWriteChunkError(func(err error) {
   202  			err = fmt.Errorf("error writing chunk for %v: %w", t, err)
   203  			fatalErr(err)
   204  		})
   205  		t.AddPeers(testPeers)
   206  		wg.Add(1)
   207  		go func() {
   208  			defer wg.Done()
   209  			select {
   210  			case <-ctx.Done():
   211  				return
   212  			case <-t.GotInfo():
   213  			}
   214  			if flags.SaveMetainfos {
   215  				path := fmt.Sprintf("%s.torrent", t.InfoHash().HexString())
   216  				err := writeMetainfoToFile(t.Metainfo(), path)
   217  				if err == nil {
   218  					log.Printf("wrote %q", path)
   219  				} else {
   220  					log.Printf("error writing %q: %v", path, err)
   221  				}
   222  			}
   223  			if flags.Verify {
   224  				err := t.VerifyDataContext(ctx)
   225  				if err != nil && ctx.Err() == nil {
   226  					log.Levelf(log.Error, "error verifying data: %v", err)
   227  				}
   228  			}
   229  			if len(flags.File) == 0 {
   230  				t.DownloadAll()
   231  				wg.Add(1)
   232  				go func() {
   233  					defer wg.Done()
   234  					waitForPieces(ctx, t, 0, t.NumPieces())
   235  				}()
   236  				done := make(chan struct{})
   237  				go func() {
   238  					defer close(done)
   239  					if flags.LinearDiscard {
   240  						r := t.NewReader()
   241  						io.Copy(io.Discard, r)
   242  						r.Close()
   243  					}
   244  				}()
   245  				select {
   246  				case <-done:
   247  				case <-ctx.Done():
   248  				}
   249  			} else {
   250  				for _, f := range t.Files() {
   251  					for _, fileArg := range flags.File {
   252  						if f.DisplayPath() == fileArg {
   253  							wg.Add(1)
   254  							go func() {
   255  								defer wg.Done()
   256  								waitForPieces(ctx, t, f.BeginPieceIndex(), f.EndPieceIndex())
   257  							}()
   258  							f.Download()
   259  							if flags.LinearDiscard {
   260  								r := f.NewReader()
   261  								go func() {
   262  									defer r.Close()
   263  									io.Copy(io.Discard, r)
   264  								}()
   265  							}
   266  						}
   267  					}
   268  				}
   269  			}
   270  		}()
   271  	}
   272  	return nil
   273  }
   274  
   275  func waitForPieces(ctx context.Context, t *torrent.Torrent, beginIndex, endIndex int) {
   276  	sub := t.SubscribePieceStateChanges()
   277  	defer sub.Close()
   278  	expected := storage.Completion{
   279  		Complete: true,
   280  		Ok:       true,
   281  	}
   282  	pending := make(map[int]struct{})
   283  	for i := beginIndex; i < endIndex; i++ {
   284  		if t.Piece(i).State().Completion != expected {
   285  			pending[i] = struct{}{}
   286  		}
   287  	}
   288  	for {
   289  		if len(pending) == 0 {
   290  			return
   291  		}
   292  		select {
   293  		case ev := <-sub.Values:
   294  			if ev.Completion == expected {
   295  				delete(pending, ev.Index)
   296  			}
   297  		case <-ctx.Done():
   298  			return
   299  		}
   300  	}
   301  }
   302  
   303  func writeMetainfoToFile(mi metainfo.MetaInfo, path string) error {
   304  	f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o640)
   305  	if err != nil {
   306  		return err
   307  	}
   308  	defer f.Close()
   309  	err = mi.Write(f)
   310  	if err != nil {
   311  		return err
   312  	}
   313  	return f.Close()
   314  }
   315  
   316  type downloadFlags struct {
   317  	Debug bool
   318  	DownloadCmd
   319  }
   320  
   321  type DownloadCmd struct {
   322  	SaveMetainfos                  bool           `help:"save metainfo files when info is obtained"`
   323  	Mmap                           bool           `help:"memory-map torrent data"`
   324  	Seed                           bool           `help:"seed after download is complete"`
   325  	Addr                           string         `help:"network listen addr"`
   326  	MaxUnverifiedBytes             *tagflag.Bytes `help:"maximum number bytes to have pending verification"`
   327  	UploadRate                     *tagflag.Bytes `help:"max piece bytes to send per second"`
   328  	MaxAllocPeerRequestDataPerConn *tagflag.Bytes `help:"max bytes to allocate for peer request data per connection"`
   329  	DownloadRate                   *tagflag.Bytes `help:"max bytes per second down from peers"`
   330  	PackedBlocklist                string
   331  	PublicIP                       net.IP
   332  	Progress                       bool `default:"true"`
   333  	PieceStates                    bool `help:"Output piece state runs at progress intervals."`
   334  	Quiet                          bool `help:"discard client logging"`
   335  	Stats                          bool `help:"print stats at termination"`
   336  	Dht                            bool `default:"true"`
   337  	PortForward                    bool `default:"true"`
   338  	Verify                         bool `help:"verify data after adding torrent"`
   339  
   340  	TcpPeers        bool `default:"true"`
   341  	UtpPeers        bool `default:"true"`
   342  	Webtorrent      bool `default:"true"`
   343  	DisableWebseeds bool
   344  	// Don't progress past handshake for peer connections where the peer doesn't offer the fast
   345  	// extension.
   346  	RequireFastExtension bool
   347  
   348  	Ipv4 bool `default:"true"`
   349  	Ipv6 bool `default:"true"`
   350  	Pex  bool `default:"true"`
   351  
   352  	LinearDiscard bool     `help:"Read and discard selected regions from start to finish. Useful for testing simultaneous Reader and static file prioritization."`
   353  	TestPeer      []string `help:"addresses of some starting peers"`
   354  
   355  	File    []string
   356  	Torrent []string `arity:"+" help:"torrent file path or magnet uri" arg:"positional"`
   357  }
   358  
   359  func statsEnabled(flags downloadFlags) bool {
   360  	return flags.Stats
   361  }
   362  
   363  func downloadErr(ctx context.Context, flags downloadFlags, logger *slog.Logger) error {
   364  	if flags.Quiet {
   365  		// This will only affect the client, and this function.
   366  		logger = slog.New(slogLevelFilterHandler{
   367  			minLevel: slog.LevelError,
   368  			inner:    logger.Handler(),
   369  		})
   370  	}
   371  	clientConfig := torrent.NewDefaultClientConfig()
   372  	clientConfig.DisableWebseeds = flags.DisableWebseeds
   373  	clientConfig.DisableTCP = !flags.TcpPeers
   374  	clientConfig.DisableUTP = !flags.UtpPeers
   375  	clientConfig.DisableIPv4 = !flags.Ipv4
   376  	clientConfig.DisableIPv6 = !flags.Ipv6
   377  	clientConfig.DisableAcceptRateLimiting = true
   378  	clientConfig.NoDHT = !flags.Dht
   379  	clientConfig.Debug = flags.Debug
   380  	clientConfig.Seed = flags.Seed
   381  	clientConfig.PublicIp4 = flags.PublicIP.To4()
   382  	clientConfig.PublicIp6 = flags.PublicIP
   383  	clientConfig.DisablePEX = !flags.Pex
   384  	clientConfig.DisableWebtorrent = !flags.Webtorrent
   385  	clientConfig.NoDefaultPortForwarding = !flags.PortForward
   386  	if flags.MaxAllocPeerRequestDataPerConn != nil {
   387  		clientConfig.MaxAllocPeerRequestDataPerConn = int(flags.MaxAllocPeerRequestDataPerConn.Int64())
   388  	}
   389  	if flags.PackedBlocklist != "" {
   390  		blocklist, err := iplist.MMapPackedFile(flags.PackedBlocklist)
   391  		if err != nil {
   392  			return fmt.Errorf("loading blocklist: %v", err)
   393  		}
   394  		defer blocklist.Close()
   395  		clientConfig.IPBlocklist = blocklist
   396  	}
   397  	if flags.Mmap {
   398  		clientConfig.DefaultStorage = storage.NewMMap("")
   399  	}
   400  	if flags.Addr != "" {
   401  		clientConfig.SetListenAddr(flags.Addr)
   402  	}
   403  	if flags.UploadRate != nil {
   404  		clientConfig.UploadRateLimiter = rate.NewLimiter(
   405  			rate.Limit(*flags.UploadRate),
   406  			// Need to ensure the expected peer request length <= the upload burst. We can't really
   407  			// encode this logic into the ClientConfig as helper because it's quite specific. We're
   408  			// assuming the MaxAllocPeerRequestDataPerConn flag is being used to support this.
   409  			max(int(*flags.MaxAllocPeerRequestDataPerConn), 256<<10))
   410  	}
   411  	if flags.DownloadRate != nil {
   412  		clientConfig.DownloadRateLimiter = rate.NewLimiter(rate.Limit(*flags.DownloadRate), 0)
   413  	}
   414  	clientConfig.Slogger = logger
   415  	if flags.RequireFastExtension {
   416  		clientConfig.MinPeerExtensions.SetBit(pp.ExtensionBitFast, true)
   417  	}
   418  	if flags.MaxUnverifiedBytes != nil {
   419  		clientConfig.MaxUnverifiedBytes = flags.MaxUnverifiedBytes.Int64()
   420  	}
   421  
   422  	client, err := torrent.NewClient(clientConfig)
   423  	if err != nil {
   424  		return fmt.Errorf("creating client: %w", err)
   425  	}
   426  	defer client.Close()
   427  
   428  	if flags.Progress {
   429  		go clientStatusWriter(ctx, client)
   430  	}
   431  
   432  	// Write status on the root path on the default HTTP muxer. This will be bound to localhost
   433  	// somewhere if GOPPROF is set, thanks to the envpprof import.
   434  	http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
   435  		client.WriteStatus(w)
   436  	})
   437  	var wg sync.WaitGroup
   438  	fatalErr := make(chan error, 1)
   439  	err = addTorrents(ctx, client, flags, &wg,
   440  		func(err error) {
   441  			select {
   442  			case fatalErr <- err:
   443  			default:
   444  				panic(err)
   445  			}
   446  		})
   447  	if err != nil {
   448  		return fmt.Errorf("adding torrents: %w", err)
   449  	}
   450  	started := time.Now()
   451  	defer outputStats(client, flags)
   452  	wgWaited := make(chan struct{})
   453  	go func() {
   454  		defer close(wgWaited)
   455  		wg.Wait()
   456  	}()
   457  	select {
   458  	case <-wgWaited:
   459  		if ctx.Err() == nil {
   460  			slog.Info("downloaded ALL the torrents")
   461  		} else {
   462  			err = ctx.Err()
   463  		}
   464  	case err = <-fatalErr:
   465  	}
   466  	clientConnStats := client.ConnStats()
   467  	log.Printf(
   468  		"average download rate: %v/s",
   469  		humanize.Bytes(uint64(float64(
   470  			clientConnStats.BytesReadUsefulData.Int64(),
   471  		)/time.Since(started).Seconds())),
   472  	)
   473  	if flags.Seed {
   474  		if len(client.Torrents()) == 0 {
   475  			log.Print("no torrents to seed")
   476  		} else {
   477  			outputStats(client, flags)
   478  			<-ctx.Done()
   479  		}
   480  	}
   481  	if flags.Stats {
   482  		fmt.Printf("chunks received: %v\n", &torrent.ChunksReceived)
   483  		var buf bytes.Buffer
   484  		spew.Fdump(&buf, client.Stats())
   485  		os.Stdout.Write(buf.Bytes())
   486  		clStats := client.ConnStats()
   487  		sentOverhead := clStats.BytesWritten.Int64() - clStats.BytesWrittenData.Int64()
   488  		slog.Info(fmt.Sprintf(
   489  			"client read %v, %.1f%% was useful data. sent %v non-data bytes",
   490  			humanize.Bytes(uint64(clStats.BytesRead.Int64())),
   491  			100*float64(clStats.BytesReadUsefulData.Int64())/float64(clStats.BytesRead.Int64()),
   492  			humanize.Bytes(uint64(sentOverhead))))
   493  	}
   494  	return err
   495  }
   496  
   497  func outputStats(cl *torrent.Client, args downloadFlags) {
   498  	if !statsEnabled(args) {
   499  		return
   500  	}
   501  	expvar.Do(func(kv expvar.KeyValue) {
   502  		fmt.Printf("%s: %s\n", kv.Key, kv.Value)
   503  	})
   504  	cl.WriteStatus(os.Stdout)
   505  }