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  }