mithril_common/digesters/
ledger_file.rs1use 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
11fn 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#[derive(Debug, PartialEq, Eq, Clone)]
23pub struct LedgerFile {
24 pub path: PathBuf,
26
27 pub slot_number: SlotNumber,
29
30 pub filename: String,
32}
33
34#[derive(Error, Debug)]
36pub enum LedgerFileListingError {
37 #[error("Couldn't find the 'ledger' folder in '{0:?}'")]
39 MissingLedgerFolder(PathBuf),
40}
41
42impl LedgerFile {
43 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 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 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}