github.com/ledgerwatch/erigon-lib@v1.0.0/downloader/util.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  	"fmt"
    22  	"net"
    23  	"os"
    24  	"path/filepath"
    25  	"regexp"
    26  	"runtime"
    27  	"strconv"
    28  	"sync/atomic"
    29  	"time"
    30  
    31  	"github.com/anacrolix/torrent"
    32  	"github.com/anacrolix/torrent/bencode"
    33  	"github.com/anacrolix/torrent/metainfo"
    34  	common2 "github.com/ledgerwatch/erigon-lib/common"
    35  	"github.com/ledgerwatch/erigon-lib/common/cmp"
    36  	"github.com/ledgerwatch/erigon-lib/common/dbg"
    37  	dir2 "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/log/v3"
    42  	"golang.org/x/sync/errgroup"
    43  )
    44  
    45  // udpOrHttpTrackers - torrent library spawning several goroutines and producing many requests for each tracker. So we limit amout of trackers by 7
    46  var udpOrHttpTrackers = []string{
    47  	"udp://tracker.opentrackr.org:1337/announce",
    48  	"udp://9.rarbg.com:2810/announce",
    49  	"udp://tracker.openbittorrent.com:6969/announce",
    50  	"http://tracker.openbittorrent.com:80/announce",
    51  	"udp://opentracker.i2p.rocks:6969/announce",
    52  	"https://opentracker.i2p.rocks:443/announce",
    53  	"udp://tracker.torrent.eu.org:451/announce",
    54  	"udp://tracker.moeking.me:6969/announce",
    55  }
    56  
    57  // nolint
    58  var websocketTrackers = []string{
    59  	"wss://tracker.btorrent.xyz",
    60  }
    61  
    62  // Trackers - break down by priority tier
    63  var Trackers = [][]string{
    64  	udpOrHttpTrackers,
    65  	//websocketTrackers // TODO: Ws protocol producing too many errors and flooding logs. But it's also very fast and reactive.
    66  }
    67  
    68  func AllTorrentPaths(dir string) ([]string, error) {
    69  	files, err := AllTorrentFiles(dir)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  	histDir := filepath.Join(dir, "history")
    74  	files2, err := AllTorrentFiles(histDir)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  	res := make([]string, 0, len(files)+len(files2))
    79  	for _, f := range files {
    80  		torrentFilePath := filepath.Join(dir, f)
    81  		res = append(res, torrentFilePath)
    82  	}
    83  	for _, f := range files2 {
    84  		torrentFilePath := filepath.Join(histDir, f)
    85  		res = append(res, torrentFilePath)
    86  	}
    87  	return res, nil
    88  }
    89  
    90  func AllTorrentFiles(dir string) ([]string, error) {
    91  	files, err := os.ReadDir(dir)
    92  	if err != nil {
    93  		return nil, err
    94  	}
    95  	res := make([]string, 0, len(files))
    96  	for _, f := range files {
    97  		if filepath.Ext(f.Name()) != ".torrent" { // filter out only compressed files
    98  			continue
    99  		}
   100  		fileInfo, err := f.Info()
   101  		if err != nil {
   102  			return nil, err
   103  		}
   104  		if fileInfo.Size() == 0 {
   105  			continue
   106  		}
   107  		res = append(res, f.Name())
   108  	}
   109  	return res, nil
   110  }
   111  
   112  func seedableSegmentFiles(dir string) ([]string, error) {
   113  	files, err := os.ReadDir(dir)
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  	res := make([]string, 0, len(files))
   118  	for _, f := range files {
   119  		if f.IsDir() {
   120  			continue
   121  		}
   122  		if !f.Type().IsRegular() {
   123  			continue
   124  		}
   125  		if !snaptype.IsCorrectFileName(f.Name()) {
   126  			continue
   127  		}
   128  		if filepath.Ext(f.Name()) != ".seg" { // filter out only compressed files
   129  			continue
   130  		}
   131  		ff, ok := snaptype.ParseFileName(dir, f.Name())
   132  		if !ok {
   133  			continue
   134  		}
   135  		if !ff.Seedable() {
   136  			continue
   137  		}
   138  		res = append(res, f.Name())
   139  	}
   140  	return res, nil
   141  }
   142  
   143  var historyFileRegex = regexp.MustCompile("^([[:lower:]]+).([0-9]+)-([0-9]+).(.*)$")
   144  
   145  func seedableHistorySnapshots(dir, subDir string) ([]string, error) {
   146  	l, err := seedableSnapshotsBySubDir(dir, "history")
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  	l2, err := seedableSnapshotsBySubDir(dir, "warm")
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  	return append(l, l2...), nil
   155  }
   156  
   157  func seedableSnapshotsBySubDir(dir, subDir string) ([]string, error) {
   158  	historyDir := filepath.Join(dir, subDir)
   159  	dir2.MustExist(historyDir)
   160  	files, err := os.ReadDir(historyDir)
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  	res := make([]string, 0, len(files))
   165  	for _, f := range files {
   166  		if f.IsDir() {
   167  			continue
   168  		}
   169  		if !f.Type().IsRegular() {
   170  			continue
   171  		}
   172  		ext := filepath.Ext(f.Name())
   173  		if ext != ".v" && ext != ".ef" { // filter out only compressed files
   174  			continue
   175  		}
   176  
   177  		subs := historyFileRegex.FindStringSubmatch(f.Name())
   178  		if len(subs) != 5 {
   179  			continue
   180  		}
   181  		// Check that it's seedable
   182  		from, err := strconv.ParseUint(subs[2], 10, 64)
   183  		if err != nil {
   184  			return nil, fmt.Errorf("ParseFileName: %w", err)
   185  		}
   186  		to, err := strconv.ParseUint(subs[3], 10, 64)
   187  		if err != nil {
   188  			return nil, fmt.Errorf("ParseFileName: %w", err)
   189  		}
   190  		if (to-from)%snaptype.Erigon3SeedableSteps != 0 {
   191  			continue
   192  		}
   193  		res = append(res, filepath.Join(subDir, f.Name()))
   194  	}
   195  	return res, nil
   196  }
   197  
   198  func ensureCantLeaveDir(fName, root string) (string, error) {
   199  	if filepath.IsAbs(fName) {
   200  		newFName, err := filepath.Rel(root, fName)
   201  		if err != nil {
   202  			return fName, err
   203  		}
   204  		if !IsLocal(newFName) {
   205  			return fName, fmt.Errorf("file=%s, is outside of snapshots dir", fName)
   206  		}
   207  		fName = newFName
   208  	}
   209  	if !IsLocal(fName) {
   210  		return fName, fmt.Errorf("relative paths are not allowed: %s", fName)
   211  	}
   212  	return fName, nil
   213  }
   214  
   215  func BuildTorrentIfNeed(ctx context.Context, fName, root string) (torrentFilePath string, err error) {
   216  	select {
   217  	case <-ctx.Done():
   218  		return "", ctx.Err()
   219  	default:
   220  	}
   221  	fName, err = ensureCantLeaveDir(fName, root)
   222  	if err != nil {
   223  		return "", err
   224  	}
   225  
   226  	fPath := filepath.Join(root, fName)
   227  	if dir2.FileExist(fPath + ".torrent") {
   228  		return
   229  	}
   230  	if !dir2.FileExist(fPath) {
   231  		return
   232  	}
   233  
   234  	info := &metainfo.Info{PieceLength: downloadercfg.DefaultPieceSize, Name: fName}
   235  	if err := info.BuildFromFilePath(fPath); err != nil {
   236  		return "", fmt.Errorf("createTorrentFileFromSegment: %w", err)
   237  	}
   238  	info.Name = fName
   239  
   240  	return fPath + ".torrent", CreateTorrentFileFromInfo(root, info, nil)
   241  }
   242  
   243  // BuildTorrentFilesIfNeed - create .torrent files from .seg files (big IO) - if .seg files were added manually
   244  func BuildTorrentFilesIfNeed(ctx context.Context, snapDir string) ([]string, error) {
   245  	logEvery := time.NewTicker(20 * time.Second)
   246  	defer logEvery.Stop()
   247  
   248  	files, err := seedableFiles(snapDir)
   249  	if err != nil {
   250  		return nil, err
   251  	}
   252  
   253  	g, ctx := errgroup.WithContext(ctx)
   254  	g.SetLimit(cmp.Max(1, runtime.GOMAXPROCS(-1)-1) * 4)
   255  	var i atomic.Int32
   256  
   257  	for _, file := range files {
   258  		file := file
   259  		g.Go(func() error {
   260  			defer i.Add(1)
   261  			if _, err := BuildTorrentIfNeed(ctx, file, snapDir); err != nil {
   262  				return err
   263  			}
   264  			return nil
   265  		})
   266  	}
   267  
   268  	var m runtime.MemStats
   269  Loop:
   270  	for int(i.Load()) < len(files) {
   271  		select {
   272  		case <-ctx.Done():
   273  			break Loop // g.Wait() will return right error
   274  		case <-logEvery.C:
   275  			dbg.ReadMemStats(&m)
   276  			log.Info("[snapshots] Creating .torrent files", "progress", fmt.Sprintf("%d/%d", i.Load(), len(files)), "alloc", common2.ByteCount(m.Alloc), "sys", common2.ByteCount(m.Sys))
   277  		}
   278  	}
   279  	if err := g.Wait(); err != nil {
   280  		return nil, err
   281  	}
   282  	return files, nil
   283  }
   284  
   285  func CreateTorrentFileIfNotExists(root string, info *metainfo.Info, mi *metainfo.MetaInfo) error {
   286  	fPath := filepath.Join(root, info.Name)
   287  	if dir2.FileExist(fPath + ".torrent") {
   288  		return nil
   289  	}
   290  	if err := CreateTorrentFileFromInfo(root, info, mi); err != nil {
   291  		return err
   292  	}
   293  	return nil
   294  }
   295  
   296  func CreateMetaInfo(info *metainfo.Info, mi *metainfo.MetaInfo) (*metainfo.MetaInfo, error) {
   297  	if mi == nil {
   298  		infoBytes, err := bencode.Marshal(info)
   299  		if err != nil {
   300  			return nil, err
   301  		}
   302  		mi = &metainfo.MetaInfo{
   303  			CreationDate: time.Now().Unix(),
   304  			CreatedBy:    "erigon",
   305  			InfoBytes:    infoBytes,
   306  			AnnounceList: Trackers,
   307  		}
   308  	} else {
   309  		mi.AnnounceList = Trackers
   310  	}
   311  	return mi, nil
   312  }
   313  func CreateTorrentFromMetaInfo(root string, info *metainfo.Info, mi *metainfo.MetaInfo) error {
   314  	torrentFileName := filepath.Join(root, info.Name+".torrent")
   315  	file, err := os.Create(torrentFileName)
   316  	if err != nil {
   317  		return err
   318  	}
   319  	defer file.Close()
   320  	if err := mi.Write(file); err != nil {
   321  		return err
   322  	}
   323  	file.Sync()
   324  	return nil
   325  }
   326  func CreateTorrentFileFromInfo(root string, info *metainfo.Info, mi *metainfo.MetaInfo) (err error) {
   327  	mi, err = CreateMetaInfo(info, mi)
   328  	if err != nil {
   329  		return err
   330  	}
   331  	return CreateTorrentFromMetaInfo(root, info, mi)
   332  }
   333  
   334  func AddTorrentFiles(snapDir string, torrentClient *torrent.Client) error {
   335  	files, err := allTorrentFiles(snapDir)
   336  	if err != nil {
   337  		return err
   338  	}
   339  	for _, ts := range files {
   340  		_, err := addTorrentFile(ts, torrentClient)
   341  		if err != nil {
   342  			return err
   343  		}
   344  	}
   345  
   346  	return nil
   347  }
   348  
   349  func allTorrentFiles(snapDir string) (res []*torrent.TorrentSpec, err error) {
   350  	res, err = torrentInDir(snapDir)
   351  	if err != nil {
   352  		return nil, err
   353  	}
   354  	res2, err := torrentInDir(filepath.Join(snapDir, "history"))
   355  	if err != nil {
   356  		return nil, err
   357  	}
   358  	res = append(res, res2...)
   359  	res2, err = torrentInDir(filepath.Join(snapDir, "warm"))
   360  	if err != nil {
   361  		return nil, err
   362  	}
   363  	res = append(res, res2...)
   364  	return res, nil
   365  }
   366  func torrentInDir(snapDir string) (res []*torrent.TorrentSpec, err error) {
   367  	files, err := os.ReadDir(snapDir)
   368  	if err != nil {
   369  		return nil, err
   370  	}
   371  	for _, f := range files {
   372  		if f.IsDir() || !f.Type().IsRegular() {
   373  			continue
   374  		}
   375  		if filepath.Ext(f.Name()) != ".torrent" { // filter out only compressed files
   376  			continue
   377  		}
   378  
   379  		a, err := loadTorrent(filepath.Join(snapDir, f.Name()))
   380  		if err != nil {
   381  			return nil, err
   382  		}
   383  		res = append(res, a)
   384  	}
   385  	return res, nil
   386  }
   387  
   388  func loadTorrent(torrentFilePath string) (*torrent.TorrentSpec, error) {
   389  	mi, err := metainfo.LoadFromFile(torrentFilePath)
   390  	if err != nil {
   391  		return nil, fmt.Errorf("LoadFromFile: %w, file=%s", err, torrentFilePath)
   392  	}
   393  	mi.AnnounceList = Trackers
   394  	return torrent.TorrentSpecFromMetaInfoErr(mi)
   395  }
   396  
   397  // addTorrentFile - adding .torrent file to torrentClient (and checking their hashes), if .torrent file
   398  // added first time - pieces verification process will start (disk IO heavy) - Progress
   399  // kept in `piece completion storage` (surviving reboot). Once it done - no disk IO needed again.
   400  // Don't need call torrent.VerifyData manually
   401  func addTorrentFile(ts *torrent.TorrentSpec, torrentClient *torrent.Client) (*torrent.Torrent, error) {
   402  	if _, ok := torrentClient.Torrent(ts.InfoHash); !ok { // can set ChunkSize only for new torrents
   403  		ts.ChunkSize = downloadercfg.DefaultNetworkChunkSize
   404  	} else {
   405  		ts.ChunkSize = 0
   406  	}
   407  
   408  	ts.DisallowDataDownload = true
   409  	t, _, err := torrentClient.AddTorrentSpec(ts)
   410  	if err != nil {
   411  		return nil, fmt.Errorf("addTorrentFile %s: %w", ts.DisplayName, err)
   412  	}
   413  
   414  	t.DisallowDataDownload()
   415  	t.AllowDataUpload()
   416  	return t, nil
   417  }
   418  
   419  var ErrSkip = fmt.Errorf("skip")
   420  
   421  func portMustBeTCPAndUDPOpen(port int) error {
   422  	tcpAddr := &net.TCPAddr{
   423  		Port: port,
   424  		IP:   net.ParseIP("127.0.0.1"),
   425  	}
   426  	ln, err := net.ListenTCP("tcp", tcpAddr)
   427  	if err != nil {
   428  		return fmt.Errorf("please open port %d for TCP and UDP. %w", port, err)
   429  	}
   430  	_ = ln.Close()
   431  	udpAddr := &net.UDPAddr{
   432  		Port: port,
   433  		IP:   net.ParseIP("127.0.0.1"),
   434  	}
   435  	ser, err := net.ListenUDP("udp", udpAddr)
   436  	if err != nil {
   437  		return fmt.Errorf("please open port %d for UDP. %w", port, err)
   438  	}
   439  	_ = ser.Close()
   440  	return nil
   441  }
   442  
   443  func savePeerID(db kv.RwDB, peerID torrent.PeerID) error {
   444  	return db.Update(context.Background(), func(tx kv.RwTx) error {
   445  		return tx.Put(kv.BittorrentInfo, []byte(kv.BittorrentPeerID), peerID[:])
   446  	})
   447  }
   448  
   449  func readPeerID(db kv.RoDB) (peerID []byte, err error) {
   450  	if err = db.View(context.Background(), func(tx kv.Tx) error {
   451  		peerIDFromDB, err := tx.GetOne(kv.BittorrentInfo, []byte(kv.BittorrentPeerID))
   452  		if err != nil {
   453  			return fmt.Errorf("get peer id: %w", err)
   454  		}
   455  		peerID = common2.Copy(peerIDFromDB)
   456  		return nil
   457  	}); err != nil {
   458  		return nil, err
   459  	}
   460  	return peerID, nil
   461  }
   462  
   463  // Deprecated: use `filepath.IsLocal` after drop go1.19 support
   464  func IsLocal(path string) bool {
   465  	return isLocal(path)
   466  }