reth_era_downloader/
fs.rs1use crate::{EraMeta, BLOCKS_PER_FILE};
2use alloy_primitives::{hex, hex::ToHexExt, BlockNumber};
3use eyre::{eyre, OptionExt};
4use futures_util::{stream, Stream};
5use reth_era::common::file_ops::EraFileType;
6use reth_fs_util as fs;
7use sha2::{Digest, Sha256};
8use std::{fmt::Debug, fs::DirEntry, io, io::BufRead, path::Path, str::FromStr};
9
10pub fn read_dir(
12 dir: impl AsRef<Path> + Send + Sync + 'static,
13 start_from: BlockNumber,
14) -> eyre::Result<impl Stream<Item = eyre::Result<EraLocalMeta>> + Send + Sync + 'static + Unpin> {
15 let mut checksums = None;
16
17 let entries = sorted_era_entries(
19 dir,
20 |ty| matches!(ty, EraFileType::Era1 | EraFileType::Ere),
21 |path| {
22 if path.file_name() == Some("checksums.txt".as_ref()) {
23 let reader = io::BufReader::new(fs::open(path)?);
24 checksums = Some(reader.lines());
25 }
26 Ok(())
27 },
28 )?;
29 let mut checksums = checksums.ok_or_eyre("Missing file `checksums.txt` in the `dir`")?;
30
31 let start_index = start_from as usize / BLOCKS_PER_FILE;
32 for _ in 0..start_index {
33 checksums.next().transpose()?.ok_or_eyre("Got less checksums than ERA files")?;
35 }
36
37 Ok(stream::iter(entries.into_iter().skip_while(move |(n, _)| *n < start_index).map(
38 move |(_, path)| {
39 let expected_checksum =
40 checksums.next().transpose()?.ok_or_eyre("Got less checksums than ERA files")?;
41 let expected_checksum = hex::decode(expected_checksum)?;
42
43 let mut hasher = Sha256::new();
44 let mut reader = io::BufReader::new(fs::open(&path)?);
45
46 io::copy(&mut reader, &mut hasher)?;
47 let actual_checksum = hasher.finalize().to_vec();
48
49 if actual_checksum != expected_checksum {
50 return Err(eyre!(
51 "Checksum mismatch, got: {}, expected: {}",
52 actual_checksum.encode_hex(),
53 expected_checksum.encode_hex()
54 ));
55 }
56
57 Ok(EraLocalMeta::new(path))
58 },
59 )))
60}
61
62pub fn read_era_dir(
69 dir: impl AsRef<Path> + Send + Sync + 'static,
70) -> eyre::Result<impl Stream<Item = eyre::Result<EraLocalMeta>> + Send + Sync + 'static + Unpin> {
71 let entries = sorted_era_entries(dir, |ty| ty == EraFileType::Era, |_| Ok(()))?;
72
73 Ok(stream::iter(entries.into_iter().map(|(_, path)| Ok(EraLocalMeta::new(path)))))
74}
75
76fn sorted_era_entries(
82 dir: impl AsRef<Path>,
83 accept: impl Fn(EraFileType) -> bool,
84 mut on_other: impl FnMut(&Path) -> eyre::Result<()>,
85) -> eyre::Result<Vec<(usize, Box<Path>)>> {
86 let mut entries = fs::read_dir(dir)?
87 .filter_map(|entry| parse_era_entry(entry, &accept, &mut on_other).transpose())
88 .collect::<eyre::Result<Vec<_>>>()?;
89
90 entries.sort_by_key(|(number, _)| *number);
91
92 Ok(entries)
93}
94
95fn parse_era_entry(
99 entry: io::Result<DirEntry>,
100 accept: &impl Fn(EraFileType) -> bool,
101 on_other: &mut impl FnMut(&Path) -> eyre::Result<()>,
102) -> eyre::Result<Option<(usize, Box<Path>)>> {
103 let path = entry?.path();
104
105 if let Some(name) = path.file_name().and_then(|name| name.to_str()) &&
106 EraFileType::from_filename(name).is_some_and(accept)
107 {
108 let parts = name.split('-').collect::<Vec<_>>();
109
110 if parts.len() >= 3 {
111 let number = usize::from_str(parts[1])?;
112
113 return Ok(Some((number, path.into_boxed_path())));
114 }
115 } else {
116 on_other(&path)?;
117 }
118
119 Ok(None)
120}
121
122#[derive(Debug)]
124pub struct EraLocalMeta {
125 path: Box<Path>,
126}
127
128impl EraLocalMeta {
129 const fn new(path: Box<Path>) -> Self {
130 Self { path }
131 }
132}
133
134impl<T: AsRef<Path>> PartialEq<T> for EraLocalMeta {
135 fn eq(&self, other: &T) -> bool {
136 self.as_ref().eq(other.as_ref())
137 }
138}
139
140impl AsRef<Path> for EraLocalMeta {
141 fn as_ref(&self) -> &Path {
142 self.path.as_ref()
143 }
144}
145
146impl EraMeta for EraLocalMeta {
147 fn mark_as_processed(&self) -> eyre::Result<()> {
149 Ok(())
150 }
151
152 fn path(&self) -> &Path {
153 &self.path
154 }
155}