github.com/ledgerwatch/erigon-lib@v1.0.0/downloader/downloader.go (about) 1 /* 2 Copyright 2021 Erigon contributors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package downloader 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "io/fs" 24 "net/http" 25 "net/url" 26 "os" 27 "path/filepath" 28 "runtime" 29 "sync" 30 "sync/atomic" 31 "time" 32 33 "github.com/anacrolix/torrent" 34 "github.com/anacrolix/torrent/metainfo" 35 "github.com/anacrolix/torrent/storage" 36 common2 "github.com/ledgerwatch/erigon-lib/common" 37 "github.com/ledgerwatch/erigon-lib/common/dir" 38 "github.com/ledgerwatch/erigon-lib/downloader/downloadercfg" 39 "github.com/ledgerwatch/erigon-lib/downloader/snaptype" 40 "github.com/ledgerwatch/erigon-lib/kv" 41 "github.com/ledgerwatch/erigon-lib/kv/mdbx" 42 "github.com/ledgerwatch/log/v3" 43 "github.com/pelletier/go-toml/v2" 44 "golang.org/x/exp/maps" 45 "golang.org/x/sync/errgroup" 46 "golang.org/x/sync/semaphore" 47 ) 48 49 // Downloader - component which downloading historical files. Can use BitTorrent, or other protocols 50 type Downloader struct { 51 db kv.RwDB 52 pieceCompletionDB storage.PieceCompletion 53 torrentClient *torrent.Client 54 55 cfg *downloadercfg.Cfg 56 57 statsLock *sync.RWMutex 58 stats AggStats 59 60 folder storage.ClientImplCloser 61 62 ctx context.Context 63 stopMainLoop context.CancelFunc 64 wg sync.WaitGroup 65 66 webseeds *WebSeeds 67 } 68 69 type AggStats struct { 70 MetadataReady, FilesTotal int32 71 PeersUnique int32 72 ConnectionsTotal uint64 73 74 Completed bool 75 Progress float32 76 77 BytesCompleted, BytesTotal uint64 78 DroppedCompleted, DroppedTotal uint64 79 80 BytesDownload, BytesUpload uint64 81 UploadRate, DownloadRate uint64 82 } 83 84 func New(ctx context.Context, cfg *downloadercfg.Cfg) (*Downloader, error) { 85 if err := portMustBeTCPAndUDPOpen(cfg.ClientConfig.ListenPort); err != nil { 86 return nil, err 87 } 88 89 // Application must never see partially-downloaded files 90 // To provide such consistent view - downloader does: 91 // add <datadir>/snapshots/tmp - then method .onComplete will remove this suffix 92 // and App only work with <datadir>/snapshot s folder 93 if dir.FileExist(cfg.SnapDir + "_tmp") { // migration from prev versions 94 _ = os.Rename(cfg.SnapDir+"_tmp", filepath.Join(cfg.SnapDir, "tmp")) // ignore error, because maybe they are on different drive, or target folder already created manually, all is fine 95 } 96 if err := moveFromTmp(cfg.SnapDir); err != nil { 97 return nil, err 98 } 99 100 db, c, m, torrentClient, err := openClient(cfg.ClientConfig) 101 if err != nil { 102 return nil, fmt.Errorf("openClient: %w", err) 103 } 104 105 peerID, err := readPeerID(db) 106 if err != nil { 107 return nil, fmt.Errorf("get peer id: %w", err) 108 } 109 cfg.ClientConfig.PeerID = string(peerID) 110 if len(peerID) == 0 { 111 if err = savePeerID(db, torrentClient.PeerID()); err != nil { 112 return nil, fmt.Errorf("save peer id: %w", err) 113 } 114 } 115 116 d := &Downloader{ 117 cfg: cfg, 118 db: db, 119 pieceCompletionDB: c, 120 folder: m, 121 torrentClient: torrentClient, 122 statsLock: &sync.RWMutex{}, 123 webseeds: &WebSeeds{}, 124 } 125 d.ctx, d.stopMainLoop = context.WithCancel(ctx) 126 127 if err := d.addSegments(d.ctx); err != nil { 128 return nil, err 129 } 130 // CornerCase: no peers -> no anoncments to trackers -> no magnetlink resolution (but magnetlink has filename) 131 // means we can start adding weebseeds without waiting for `<-t.GotInfo()` 132 d.wg.Add(1) 133 go func() { 134 defer d.wg.Done() 135 d.webseeds.Discover(d.ctx, d.cfg.WebSeedUrls, d.cfg.WebSeedFiles) 136 d.applyWebseeds() 137 }() 138 return d, nil 139 } 140 141 func (d *Downloader) MainLoopInBackground(silent bool) { 142 d.wg.Add(1) 143 go func() { 144 defer d.wg.Done() 145 if err := d.mainLoop(silent); err != nil { 146 if !errors.Is(err, context.Canceled) { 147 log.Warn("[snapshots]", "err", err) 148 } 149 } 150 }() 151 } 152 153 func (d *Downloader) mainLoop(silent bool) error { 154 var sem = semaphore.NewWeighted(int64(d.cfg.DownloadSlots)) 155 156 d.wg.Add(1) 157 go func() { 158 defer d.wg.Done() 159 160 // Torrents that are already taken care of 161 torrentMap := map[metainfo.Hash]struct{}{} 162 // First loop drops torrents that were downloaded or are already complete 163 // This improves efficiency of download by reducing number of active torrent (empirical observation) 164 for torrents := d.torrentClient.Torrents(); len(torrents) > 0; torrents = d.torrentClient.Torrents() { 165 for _, t := range torrents { 166 if _, already := torrentMap[t.InfoHash()]; already { 167 continue 168 } 169 select { 170 case <-d.ctx.Done(): 171 return 172 case <-t.GotInfo(): 173 } 174 if t.Complete.Bool() { 175 atomic.AddUint64(&d.stats.DroppedCompleted, uint64(t.BytesCompleted())) 176 atomic.AddUint64(&d.stats.DroppedTotal, uint64(t.Length())) 177 t.Drop() 178 torrentMap[t.InfoHash()] = struct{}{} 179 continue 180 } 181 if err := sem.Acquire(d.ctx, 1); err != nil { 182 return 183 } 184 t.AllowDataDownload() 185 t.DownloadAll() 186 torrentMap[t.InfoHash()] = struct{}{} 187 d.wg.Add(1) 188 go func(t *torrent.Torrent) { 189 defer d.wg.Done() 190 defer sem.Release(1) 191 select { 192 case <-d.ctx.Done(): 193 return 194 case <-t.Complete.On(): 195 } 196 atomic.AddUint64(&d.stats.DroppedCompleted, uint64(t.BytesCompleted())) 197 atomic.AddUint64(&d.stats.DroppedTotal, uint64(t.Length())) 198 t.Drop() 199 }(t) 200 } 201 } 202 atomic.StoreUint64(&d.stats.DroppedCompleted, 0) 203 atomic.StoreUint64(&d.stats.DroppedTotal, 0) 204 d.addSegments(d.ctx) 205 maps.Clear(torrentMap) 206 for { 207 torrents := d.torrentClient.Torrents() 208 for _, t := range torrents { 209 if _, already := torrentMap[t.InfoHash()]; already { 210 continue 211 } 212 select { 213 case <-d.ctx.Done(): 214 return 215 case <-t.GotInfo(): 216 } 217 if t.Complete.Bool() { 218 torrentMap[t.InfoHash()] = struct{}{} 219 continue 220 } 221 if err := sem.Acquire(d.ctx, 1); err != nil { 222 return 223 } 224 t.AllowDataDownload() 225 t.DownloadAll() 226 torrentMap[t.InfoHash()] = struct{}{} 227 d.wg.Add(1) 228 go func(t *torrent.Torrent) { 229 defer d.wg.Done() 230 defer sem.Release(1) 231 select { 232 case <-d.ctx.Done(): 233 return 234 case <-t.Complete.On(): 235 } 236 }(t) 237 } 238 time.Sleep(10 * time.Second) 239 } 240 }() 241 242 logEvery := time.NewTicker(20 * time.Second) 243 defer logEvery.Stop() 244 245 statInterval := 20 * time.Second 246 statEvery := time.NewTicker(statInterval) 247 defer statEvery.Stop() 248 249 justCompleted := true 250 for { 251 select { 252 case <-d.ctx.Done(): 253 return d.ctx.Err() 254 case <-statEvery.C: 255 d.ReCalcStats(statInterval) 256 257 case <-logEvery.C: 258 if silent { 259 continue 260 } 261 262 stats := d.Stats() 263 264 if stats.Completed { 265 if justCompleted { 266 justCompleted = false 267 // force fsync of db. to not loose results of downloading on power-off 268 _ = d.db.Update(d.ctx, func(tx kv.RwTx) error { return nil }) 269 } 270 271 log.Info("[snapshots] Seeding", 272 "up", common2.ByteCount(stats.UploadRate)+"/s", 273 "peers", stats.PeersUnique, 274 "conns", stats.ConnectionsTotal, 275 "files", stats.FilesTotal) 276 continue 277 } 278 279 log.Info("[snapshots] Downloading", 280 "progress", fmt.Sprintf("%.2f%% %s/%s", stats.Progress, common2.ByteCount(stats.BytesCompleted), common2.ByteCount(stats.BytesTotal)), 281 "download", common2.ByteCount(stats.DownloadRate)+"/s", 282 "upload", common2.ByteCount(stats.UploadRate)+"/s", 283 "peers", stats.PeersUnique, 284 "conns", stats.ConnectionsTotal, 285 "files", stats.FilesTotal) 286 287 if stats.PeersUnique == 0 { 288 ips := d.TorrentClient().BadPeerIPs() 289 if len(ips) > 0 { 290 log.Info("[snapshots] Stats", "banned", ips) 291 } 292 } 293 } 294 } 295 } 296 297 func (d *Downloader) SnapDir() string { return d.cfg.SnapDir } 298 299 func (d *Downloader) ReCalcStats(interval time.Duration) { 300 //Call this methods outside of `statsLock` critical section, because they have own locks with contention 301 torrents := d.torrentClient.Torrents() 302 connStats := d.torrentClient.ConnStats() 303 peers := make(map[torrent.PeerID]struct{}, 16) 304 305 d.statsLock.Lock() 306 defer d.statsLock.Unlock() 307 prevStats, stats := d.stats, d.stats 308 309 stats.Completed = true 310 stats.BytesDownload = uint64(connStats.BytesReadUsefulIntendedData.Int64()) 311 stats.BytesUpload = uint64(connStats.BytesWrittenData.Int64()) 312 313 stats.BytesTotal, stats.BytesCompleted, stats.ConnectionsTotal, stats.MetadataReady = atomic.LoadUint64(&stats.DroppedTotal), atomic.LoadUint64(&stats.DroppedCompleted), 0, 0 314 for _, t := range torrents { 315 select { 316 case <-t.GotInfo(): 317 stats.MetadataReady++ 318 for _, peer := range t.PeerConns() { 319 stats.ConnectionsTotal++ 320 peers[peer.PeerID] = struct{}{} 321 } 322 stats.BytesCompleted += uint64(t.BytesCompleted()) 323 stats.BytesTotal += uint64(t.Length()) 324 if !t.Complete.Bool() { 325 progress := float32(float64(100) * (float64(t.BytesCompleted()) / float64(t.Length()))) 326 log.Debug("[downloader] file not downloaded yet", "name", t.Name(), "progress", fmt.Sprintf("%.2f%%", progress)) 327 } 328 default: 329 log.Debug("[downloader] file has no metadata yet", "name", t.Name()) 330 } 331 332 stats.Completed = stats.Completed && t.Complete.Bool() 333 } 334 335 stats.DownloadRate = (stats.BytesDownload - prevStats.BytesDownload) / uint64(interval.Seconds()) 336 stats.UploadRate = (stats.BytesUpload - prevStats.BytesUpload) / uint64(interval.Seconds()) 337 338 if stats.BytesTotal == 0 { 339 stats.Progress = 0 340 } else { 341 stats.Progress = float32(float64(100) * (float64(stats.BytesCompleted) / float64(stats.BytesTotal))) 342 if stats.Progress == 100 && !stats.Completed { 343 stats.Progress = 99.99 344 } 345 } 346 stats.PeersUnique = int32(len(peers)) 347 stats.FilesTotal = int32(len(torrents)) 348 349 d.stats = stats 350 } 351 352 func moveFromTmp(snapDir string) error { 353 tmpDir := filepath.Join(snapDir, "tmp") 354 if !dir.FileExist(tmpDir) { 355 return nil 356 } 357 358 snFs := os.DirFS(tmpDir) 359 paths, err := fs.ReadDir(snFs, ".") 360 if err != nil { 361 return err 362 } 363 for _, p := range paths { 364 if p.IsDir() || !p.Type().IsRegular() { 365 continue 366 } 367 if p.Name() == "tmp" { 368 continue 369 } 370 src := filepath.Join(tmpDir, p.Name()) 371 if err := os.Rename(src, filepath.Join(snapDir, p.Name())); err != nil { 372 if os.IsExist(err) { 373 _ = os.Remove(src) 374 continue 375 } 376 return err 377 } 378 } 379 _ = os.Remove(tmpDir) 380 return nil 381 } 382 383 func (d *Downloader) verifyFile(ctx context.Context, t *torrent.Torrent, completePieces *atomic.Uint64) error { 384 select { 385 case <-ctx.Done(): 386 return ctx.Err() 387 case <-t.GotInfo(): 388 } 389 390 g := &errgroup.Group{} 391 for i := 0; i < t.NumPieces(); i++ { 392 i := i 393 g.Go(func() error { 394 select { 395 case <-ctx.Done(): 396 return ctx.Err() 397 default: 398 } 399 400 t.Piece(i).VerifyData() 401 completePieces.Add(1) 402 return nil 403 }) 404 //<-t.Complete.On() 405 } 406 return g.Wait() 407 } 408 409 func (d *Downloader) VerifyData(ctx context.Context) error { 410 total := 0 411 for _, t := range d.torrentClient.Torrents() { 412 select { 413 case <-t.GotInfo(): 414 total += t.NumPieces() 415 default: 416 continue 417 } 418 } 419 420 completedPieces := &atomic.Uint64{} 421 422 { 423 log.Info("[snapshots] Verify start") 424 defer log.Info("[snapshots] Verify done") 425 ctx, cancel := context.WithCancel(ctx) 426 defer cancel() 427 logInterval := 20 * time.Second 428 logEvery := time.NewTicker(logInterval) 429 defer logEvery.Stop() 430 d.wg.Add(1) 431 go func() { 432 defer d.wg.Done() 433 for { 434 select { 435 case <-ctx.Done(): 436 return 437 case <-logEvery.C: 438 log.Info("[snapshots] Verify", "progress", fmt.Sprintf("%.2f%%", 100*float64(completedPieces.Load())/float64(total))) 439 } 440 } 441 }() 442 } 443 444 g, ctx := errgroup.WithContext(ctx) 445 // torrent lib internally limiting amount of hashers per file 446 // set limit here just to make load predictable, not to control Disk/CPU consumption 447 g.SetLimit(runtime.GOMAXPROCS(-1) * 4) 448 449 for _, t := range d.torrentClient.Torrents() { 450 t := t 451 g.Go(func() error { 452 return d.verifyFile(ctx, t, completedPieces) 453 }) 454 } 455 456 g.Wait() 457 // force fsync of db. to not loose results of validation on power-off 458 return d.db.Update(context.Background(), func(tx kv.RwTx) error { return nil }) 459 } 460 461 // AddNewSeedableFile decides what we do depending on wether we have the .seg file or the .torrent file 462 // have .torrent no .seg => get .seg file from .torrent 463 // have .seg no .torrent => get .torrent from .seg 464 func (d *Downloader) AddNewSeedableFile(ctx context.Context, name string) error { 465 select { 466 case <-ctx.Done(): 467 return ctx.Err() 468 default: 469 } 470 // if we don't have the torrent file we build it if we have the .seg file 471 torrentFilePath, err := BuildTorrentIfNeed(ctx, name, d.SnapDir()) 472 if err != nil { 473 return err 474 } 475 ts, err := loadTorrent(torrentFilePath) 476 if err != nil { 477 return err 478 } 479 _, err = addTorrentFile(ts, d.torrentClient) 480 if err != nil { 481 return fmt.Errorf("addTorrentFile: %w", err) 482 } 483 return nil 484 } 485 486 func (d *Downloader) exists(name string) bool { 487 // Paranoic Mode on: if same file changed infoHash - skip it 488 // use-cases: 489 // - release of re-compressed version of same file, 490 // - ErigonV1.24 produced file X, then ErigonV1.25 released with new compression algorithm and produced X with anouther infoHash. 491 // ErigonV1.24 node must keep using existing file instead of downloading new one. 492 for _, t := range d.torrentClient.Torrents() { 493 if t.Name() == name { 494 return true 495 } 496 } 497 return false 498 } 499 func (d *Downloader) AddInfoHashAsMagnetLink(ctx context.Context, infoHash metainfo.Hash, name string) error { 500 if d.exists(name) { 501 return nil 502 } 503 mi := &metainfo.MetaInfo{AnnounceList: Trackers} 504 505 magnet := mi.Magnet(&infoHash, &metainfo.Info{Name: name}) 506 t, err := d.torrentClient.AddMagnet(magnet.String()) 507 if err != nil { 508 //log.Warn("[downloader] add magnet link", "err", err) 509 return err 510 } 511 t.DisallowDataDownload() 512 t.AllowDataUpload() 513 d.wg.Add(1) 514 go func(t *torrent.Torrent) { 515 defer d.wg.Done() 516 select { 517 case <-ctx.Done(): 518 return 519 case <-t.GotInfo(): 520 } 521 522 mi := t.Metainfo() 523 if err := CreateTorrentFileIfNotExists(d.SnapDir(), t.Info(), &mi); err != nil { 524 log.Warn("[downloader] create torrent file", "err", err) 525 return 526 } 527 }(t) 528 //log.Debug("[downloader] downloaded both seg and torrent files", "hash", infoHash) 529 return nil 530 } 531 532 func seedableFiles(snapDir string) ([]string, error) { 533 files, err := seedableSegmentFiles(snapDir) 534 if err != nil { 535 return nil, fmt.Errorf("seedableSegmentFiles: %w", err) 536 } 537 files2, err := seedableHistorySnapshots(snapDir, "history") 538 if err != nil { 539 return nil, fmt.Errorf("seedableHistorySnapshots: %w", err) 540 } 541 files = append(files, files2...) 542 files2, err = seedableHistorySnapshots(snapDir, "warm") 543 if err != nil { 544 return nil, fmt.Errorf("seedableHistorySnapshots: %w", err) 545 } 546 files = append(files, files2...) 547 return files, nil 548 } 549 func (d *Downloader) addSegments(ctx context.Context) error { 550 _, err := BuildTorrentFilesIfNeed(ctx, d.SnapDir()) 551 if err != nil { 552 return err 553 } 554 return AddTorrentFiles(d.SnapDir(), d.torrentClient) 555 } 556 557 func (d *Downloader) Stats() AggStats { 558 d.statsLock.RLock() 559 defer d.statsLock.RUnlock() 560 return d.stats 561 } 562 563 func (d *Downloader) Close() { 564 d.stopMainLoop() 565 d.wg.Wait() 566 d.torrentClient.Close() 567 if err := d.folder.Close(); err != nil { 568 log.Warn("[snapshots] folder.close", "err", err) 569 } 570 if err := d.pieceCompletionDB.Close(); err != nil { 571 log.Warn("[snapshots] pieceCompletionDB.close", "err", err) 572 } 573 d.db.Close() 574 } 575 576 func (d *Downloader) PeerID() []byte { 577 peerID := d.torrentClient.PeerID() 578 return peerID[:] 579 } 580 581 func (d *Downloader) StopSeeding(hash metainfo.Hash) error { 582 t, ok := d.torrentClient.Torrent(hash) 583 if !ok { 584 return nil 585 } 586 ch := t.Closed() 587 t.Drop() 588 <-ch 589 return nil 590 } 591 592 func (d *Downloader) TorrentClient() *torrent.Client { return d.torrentClient } 593 594 func openClient(cfg *torrent.ClientConfig) (db kv.RwDB, c storage.PieceCompletion, m storage.ClientImplCloser, torrentClient *torrent.Client, err error) { 595 snapDir := cfg.DataDir 596 db, err = mdbx.NewMDBX(log.New()). 597 Label(kv.DownloaderDB). 598 WithTableCfg(func(defaultBuckets kv.TableCfg) kv.TableCfg { return kv.DownloaderTablesCfg }). 599 SyncPeriod(15 * time.Second). 600 Path(filepath.Join(snapDir, "db")). 601 Open() 602 if err != nil { 603 return nil, nil, nil, nil, fmt.Errorf("torrentcfg.openClient: %w", err) 604 } 605 c, err = NewMdbxPieceCompletion(db) 606 if err != nil { 607 return nil, nil, nil, nil, fmt.Errorf("torrentcfg.NewMdbxPieceCompletion: %w", err) 608 } 609 m = storage.NewMMapWithCompletion(snapDir, c) 610 cfg.DefaultStorage = m 611 612 for retry := 0; retry < 5; retry++ { 613 torrentClient, err = torrent.NewClient(cfg) 614 if err == nil { 615 break 616 } 617 time.Sleep(10 * time.Millisecond) 618 } 619 if err != nil { 620 return nil, nil, nil, nil, fmt.Errorf("torrent.NewClient: %w", err) 621 } 622 623 return db, c, m, torrentClient, nil 624 } 625 626 func (d *Downloader) applyWebseeds() { 627 for _, t := range d.TorrentClient().Torrents() { 628 urls, ok := d.webseeds.GetByFileNames()[t.Name()] 629 if !ok { 630 continue 631 } 632 log.Debug("[downloader] addd webseeds", "file", t.Name()) 633 t.AddWebSeeds(urls) 634 } 635 } 636 637 type WebSeeds struct { 638 lock sync.Mutex 639 webSeedsByFilName snaptype.WebSeeds 640 } 641 642 func (d *WebSeeds) GetByFileNames() snaptype.WebSeeds { 643 d.lock.Lock() 644 defer d.lock.Unlock() 645 return d.webSeedsByFilName 646 } 647 func (d *WebSeeds) SetByFileNames(l snaptype.WebSeeds) { 648 d.lock.Lock() 649 defer d.lock.Unlock() 650 d.webSeedsByFilName = l 651 } 652 653 func (d *WebSeeds) callWebSeedsProvider(ctx context.Context, webSeedProviderUrl *url.URL) (snaptype.WebSeedsFromProvider, error) { 654 request, err := http.NewRequest(http.MethodGet, webSeedProviderUrl.String(), nil) 655 if err != nil { 656 return nil, err 657 } 658 request = request.WithContext(ctx) 659 resp, err := http.DefaultClient.Do(request) 660 if err != nil { 661 return nil, err 662 } 663 defer resp.Body.Close() 664 response := snaptype.WebSeedsFromProvider{} 665 if err := toml.NewDecoder(resp.Body).Decode(&response); err != nil { 666 return nil, err 667 } 668 return response, nil 669 } 670 func (d *WebSeeds) readWebSeedsFile(webSeedProviderPath string) (snaptype.WebSeedsFromProvider, error) { 671 data, err := os.ReadFile(webSeedProviderPath) 672 if err != nil { 673 return nil, err 674 } 675 response := snaptype.WebSeedsFromProvider{} 676 if err := toml.Unmarshal(data, &response); err != nil { 677 return nil, err 678 } 679 return response, nil 680 } 681 682 func (d *WebSeeds) Discover(ctx context.Context, urls []*url.URL, files []string) { 683 list := make([]snaptype.WebSeedsFromProvider, len(urls)+len(files)) 684 for _, webSeedProviderURL := range urls { 685 select { 686 case <-ctx.Done(): 687 break 688 default: 689 } 690 response, err := d.callWebSeedsProvider(ctx, webSeedProviderURL) 691 if err != nil { // don't fail on error 692 log.Warn("[downloader] callWebSeedsProvider", "err", err, "url", webSeedProviderURL.EscapedPath()) 693 continue 694 } 695 list = append(list, response) 696 } 697 for _, webSeedFile := range files { 698 response, err := d.readWebSeedsFile(webSeedFile) 699 if err != nil { // don't fail on error 700 _, fileName := filepath.Split(webSeedFile) 701 log.Warn("[downloader] readWebSeedsFile", "err", err, "file", fileName) 702 continue 703 } 704 list = append(list, response) 705 } 706 d.SetByFileNames(snaptype.NewWebSeeds(list)) 707 }