diff --git a/cmd/snapshots/cmp/cmp.go b/cmd/snapshots/cmp/cmp.go index 94da963357d..774b96bffa7 100644 --- a/cmd/snapshots/cmp/cmp.go +++ b/cmd/snapshots/cmp/cmp.go @@ -12,6 +12,10 @@ import ( "time" "github.com/c2h5oh/datasize" + "github.com/ledgerwatch/log/v3" + "github.com/urfave/cli/v2" + "golang.org/x/sync/errgroup" + "github.com/ledgerwatch/erigon-lib/chain" "github.com/ledgerwatch/erigon-lib/common" "github.com/ledgerwatch/erigon-lib/downloader" @@ -24,9 +28,6 @@ import ( "github.com/ledgerwatch/erigon/params" "github.com/ledgerwatch/erigon/turbo/logging" "github.com/ledgerwatch/erigon/turbo/snapshotsync/freezeblocks" - "github.com/ledgerwatch/log/v3" - "github.com/urfave/cli/v2" - "golang.org/x/sync/errgroup" ) var Command = cli.Command{ @@ -140,7 +141,8 @@ func cmp(cliCtx *cli.Context) error { } if loc1.LType == sync.TorrentFs || loc2.LType == sync.TorrentFs { - torrentCli, err = sync.NewTorrentClient(cliCtx, chain) + config := sync.NewTorrentClientConfigFromCobra(cliCtx, chain) + torrentCli, err = sync.NewTorrentClient(config) if err != nil { return fmt.Errorf("can't create torrent: %w", err) } diff --git a/cmd/snapshots/copy/copy.go b/cmd/snapshots/copy/copy.go index 05f8ad5d37b..c3c6c46b93d 100644 --- a/cmd/snapshots/copy/copy.go +++ b/cmd/snapshots/copy/copy.go @@ -8,13 +8,14 @@ import ( "strconv" "strings" + "github.com/urfave/cli/v2" + "github.com/ledgerwatch/erigon-lib/downloader" "github.com/ledgerwatch/erigon-lib/downloader/snaptype" "github.com/ledgerwatch/erigon/cmd/snapshots/flags" "github.com/ledgerwatch/erigon/cmd/snapshots/sync" "github.com/ledgerwatch/erigon/cmd/utils" "github.com/ledgerwatch/erigon/turbo/logging" - "github.com/urfave/cli/v2" ) var ( @@ -126,7 +127,8 @@ func copy(cliCtx *cli.Context) error { switch src.LType { case sync.TorrentFs: - torrentCli, err = sync.NewTorrentClient(cliCtx, dst.Chain) + config := sync.NewTorrentClientConfigFromCobra(cliCtx, dst.Chain) + torrentCli, err = sync.NewTorrentClient(config) if err != nil { return fmt.Errorf("can't create torrent: %w", err) } diff --git a/cmd/snapshots/sync/sync.go b/cmd/snapshots/sync/sync.go index e5bd80c74bd..c010266c9a7 100644 --- a/cmd/snapshots/sync/sync.go +++ b/cmd/snapshots/sync/sync.go @@ -17,6 +17,11 @@ import ( "github.com/anacrolix/torrent/metainfo" "github.com/anacrolix/torrent/storage" "github.com/c2h5oh/datasize" + "github.com/ledgerwatch/log/v3" + "github.com/urfave/cli/v2" + "golang.org/x/exp/slices" + "golang.org/x/sync/errgroup" + "github.com/ledgerwatch/erigon-lib/chain/snapcfg" "github.com/ledgerwatch/erigon-lib/common" "github.com/ledgerwatch/erigon-lib/common/datadir" @@ -27,9 +32,6 @@ import ( "github.com/ledgerwatch/erigon/cmd/utils" "github.com/ledgerwatch/erigon/p2p/nat" "github.com/ledgerwatch/erigon/params" - "github.com/urfave/cli/v2" - "golang.org/x/exp/slices" - "golang.org/x/sync/errgroup" ) type LType int @@ -128,31 +130,80 @@ type TorrentClient struct { cfg *torrent.ClientConfig } -func NewTorrentClient(cliCtx *cli.Context, chain string) (*TorrentClient, error) { - logger := Logger(cliCtx.Context) - tempDir := TempDir(cliCtx.Context) +type CreateNewTorrentClientConfig struct { + Chain string + WebSeeds string + DownloadRate string + UploadRate string + Verbosity int + TorrentPort int + ConnsPerFile int + DisableIPv6 bool + DisableIPv4 bool + NatFlag string + Logger log.Logger + TempDir string +} + +func NewTorrentClientConfigFromCobra(cliCtx *cli.Context, chain string) CreateNewTorrentClientConfig { + return CreateNewTorrentClientConfig{ + Chain: chain, + WebSeeds: cliCtx.String(utils.WebSeedsFlag.Name), + DownloadRate: cliCtx.String(utils.TorrentDownloadRateFlag.Name), + UploadRate: cliCtx.String(utils.TorrentUploadRateFlag.Name), + Verbosity: cliCtx.Int(utils.TorrentVerbosityFlag.Name), + TorrentPort: cliCtx.Int(utils.TorrentPortFlag.Name), + ConnsPerFile: cliCtx.Int(utils.TorrentConnsPerFileFlag.Name), + DisableIPv6: cliCtx.Bool(utils.DisableIPV6.Name), + DisableIPv4: cliCtx.Bool(utils.DisableIPV4.Name), + NatFlag: cliCtx.String(utils.NATFlag.Name), + Logger: Logger(cliCtx.Context), + TempDir: TempDir(cliCtx.Context), + } +} + +func NewDefaultTorrentClientConfig(chain string, torrentDir string, logger log.Logger) CreateNewTorrentClientConfig { + return CreateNewTorrentClientConfig{ + Chain: chain, + WebSeeds: utils.WebSeedsFlag.Value, + DownloadRate: utils.TorrentDownloadRateFlag.Value, + UploadRate: utils.TorrentUploadRateFlag.Value, + Verbosity: utils.TorrentVerbosityFlag.Value, + TorrentPort: utils.TorrentPortFlag.Value, + ConnsPerFile: utils.TorrentConnsPerFileFlag.Value, + DisableIPv6: utils.DisableIPV6.Value, + DisableIPv4: utils.DisableIPV4.Value, + NatFlag: utils.NATFlag.Value, + Logger: logger, + TempDir: torrentDir, + } +} + +func NewTorrentClient(config CreateNewTorrentClientConfig) (*TorrentClient, error) { + logger := config.Logger + tempDir := config.TempDir - torrentDir := filepath.Join(tempDir, "torrents", chain) + torrentDir := filepath.Join(tempDir, "torrents", config.Chain) dirs := datadir.New(torrentDir) - webseedsList := common.CliString2Array(cliCtx.String(utils.WebSeedsFlag.Name)) + webseedsList := common.CliString2Array(config.WebSeeds) - if known, ok := snapcfg.KnownWebseeds[chain]; ok { + if known, ok := snapcfg.KnownWebseeds[config.Chain]; ok { webseedsList = append(webseedsList, known...) } var downloadRate, uploadRate datasize.ByteSize - if err := downloadRate.UnmarshalText([]byte(cliCtx.String(utils.TorrentDownloadRateFlag.Name))); err != nil { + if err := downloadRate.UnmarshalText([]byte(config.DownloadRate)); err != nil { return nil, err } - if err := uploadRate.UnmarshalText([]byte(cliCtx.String(utils.TorrentUploadRateFlag.Name))); err != nil { + if err := uploadRate.UnmarshalText([]byte(config.UploadRate)); err != nil { return nil, err } - logLevel, _, err := downloadercfg.Int2LogLevel(cliCtx.Int(utils.TorrentVerbosityFlag.Name)) + logLevel, _, err := downloadercfg.Int2LogLevel(config.Verbosity) if err != nil { return nil, err @@ -161,8 +212,8 @@ func NewTorrentClient(cliCtx *cli.Context, chain string) (*TorrentClient, error) version := "erigon: " + params.VersionWithCommit(params.GitCommit) cfg, err := downloadercfg.New(dirs, version, logLevel, downloadRate, uploadRate, - cliCtx.Int(utils.TorrentPortFlag.Name), - cliCtx.Int(utils.TorrentConnsPerFileFlag.Name), 0, nil, webseedsList, chain, true) + config.TorrentPort, + config.ConnsPerFile, 0, nil, webseedsList, config.Chain, true) if err != nil { return nil, err @@ -181,10 +232,10 @@ func NewTorrentClient(cliCtx *cli.Context, chain string) (*TorrentClient, error) cfg.ClientConfig.DataDir = torrentDir cfg.ClientConfig.PieceHashersPerTorrent = 32 * runtime.NumCPU() - cfg.ClientConfig.DisableIPv6 = cliCtx.Bool(utils.DisableIPV6.Name) - cfg.ClientConfig.DisableIPv4 = cliCtx.Bool(utils.DisableIPV4.Name) + cfg.ClientConfig.DisableIPv6 = config.DisableIPv6 + cfg.ClientConfig.DisableIPv4 = config.DisableIPv4 - natif, err := nat.Parse(utils.NATFlag.Value) + natif, err := nat.Parse(config.NatFlag) if err != nil { return nil, fmt.Errorf("invalid nat option %s: %w", utils.NATFlag.Value, err) diff --git a/cmd/snapshots/verify/verify.go b/cmd/snapshots/verify/verify.go index c19b52496c6..788c35b2c4e 100644 --- a/cmd/snapshots/verify/verify.go +++ b/cmd/snapshots/verify/verify.go @@ -6,12 +6,13 @@ import ( "path/filepath" "strconv" + "github.com/urfave/cli/v2" + "github.com/ledgerwatch/erigon-lib/downloader" "github.com/ledgerwatch/erigon-lib/downloader/snaptype" "github.com/ledgerwatch/erigon/cmd/snapshots/flags" "github.com/ledgerwatch/erigon/cmd/snapshots/sync" "github.com/ledgerwatch/erigon/cmd/utils" - "github.com/urfave/cli/v2" ) var ( @@ -99,7 +100,8 @@ func verify(cliCtx *cli.Context) error { switch dst.LType { case sync.TorrentFs: - torrentCli, err = sync.NewTorrentClient(cliCtx, dst.Chain) + config := sync.NewTorrentClientConfigFromCobra(cliCtx, dst.Chain) + torrentCli, err = sync.NewTorrentClient(config) if err != nil { return fmt.Errorf("can't create torrent: %w", err) } @@ -125,7 +127,8 @@ func verify(cliCtx *cli.Context) error { switch src.LType { case sync.TorrentFs: if torrentCli == nil { - torrentCli, err = sync.NewTorrentClient(cliCtx, dst.Chain) + config := sync.NewTorrentClientConfigFromCobra(cliCtx, dst.Chain) + torrentCli, err = sync.NewTorrentClient(config) if err != nil { return fmt.Errorf("can't create torrent: %w", err) } diff --git a/p2p/sentry/simulator/sentry_simulator.go b/p2p/sentry/simulator/sentry_simulator.go index 5060860497b..89424ec98ed 100644 --- a/p2p/sentry/simulator/sentry_simulator.go +++ b/p2p/sentry/simulator/sentry_simulator.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "path/filepath" "github.com/ledgerwatch/log/v3" "google.golang.org/protobuf/types/known/emptypb" @@ -14,6 +15,7 @@ import ( "github.com/ledgerwatch/erigon-lib/gointerfaces" sentry_if "github.com/ledgerwatch/erigon-lib/gointerfaces/sentry" "github.com/ledgerwatch/erigon-lib/gointerfaces/types" + "github.com/ledgerwatch/erigon/cmd/snapshots/sync" core_types "github.com/ledgerwatch/erigon/core/types" "github.com/ledgerwatch/erigon/crypto" "github.com/ledgerwatch/erigon/eth/ethconfig" @@ -35,7 +37,8 @@ type server struct { knownSnapshots *freezeblocks.RoSnapshots activeSnapshots *freezeblocks.RoSnapshots blockReader *freezeblocks.BlockReader - downloader *TorrentClient + downloader *sync.TorrentClient + chain string } func newPeer(name string, caps []p2p.Cap) (*p2p.Peer, error) { @@ -61,6 +64,7 @@ func NewSentry(ctx context.Context, chain string, snapshotLocation string, peerC } cfg := snapcfg.KnownCfg(chain) + torrentDir := filepath.Join(snapshotLocation, "torrents", chain) knownSnapshots := freezeblocks.NewRoSnapshots(ethconfig.BlocksFreezing{ Enabled: true, @@ -81,13 +85,14 @@ func NewSentry(ctx context.Context, chain string, snapshotLocation string, peerC Enabled: true, Produce: false, NoDownloader: true, - }, snapshotLocation, 0, logger) + }, torrentDir, 0, logger) if err := activeSnapshots.ReopenFolder(); err != nil { return nil, err } - downloader, err := NewTorrentClient(ctx, chain, snapshotLocation, logger) + config := sync.NewDefaultTorrentClientConfig(chain, snapshotLocation, logger) + downloader, err := sync.NewTorrentClient(config) if err != nil { return nil, err @@ -102,6 +107,7 @@ func NewSentry(ctx context.Context, chain string, snapshotLocation string, peerC blockReader: freezeblocks.NewBlockReader(activeSnapshots, nil), logger: logger, downloader: downloader, + chain: chain, } go func() { @@ -434,10 +440,11 @@ func (s *server) getHeaderByHash(ctx context.Context, hash common.Hash) (*core_t func (s *server) downloadHeaders(ctx context.Context, header *freezeblocks.Segment) error { fileName := snaptype.SegmentFileName(0, header.From(), header.To(), snaptype.Enums.Headers) + session := sync.NewTorrentSession(s.downloader, s.chain) s.logger.Info(fmt.Sprintf("Downloading %s", fileName)) - err := s.downloader.Download(ctx, fileName) + err := session.Download(ctx, fileName) if err != nil { return fmt.Errorf("can't download %s: %w", fileName, err) @@ -445,8 +452,8 @@ func (s *server) downloadHeaders(ctx context.Context, header *freezeblocks.Segme s.logger.Info(fmt.Sprintf("Indexing %s", fileName)) - info, _, _ := snaptype.ParseFileName(s.downloader.LocalFsRoot(), fileName) + info, _, _ := snaptype.ParseFileName(session.LocalFsRoot(), fileName) - salt := freezeblocks.GetIndicesSalt(s.downloader.LocalFsRoot()) - return freezeblocks.HeadersIdx(ctx, info, salt, s.downloader.LocalFsRoot(), nil, log.LvlDebug, s.logger) + salt := freezeblocks.GetIndicesSalt(session.LocalFsRoot()) + return freezeblocks.HeadersIdx(ctx, info, salt, session.LocalFsRoot(), nil, log.LvlDebug, s.logger) } diff --git a/p2p/sentry/simulator/syncutil.go b/p2p/sentry/simulator/syncutil.go deleted file mode 100644 index 684f2548f99..00000000000 --- a/p2p/sentry/simulator/syncutil.go +++ /dev/null @@ -1,208 +0,0 @@ -package simulator - -import ( - "context" - "fmt" - "io/fs" - "os" - "path/filepath" - "runtime" - - "github.com/anacrolix/torrent" - "github.com/anacrolix/torrent/metainfo" - "github.com/anacrolix/torrent/storage" - "github.com/c2h5oh/datasize" - "github.com/ledgerwatch/log/v3" - "golang.org/x/sync/errgroup" - - "github.com/ledgerwatch/erigon/cmd/downloader/downloadernat" - "github.com/ledgerwatch/erigon/cmd/utils" - "github.com/ledgerwatch/erigon/p2p/nat" - "github.com/ledgerwatch/erigon/params" - - "github.com/ledgerwatch/erigon-lib/chain/snapcfg" - "github.com/ledgerwatch/erigon-lib/common" - "github.com/ledgerwatch/erigon-lib/common/datadir" - "github.com/ledgerwatch/erigon-lib/downloader" - "github.com/ledgerwatch/erigon-lib/downloader/downloadercfg" - "github.com/ledgerwatch/erigon-lib/downloader/snaptype" -) - -// The code in this file is taken from cmd/snapshots - which is yet to be merged -// to devel - once that is done this file can be removed - -type TorrentClient struct { - *torrent.Client - cfg *torrent.ClientConfig - items map[string]snapcfg.PreverifiedItem -} - -func NewTorrentClient(ctx context.Context, chain string, torrentDir string, logger log.Logger) (*TorrentClient, error) { - - relativeDataDir := torrentDir - if torrentDir != "" { - var err error - absdatadir, err := filepath.Abs(torrentDir) - if err != nil { - panic(err) - } - torrentDir = absdatadir - } - - dirs := datadir.Dirs{ - RelativeDataDir: relativeDataDir, - DataDir: torrentDir, - Snap: torrentDir, - } - - webseedsList := common.CliString2Array(utils.WebSeedsFlag.Value) - - if known, ok := snapcfg.KnownWebseeds[chain]; ok { - webseedsList = append(webseedsList, known...) - } - - var downloadRate, uploadRate datasize.ByteSize - - if err := downloadRate.UnmarshalText([]byte(utils.TorrentDownloadRateFlag.Value)); err != nil { - return nil, err - } - - if err := uploadRate.UnmarshalText([]byte(utils.TorrentUploadRateFlag.Value)); err != nil { - return nil, err - } - - logLevel, _, err := downloadercfg.Int2LogLevel(utils.TorrentVerbosityFlag.Value) - - if err != nil { - return nil, err - } - - version := "erigon: " + params.VersionWithCommit(params.GitCommit) - - cfg, err := downloadercfg.New(dirs, version, logLevel, downloadRate, uploadRate, - utils.TorrentPortFlag.Value, utils.TorrentConnsPerFileFlag.Value, 0, nil, webseedsList, chain, false) - - if err != nil { - return nil, err - } - - if err := os.MkdirAll(torrentDir, 0755); err != nil { - return nil, err - } - - cfg.ClientConfig.DataDir = torrentDir - - cfg.ClientConfig.PieceHashersPerTorrent = 32 * runtime.NumCPU() - cfg.ClientConfig.DisableIPv6 = utils.DisableIPV6.Value - cfg.ClientConfig.DisableIPv4 = utils.DisableIPV4.Value - - natif, err := nat.Parse(utils.NATFlag.Value) - - if err != nil { - return nil, fmt.Errorf("invalid nat option %s: %w", utils.NATFlag.Value, err) - } - - downloadernat.DoNat(natif, cfg.ClientConfig, logger) - - cfg.ClientConfig.DefaultStorage = storage.NewMMap(torrentDir) - - cli, err := torrent.NewClient(cfg.ClientConfig) - - if err != nil { - return nil, fmt.Errorf("can't create torrent client: %w", err) - } - - items := map[string]snapcfg.PreverifiedItem{} - for _, it := range snapcfg.KnownCfg(chain).Preverified { - items[it.Name] = it - } - - return &TorrentClient{cli, cfg.ClientConfig, items}, nil -} - -func (s *TorrentClient) LocalFsRoot() string { - return s.cfg.DataDir -} - -func (s *TorrentClient) Close() error { - if closer, ok := s.cfg.DefaultStorage.(interface{ Close() error }); ok { - err := closer.Close() - if err != nil { - return err - } - } - - return nil -} - -func (s *TorrentClient) Download(ctx context.Context, files ...string) error { - g, ctx := errgroup.WithContext(ctx) - g.SetLimit(len(files)) - - for _, f := range files { - file := f - - g.Go(func() error { - it, ok := s.items[file] - - if !ok { - return fs.ErrNotExist - } - - t, err := func() (*torrent.Torrent, error) { - infoHash := snaptype.Hex2InfoHash(it.Hash) - - for _, t := range s.Torrents() { - if t.Name() == file { - return t, nil - } - } - - mi := &metainfo.MetaInfo{AnnounceList: downloader.Trackers} - magnet := mi.Magnet(&infoHash, &metainfo.Info{Name: file}) - spec, err := torrent.TorrentSpecFromMagnetUri(magnet.String()) - - if err != nil { - return nil, err - } - - spec.DisallowDataDownload = true - - t, _, err := s.AddTorrentSpec(spec) - if err != nil { - return nil, err - } - - return t, nil - }() - - if err != nil { - return err - } - - select { - case <-ctx.Done(): - return ctx.Err() - case <-t.GotInfo(): - } - - if !t.Complete.Bool() { - t.AllowDataDownload() - t.DownloadAll() - select { - case <-ctx.Done(): - return ctx.Err() - case <-t.Complete.On(): - } - } - - closed := t.Closed() - t.Drop() - <-closed - - return nil - }) - } - - return g.Wait() -} diff --git a/polygon/heimdall/simulator/heimdall_simulator.go b/polygon/heimdall/simulator/heimdall_simulator.go index b82c41febfc..b2bdf232f8e 100644 --- a/polygon/heimdall/simulator/heimdall_simulator.go +++ b/polygon/heimdall/simulator/heimdall_simulator.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "path/filepath" "time" "github.com/ledgerwatch/log/v3" @@ -14,8 +15,8 @@ import ( "github.com/ledgerwatch/erigon-lib/chain/snapcfg" "github.com/ledgerwatch/erigon-lib/common/background" "github.com/ledgerwatch/erigon-lib/downloader/snaptype" + "github.com/ledgerwatch/erigon/cmd/snapshots/sync" "github.com/ledgerwatch/erigon/eth/ethconfig" - "github.com/ledgerwatch/erigon/p2p/sentry/simulator" "github.com/ledgerwatch/erigon/polygon/heimdall" "github.com/ledgerwatch/erigon/turbo/snapshotsync/freezeblocks" ) @@ -26,7 +27,8 @@ type HeimdallSimulator struct { activeBorSnapshots *freezeblocks.BorRoSnapshots blockReader *freezeblocks.BlockReader logger log.Logger - downloader *simulator.TorrentClient + downloader *sync.TorrentClient + chain string lastDownloadedBlockNumber uint64 } @@ -35,8 +37,9 @@ type IndexFnType func(context.Context, snaptype.FileInfo, uint32, string, *backg func NewHeimdall(ctx context.Context, chain string, snapshotLocation string, logger log.Logger) (HeimdallSimulator, error) { cfg := snapcfg.KnownCfg(chain) + torrentDir := filepath.Join(snapshotLocation, "torrents", chain) - knownBorSnapshots := freezeblocks.NewBorRoSnapshots(ethconfig.Defaults.Snapshot, snapshotLocation, 0, logger) + knownBorSnapshots := freezeblocks.NewBorRoSnapshots(ethconfig.Defaults.Snapshot, torrentDir, 0, logger) files := make([]string, 0, len(cfg.Preverified)) @@ -49,13 +52,14 @@ func NewHeimdall(ctx context.Context, chain string, snapshotLocation string, log return HeimdallSimulator{}, err } - activeBorSnapshots := freezeblocks.NewBorRoSnapshots(ethconfig.Defaults.Snapshot, snapshotLocation, 0, logger) + activeBorSnapshots := freezeblocks.NewBorRoSnapshots(ethconfig.Defaults.Snapshot, torrentDir, 0, logger) if err := activeBorSnapshots.ReopenFolder(); err != nil { return HeimdallSimulator{}, err } - downloader, err := simulator.NewTorrentClient(ctx, chain, snapshotLocation, logger) + config := sync.NewDefaultTorrentClientConfig(chain, snapshotLocation, logger) + downloader, err := sync.NewTorrentClient(config) if err != nil { return HeimdallSimulator{}, err } @@ -67,6 +71,7 @@ func NewHeimdall(ctx context.Context, chain string, snapshotLocation string, log blockReader: freezeblocks.NewBlockReader(nil, activeBorSnapshots), logger: logger, downloader: downloader, + chain: chain, lastDownloadedBlockNumber: 0, } @@ -169,18 +174,20 @@ func (h *HeimdallSimulator) downloadData(ctx context.Context, spans *freezeblock return h.activeBorSnapshots.ReopenSegments([]snaptype.Type{sType}, true) } + session := sync.NewTorrentSession(h.downloader, h.chain) + h.logger.Info(fmt.Sprintf("Downloading %s", fileName)) - err := h.downloader.Download(ctx, fileName) + err := session.Download(ctx, fileName) if err != nil { return fmt.Errorf("can't download %s: %w", fileName, err) } h.logger.Info(fmt.Sprintf("Indexing %s", fileName)) - info, _, _ := snaptype.ParseFileName(h.downloader.LocalFsRoot(), fileName) + info, _, _ := snaptype.ParseFileName(session.LocalFsRoot(), fileName) - err = indexFn(ctx, info, h.activeBorSnapshots.Salt, h.downloader.LocalFsRoot(), nil, log.LvlWarn, h.logger) + err = indexFn(ctx, info, h.activeBorSnapshots.Salt, session.LocalFsRoot(), nil, log.LvlWarn, h.logger) if err != nil { return fmt.Errorf("can't download %s: %w", fileName, err) }