mithril_common/digesters/
ledger_file.rs

1use std::{
2    cmp::Ordering,
3    path::{Path, PathBuf},
4};
5use thiserror::Error;
6use walkdir::WalkDir;
7
8use crate::digesters::LEDGER_DIR;
9use crate::entities::SlotNumber;
10
11/// Walk the given path and return the first directory named "ledger" it finds
12fn find_ledger_dir(path_to_walk: &Path) -> Option<PathBuf> {
13    WalkDir::new(path_to_walk)
14        .into_iter()
15        .filter_entry(|e| e.file_type().is_dir())
16        .filter_map(|e| e.ok())
17        .find(|f| f.file_name() == LEDGER_DIR)
18        .map(|e| e.into_path())
19}
20
21/// Represent an ledger file in a Cardano node database directory
22#[derive(Debug, PartialEq, Eq, Clone)]
23pub struct LedgerFile {
24    /// The path to the ledger file
25    pub path: PathBuf,
26
27    /// The ledger file slot number
28    pub slot_number: SlotNumber,
29
30    /// The filename
31    pub filename: String,
32}
33
34/// [LedgerFile::list_all_in_dir] related errors.
35#[derive(Error, Debug)]
36pub enum LedgerFileListingError {
37    /// Raised when the "ledger" folder could not be found in a file structure.
38    #[error("Couldn't find the 'ledger' folder in '{0:?}'")]
39    MissingLedgerFolder(PathBuf),
40}
41
42impl LedgerFile {
43    /// LedgerFile factory
44    pub fn new<T: Into<String>>(path: PathBuf, slot_number: SlotNumber, filename: T) -> Self {
45        Self {
46            path,
47            slot_number,
48            filename: filename.into(),
49        }
50    }
51
52    /// Convert a path to a LedgerFile if it satisfies the LedgerFile constraints.
53    ///
54    /// The constraints are: the path must be a file, the filename should only contain a number (no
55    /// extension).
56    pub fn from_path(path: &Path) -> Option<LedgerFile> {
57        path.file_name()
58            .map(|name| name.to_string_lossy())
59            .and_then(|filename| {
60                filename
61                    .parse::<u64>()
62                    .map(|number| Self::new(path.to_path_buf(), SlotNumber(number), filename))
63                    .ok()
64            })
65    }
66
67    /// List all [`LedgerFile`] in a given directory.
68    pub fn list_all_in_dir(dir: &Path) -> Result<Vec<LedgerFile>, LedgerFileListingError> {
69        let ledger_dir = find_ledger_dir(dir).ok_or(
70            LedgerFileListingError::MissingLedgerFolder(dir.to_path_buf()),
71        )?;
72        let mut files: Vec<LedgerFile> = vec![];
73
74        for path in WalkDir::new(ledger_dir)
75            .min_depth(1)
76            .max_depth(1)
77            .into_iter()
78            .filter_entry(|e| e.file_type().is_file())
79            .filter_map(|file| file.ok())
80        {
81            if let Some(ledger_file) = LedgerFile::from_path(path.path()) {
82                files.push(ledger_file);
83            }
84        }
85        files.sort();
86
87        Ok(files)
88    }
89}
90
91impl PartialOrd for LedgerFile {
92    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
93        Some(self.cmp(other))
94    }
95}
96
97impl Ord for LedgerFile {
98    fn cmp(&self, other: &Self) -> Ordering {
99        self.slot_number
100            .cmp(&other.slot_number)
101            .then(self.path.cmp(&other.path))
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use std::fs::File;
108    use std::io::prelude::*;
109    use std::path::{Path, PathBuf};
110
111    use crate::test_utils::TempDir;
112
113    use super::LedgerFile;
114
115    fn get_test_dir(subdir_name: &str) -> PathBuf {
116        TempDir::create("ledger_file", subdir_name)
117    }
118
119    fn create_fake_files(parent_dir: &Path, child_filenames: &[&str]) {
120        for filename in child_filenames {
121            let file = parent_dir.join(Path::new(filename));
122            let mut source_file = File::create(file).unwrap();
123            write!(source_file, "This is a test file named '{filename}'").unwrap();
124        }
125    }
126
127    fn extract_filenames(ledger_files: &[LedgerFile]) -> Vec<String> {
128        ledger_files
129            .iter()
130            .map(|i| i.path.file_name().unwrap().to_str().unwrap().to_owned())
131            .collect()
132    }
133
134    #[test]
135    fn list_all_ledger_file_fail_if_not_in_ledger_dir() {
136        let target_dir = get_test_dir("list_all_ledger_file_fail_if_not_in_ledger_dir/invalid");
137        let entries = vec![];
138        create_fake_files(&target_dir, &entries);
139
140        LedgerFile::list_all_in_dir(target_dir.parent().unwrap())
141            .expect_err("LedgerFile::list_all_in_dir should have Failed");
142    }
143
144    #[test]
145    fn list_all_ledger_file_should_works_in_a_empty_folder() {
146        let target_dir = get_test_dir("list_all_ledger_file_should_works_in_a_empty_folder/ledger");
147        let result = LedgerFile::list_all_in_dir(target_dir.parent().unwrap())
148            .expect("LedgerFile::list_all_in_dir should work in a empty folder");
149
150        assert!(result.is_empty());
151    }
152
153    #[test]
154    fn list_all_ledger_file_order_should_be_deterministic() {
155        let target_dir = get_test_dir("list_all_ledger_file_order_should_be_deterministic/ledger");
156        let entries = vec!["424", "123", "124", "00125", "21", "223", "0423"];
157        create_fake_files(&target_dir, &entries);
158        let ledger_files = LedgerFile::list_all_in_dir(target_dir.parent().unwrap())
159            .expect("LedgerFile::list_all_in_dir Failed");
160
161        assert_eq!(
162            vec!["21", "123", "124", "00125", "223", "0423", "424"],
163            extract_filenames(&ledger_files)
164        );
165    }
166
167    #[test]
168    fn list_all_ledger_file_should_work_with_non_ledger_files() {
169        let target_dir =
170            get_test_dir("list_all_ledger_file_should_work_with_non_ledger_files/ledger");
171        let entries = vec!["123", "124", "README.md", "124.back"];
172        create_fake_files(&target_dir, &entries);
173        let ledger_files = LedgerFile::list_all_in_dir(target_dir.parent().unwrap())
174            .expect("LedgerFile::list_all_in_dir Failed");
175
176        assert_eq!(vec!["123", "124"], extract_filenames(&ledger_files));
177    }
178}