reth_era_downloader/
fs.rs

1use crate::EraMeta;
2use alloy_primitives::{hex, hex::ToHexExt};
3use eyre::{eyre, OptionExt};
4use futures_util::{stream, Stream};
5use reth_fs_util as fs;
6use sha2::{Digest, Sha256};
7use std::{fmt::Debug, io, io::BufRead, path::Path, str::FromStr};
8
9/// Creates a new ordered asynchronous [`Stream`] of ERA1 files read from `dir`.
10pub fn read_dir(
11    dir: impl AsRef<Path> + Send + Sync + 'static,
12) -> eyre::Result<impl Stream<Item = eyre::Result<EraLocalMeta>> + Send + Sync + 'static + Unpin> {
13    let mut checksums = None;
14    let mut entries = fs::read_dir(dir)?
15        .filter_map(|entry| {
16            (|| {
17                let path = entry?.path();
18
19                if path.extension() == Some("era1".as_ref()) {
20                    if let Some(last) = path.components().next_back() {
21                        let str = last.as_os_str().to_string_lossy().to_string();
22                        let parts = str.split('-').collect::<Vec<_>>();
23
24                        if parts.len() == 3 {
25                            let number = usize::from_str(parts[1])?;
26
27                            return Ok(Some((number, path.into_boxed_path())));
28                        }
29                    }
30                }
31                if path.file_name() == Some("checksums.txt".as_ref()) {
32                    let file = fs::open(path)?;
33                    let reader = io::BufReader::new(file);
34                    let lines = reader.lines();
35                    checksums = Some(lines);
36                }
37
38                Ok(None)
39            })()
40            .transpose()
41        })
42        .collect::<eyre::Result<Vec<_>>>()?;
43    let mut checksums = checksums.ok_or_eyre("Missing file `checksums.txt` in the `dir`")?;
44
45    entries.sort_by(|(left, _), (right, _)| left.cmp(right));
46
47    Ok(stream::iter(entries.into_iter().map(move |(_, path)| {
48        let expected_checksum =
49            checksums.next().transpose()?.ok_or_eyre("Got less checksums than ERA files")?;
50        let expected_checksum = hex::decode(expected_checksum)?;
51
52        let mut hasher = Sha256::new();
53        let mut reader = io::BufReader::new(fs::open(&path)?);
54
55        io::copy(&mut reader, &mut hasher)?;
56        let actual_checksum = hasher.finalize().to_vec();
57
58        if actual_checksum != expected_checksum {
59            return Err(eyre!(
60                "Checksum mismatch, got: {}, expected: {}",
61                actual_checksum.encode_hex(),
62                expected_checksum.encode_hex()
63            ));
64        }
65
66        Ok(EraLocalMeta::new(path))
67    })))
68}
69
70/// Contains information about an ERA file that is on the local file-system and is read-only.
71#[derive(Debug)]
72pub struct EraLocalMeta {
73    path: Box<Path>,
74}
75
76impl EraLocalMeta {
77    const fn new(path: Box<Path>) -> Self {
78        Self { path }
79    }
80}
81
82impl<T: AsRef<Path>> PartialEq<T> for EraLocalMeta {
83    fn eq(&self, other: &T) -> bool {
84        self.as_ref().eq(other.as_ref())
85    }
86}
87
88impl AsRef<Path> for EraLocalMeta {
89    fn as_ref(&self) -> &Path {
90        self.path.as_ref()
91    }
92}
93
94impl EraMeta for EraLocalMeta {
95    /// A no-op.
96    fn mark_as_processed(self) -> eyre::Result<()> {
97        Ok(())
98    }
99}