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 }