mithril_cardano_node_internal_database/entities/
immutable_file.rs1use digest::{Digest, Output};
2use std::{
3 cmp::Ordering,
4 fs::File,
5 io,
6 num::ParseIntError,
7 path::{Path, PathBuf},
8};
9use thiserror::Error;
10use walkdir::{DirEntry, WalkDir};
11
12use mithril_common::entities::{ImmutableFileName, ImmutableFileNumber};
13
14use crate::entities::ImmutableFileListingError::{MissingImmutableFiles, MissingImmutableFolder};
15use crate::IMMUTABLE_DIR;
16
17const IMMUTABLE_FILE_EXTENSIONS: [&str; 3] = ["chunk", "primary", "secondary"];
18
19fn is_immutable(entry: &walkdir::DirEntry) -> bool {
20 let is_file = entry.file_type().is_file();
21 let extension = entry.path().extension().map(|e| e.to_string_lossy());
22
23 is_file && extension.is_some_and(|e| IMMUTABLE_FILE_EXTENSIONS.contains(&e.as_ref()))
24}
25
26fn find_immutables_dir(path_to_walk: &Path) -> Option<PathBuf> {
28 WalkDir::new(path_to_walk)
29 .into_iter()
30 .filter_entry(|e| e.file_type().is_dir())
31 .filter_map(|e| e.ok())
32 .find(|f| f.file_name() == IMMUTABLE_DIR)
33 .map(|e| e.into_path())
34}
35
36fn walk_immutables_in_dir<P: AsRef<Path>>(immutable_dir: P) -> impl Iterator<Item = DirEntry> {
38 WalkDir::new(immutable_dir)
39 .min_depth(1)
40 .max_depth(1)
41 .into_iter()
42 .filter_entry(is_immutable)
43 .filter_map(|file| file.ok())
44}
45
46#[derive(Debug, PartialEq, Eq, Clone)]
48pub struct ImmutableFile {
49 pub path: PathBuf,
51
52 pub number: ImmutableFileNumber,
54
55 pub filename: ImmutableFileName,
57}
58
59#[derive(Error, Debug)]
61pub enum ImmutableFileCreationError {
62 #[error("Couldn't extract the file stem for '{path:?}'")]
64 FileStemExtraction {
65 path: PathBuf,
67 },
68
69 #[error("Couldn't extract the filename as string for '{path:?}'")]
71 FileNameExtraction {
72 path: PathBuf,
74 },
75
76 #[error("Error while parsing immutable file number")]
78 FileNumberParsing(#[from] ParseIntError),
79}
80
81#[derive(Error, Debug)]
83pub enum ImmutableFileListingError {
84 #[error("metadata parsing failed")]
86 MetadataParsing(#[from] io::Error),
87
88 #[error("immutable file creation error")]
90 ImmutableFileCreation(#[from] ImmutableFileCreationError),
91
92 #[error("Couldn't find the 'immutable' folder in '{0:?}'")]
94 MissingImmutableFolder(PathBuf),
95
96 #[error("There are no immutable files in '{0:?}'")]
98 MissingImmutableFiles(PathBuf),
99}
100
101impl ImmutableFile {
102 pub fn new(path: PathBuf) -> Result<ImmutableFile, ImmutableFileCreationError> {
104 let filename = path
105 .file_name()
106 .ok_or(ImmutableFileCreationError::FileNameExtraction { path: path.clone() })?
107 .to_str()
108 .ok_or(ImmutableFileCreationError::FileNameExtraction { path: path.clone() })?
109 .to_string();
110
111 let filestem = path
112 .file_stem()
113 .ok_or(ImmutableFileCreationError::FileStemExtraction { path: path.clone() })?
114 .to_str()
115 .ok_or(ImmutableFileCreationError::FileNameExtraction { path: path.clone() })?;
116 let immutable_file_number = filestem.parse::<ImmutableFileNumber>()?;
117
118 Ok(Self {
119 path,
120 number: immutable_file_number,
121 filename,
122 })
123 }
124
125 pub fn compute_raw_hash<D>(&self) -> Result<Output<D>, io::Error>
127 where
128 D: Digest + io::Write,
129 {
130 let mut hasher = D::new();
131 let mut file = File::open(&self.path)?;
132 io::copy(&mut file, &mut hasher)?;
133 Ok(hasher.finalize())
134 }
135
136 pub fn list_all_in_dir(dir: &Path) -> Result<Vec<ImmutableFile>, ImmutableFileListingError> {
138 let immutable_dir = find_immutables_dir(dir).ok_or(
139 ImmutableFileListingError::MissingImmutableFolder(dir.to_path_buf()),
140 )?;
141 let mut files: Vec<ImmutableFile> = vec![];
142
143 for path in walk_immutables_in_dir(&immutable_dir) {
144 let immutable_file = ImmutableFile::new(path.into_path())?;
145 files.push(immutable_file);
146 }
147 files.sort();
148
149 Ok(files)
150 }
151
152 pub fn list_completed_in_dir(
157 dir: &Path,
158 ) -> Result<Vec<ImmutableFile>, ImmutableFileListingError> {
159 let files = Self::list_all_in_dir(dir)?;
160
161 match files.last() {
162 None => Ok(files),
164 Some(last_file) => {
166 let last_number = last_file.number;
167 Ok(files
168 .into_iter()
169 .filter(|f| f.number < last_number)
170 .collect())
171 }
172 }
173 }
174
175 pub fn at_least_one_immutable_files_exist_in_dir(
177 dir: &Path,
178 ) -> Result<(), ImmutableFileListingError> {
179 let immutable_dir =
180 find_immutables_dir(dir).ok_or(MissingImmutableFolder(dir.to_path_buf()))?;
181 if walk_immutables_in_dir(immutable_dir).next().is_some() {
182 Ok(())
183 } else {
184 Err(MissingImmutableFiles(dir.to_path_buf().join(IMMUTABLE_DIR)))
185 }
186 }
187}
188
189impl PartialOrd for ImmutableFile {
190 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
191 Some(self.cmp(other))
192 }
193}
194
195impl Ord for ImmutableFile {
196 fn cmp(&self, other: &Self) -> Ordering {
197 self.number
198 .cmp(&other.number)
199 .then(self.path.cmp(&other.path))
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use std::fs;
206 use std::io::prelude::*;
207
208 use mithril_common::temp_dir_create;
209 use mithril_common::test_utils::TempDir;
210
211 use super::*;
212
213 fn get_test_dir(subdir_name: &str) -> PathBuf {
214 TempDir::create("immutable_file", subdir_name)
215 }
216
217 fn create_fake_files(parent_dir: &Path, child_filenames: &[&str]) {
218 for filename in child_filenames {
219 let file = parent_dir.join(Path::new(filename));
220 let mut source_file = File::create(file).unwrap();
221 write!(source_file, "This is a test file named '{filename}'").unwrap();
222 }
223 }
224
225 fn extract_filenames(immutables: &[ImmutableFile]) -> Vec<String> {
226 immutables
227 .iter()
228 .map(|i| i.path.file_name().unwrap().to_str().unwrap().to_owned())
229 .collect()
230 }
231
232 #[test]
233 fn list_completed_immutable_file_fail_if_not_in_immutable_dir() {
234 let target_dir = get_test_dir("list_immutable_file_fail_if_not_in_immutable_dir/invalid");
235 let entries = vec![];
236 create_fake_files(&target_dir, &entries);
237
238 ImmutableFile::list_completed_in_dir(target_dir.parent().unwrap())
239 .expect_err("ImmutableFile::list_in_dir should have Failed");
240 }
241
242 #[test]
243 fn list_all_immutable_file_should_not_skip_last_number() {
244 let target_dir =
245 get_test_dir("list_all_immutable_file_should_not_skip_last_number/immutable");
246 let entries = vec![
247 "123.chunk",
248 "123.primary",
249 "123.secondary",
250 "125.chunk",
251 "125.primary",
252 "125.secondary",
253 "0124.chunk",
254 "0124.primary",
255 "0124.secondary",
256 "223.chunk",
257 "223.primary",
258 "223.secondary",
259 "0423.chunk",
260 "0423.primary",
261 "0423.secondary",
262 "0424.chunk",
263 "0424.primary",
264 "0424.secondary",
265 "21.chunk",
266 "21.primary",
267 "21.secondary",
268 ];
269 create_fake_files(&target_dir, &entries);
270 let result = ImmutableFile::list_all_in_dir(target_dir.parent().unwrap())
271 .expect("ImmutableFile::list_in_dir Failed");
272
273 assert_eq!(result.last().unwrap().number, 424);
274 let expected_entries_length = 21;
275 assert_eq!(
276 expected_entries_length,
277 result.len(),
278 "Expected to find {} files but found {}",
279 entries.len(),
280 result.len(),
281 );
282 }
283
284 #[test]
285 fn list_completed_immutable_file_should_skip_last_number() {
286 let target_dir = get_test_dir("list_immutable_file_should_skip_last_number/immutable");
287 let entries = vec![
288 "123.chunk",
289 "123.primary",
290 "123.secondary",
291 "125.chunk",
292 "125.primary",
293 "125.secondary",
294 "0124.chunk",
295 "0124.primary",
296 "0124.secondary",
297 "223.chunk",
298 "223.primary",
299 "223.secondary",
300 "0423.chunk",
301 "0423.primary",
302 "0423.secondary",
303 "0424.chunk",
304 "0424.primary",
305 "0424.secondary",
306 "21.chunk",
307 "21.primary",
308 "21.secondary",
309 ];
310 create_fake_files(&target_dir, &entries);
311 let result = ImmutableFile::list_completed_in_dir(target_dir.parent().unwrap())
312 .expect("ImmutableFile::list_in_dir Failed");
313
314 assert_eq!(result.last().unwrap().number, 423);
315 assert_eq!(
316 result.len(),
317 entries.len() - 3,
318 "Expected to find {} files since the last (chunk, primary, secondary) trio is skipped, but found {}",
319 entries.len() - 3,
320 result.len(),
321 );
322 }
323
324 #[test]
325 fn list_completed_immutable_file_should_works_in_a_empty_folder() {
326 let target_dir =
327 get_test_dir("list_immutable_file_should_works_even_in_a_empty_folder/immutable");
328 let entries = vec![];
329 create_fake_files(&target_dir, &entries);
330 let result = ImmutableFile::list_completed_in_dir(target_dir.parent().unwrap())
331 .expect("ImmutableFile::list_in_dir Failed");
332
333 assert!(result.is_empty());
334 }
335
336 #[test]
337 fn list_completed_immutable_file_order_should_be_deterministic() {
338 let target_dir =
339 get_test_dir("list_completed_immutable_file_order_should_be_deterministic/immutable");
340 let entries = vec![
341 "21.chunk",
342 "21.primary",
343 "21.secondary",
344 "123.chunk",
345 "123.primary",
346 "123.secondary",
347 "124.chunk",
348 "124.primary",
349 "124.secondary",
350 "125.chunk",
351 "125.primary",
352 "125.secondary",
353 "223.chunk",
354 "223.primary",
355 "223.secondary",
356 "423.chunk",
357 "423.primary",
358 "423.secondary",
359 "424.chunk",
360 "424.primary",
361 "424.secondary",
362 ];
363 create_fake_files(&target_dir, &entries);
364 let immutables = ImmutableFile::list_completed_in_dir(target_dir.parent().unwrap())
365 .expect("ImmutableFile::list_in_dir Failed");
366 let immutables_names: Vec<String> = extract_filenames(&immutables);
367
368 let expected: Vec<&str> = entries.into_iter().rev().skip(3).rev().collect();
369 assert_eq!(expected, immutables_names);
370 }
371
372 #[test]
373 fn list_completed_immutable_file_should_work_with_non_immutable_files() {
374 let target_dir =
375 get_test_dir("list_immutable_file_should_work_with_non_immutable_files/immutable");
376 let entries = vec![
377 "123.chunk",
378 "123.primary",
379 "123.secondary",
380 "124.chunk",
381 "124.primary",
382 "124.secondary",
383 "README.md",
384 "124.secondary.back",
385 ];
386 create_fake_files(&target_dir, &entries);
387 let immutables = ImmutableFile::list_completed_in_dir(target_dir.parent().unwrap())
388 .expect("ImmutableFile::list_in_dir Failed");
389 let immutables_names: Vec<String> = extract_filenames(&immutables);
390
391 let expected: Vec<&str> = entries.into_iter().rev().skip(5).rev().collect();
392 assert_eq!(expected, immutables_names);
393 }
394
395 #[test]
396 fn list_completed_immutable_file_can_list_incomplete_trio() {
397 let target_dir = get_test_dir("list_immutable_file_can_list_incomplete_trio/immutable");
398 let entries = vec![
399 "21.chunk",
400 "21.primary",
401 "21.secondary",
402 "123.chunk",
403 "123.secondary",
404 "124.chunk",
405 "124.primary",
406 "125.primary",
407 "125.secondary",
408 "223.chunk",
409 "224.primary",
410 "225.secondary",
411 "226.chunk",
412 ];
413 create_fake_files(&target_dir, &entries);
414 let immutables = ImmutableFile::list_completed_in_dir(target_dir.parent().unwrap())
415 .expect("ImmutableFile::list_in_dir Failed");
416 let immutables_names: Vec<String> = extract_filenames(&immutables);
417
418 let expected: Vec<&str> = entries.into_iter().rev().skip(1).rev().collect();
419 assert_eq!(expected, immutables_names);
420 }
421
422 #[test]
423 fn at_least_one_immutable_files_exist_in_dir_throw_error_if_immutable_dir_does_not_exist() {
424 let database_path = temp_dir_create!();
425
426 let error = ImmutableFile::at_least_one_immutable_files_exist_in_dir(&database_path)
427 .expect_err("check_presence_of_immutables should fail");
428 assert_eq!(
429 error.to_string(),
430 format!("Couldn't find the 'immutable' folder in '{database_path:?}'")
431 );
432 }
433
434 #[test]
435 fn at_least_one_immutable_files_exist_in_dir_throw_error_if_immutable_dir_is_empty() {
436 let database_path = temp_dir_create!();
437 fs::create_dir(database_path.join(IMMUTABLE_DIR)).unwrap();
438
439 let error = ImmutableFile::at_least_one_immutable_files_exist_in_dir(&database_path)
440 .expect_err("check_presence_of_immutables should fail");
441 assert_eq!(
442 error.to_string(),
443 format!(
444 "There are no immutable files in '{:?}'",
445 database_path.join(IMMUTABLE_DIR)
446 )
447 );
448 }
449
450 #[test]
451 fn at_least_one_immutable_files_exist_in_dir_is_ok_if_immutable_dir_contains_at_least_one_file()
452 {
453 let database_dir = temp_dir_create!();
454 let database_path = database_dir.as_path();
455 let immutable_file_path = database_dir.join(IMMUTABLE_DIR).join("00001.chunk");
456 fs::create_dir(database_dir.join(IMMUTABLE_DIR)).unwrap();
457 File::create(immutable_file_path).unwrap();
458
459 ImmutableFile::at_least_one_immutable_files_exist_in_dir(database_path)
460 .expect("check_presence_of_immutables should succeed");
461 }
462}