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 }