mithril_client_cli/utils/
cardano_db_download_checker.rs1use std::{
2 fs,
3 ops::Not,
4 path::{Path, PathBuf},
5};
6
7use anyhow::Context;
8use human_bytes::human_bytes;
9use thiserror::Error;
10
11use mithril_client::{MithrilError, MithrilResult, common::CompressionAlgorithm};
12
13pub struct CardanoDbDownloadChecker;
15
16#[derive(Debug, Error)]
18pub enum CardanoDbDownloadCheckerError {
19 #[error("There is only {} remaining in directory '{}' to store and unpack a {} large archive.", human_bytes(*left_space), pathdir.display(), human_bytes(*archive_size))]
23 NotEnoughSpaceForArchive {
24 left_space: f64,
26
27 pathdir: PathBuf,
29
30 archive_size: f64,
32 },
33
34 #[error("There is only {} remaining in directory '{}' to store and unpack a {} Cardano database.", human_bytes(*left_space), pathdir.display(), human_bytes(*db_size))]
36 NotEnoughSpaceForUncompressedData {
37 left_space: f64,
39
40 pathdir: PathBuf,
42
43 db_size: f64,
45 },
46
47 #[error("Unpack directory '{0}' is not empty, please clean up its content.")]
51 UnpackDirectoryNotEmpty(PathBuf),
52
53 #[error(
55 "Unpack directory '{0}' is not writable, please check own or parents' permissions and ownership."
56 )]
57 UnpackDirectoryIsNotWritable(PathBuf, #[source] MithrilError),
58}
59
60impl CardanoDbDownloadChecker {
61 pub fn ensure_dir_exist(pathdir: &Path) -> MithrilResult<()> {
63 if pathdir.exists().not() {
64 fs::create_dir_all(pathdir).map_err(|e| {
65 CardanoDbDownloadCheckerError::UnpackDirectoryIsNotWritable(
66 pathdir.to_owned(),
67 e.into(),
68 )
69 })?;
70 }
71
72 Ok(())
73 }
74
75 pub fn check_prerequisites_for_archive(
78 pathdir: &Path,
79 size: u64,
80 compression_algorithm: CompressionAlgorithm,
81 ) -> MithrilResult<()> {
82 Self::check_path_is_an_empty_dir(pathdir)?;
83 Self::check_dir_writable(pathdir)?;
84 Self::check_disk_space_for_archive(pathdir, size, compression_algorithm)
85 }
86
87 pub fn check_prerequisites_for_uncompressed_data(
89 pathdir: &Path,
90 total_size_uncompressed: u64,
91 allow_override: bool,
92 ) -> MithrilResult<()> {
93 if !allow_override {
94 Self::check_path_is_an_empty_dir(pathdir)?;
95 }
96 Self::check_dir_writable(pathdir)?;
97 Self::check_disk_space_for_uncompressed_data(pathdir, total_size_uncompressed)
98 }
99
100 fn check_path_is_an_empty_dir(pathdir: &Path) -> MithrilResult<()> {
101 if pathdir.is_dir().not() {
102 anyhow::bail!("Given path is not a directory: {}", pathdir.display());
103 }
104
105 if fs::read_dir(pathdir)
106 .with_context(|| {
107 format!(
108 "Could not list directory `{}` to check if it's empty",
109 pathdir.display()
110 )
111 })?
112 .next()
113 .is_some()
114 {
115 return Err(
116 CardanoDbDownloadCheckerError::UnpackDirectoryNotEmpty(pathdir.to_owned()).into(),
117 );
118 }
119
120 Ok(())
121 }
122
123 fn check_dir_writable(pathdir: &Path) -> MithrilResult<()> {
124 let temp_file_path = pathdir.join("temp_file");
126 fs::File::create(&temp_file_path).map_err(|e| {
127 CardanoDbDownloadCheckerError::UnpackDirectoryIsNotWritable(
128 pathdir.to_owned(),
129 e.into(),
130 )
131 })?;
132
133 fs::remove_file(temp_file_path).map_err(|e| {
135 CardanoDbDownloadCheckerError::UnpackDirectoryIsNotWritable(
136 pathdir.to_owned(),
137 e.into(),
138 )
139 })?;
140
141 Ok(())
142 }
143
144 fn check_disk_space_for_archive(
145 pathdir: &Path,
146 size: u64,
147 compression_algorithm: CompressionAlgorithm,
148 ) -> MithrilResult<()> {
149 let free_space = fs2::available_space(pathdir)? as f64;
150 if free_space < compression_algorithm.free_space_snapshot_ratio() * size as f64 {
151 return Err(CardanoDbDownloadCheckerError::NotEnoughSpaceForArchive {
152 left_space: free_space,
153 pathdir: pathdir.to_owned(),
154 archive_size: size as f64,
155 }
156 .into());
157 }
158 Ok(())
159 }
160
161 fn check_disk_space_for_uncompressed_data(pathdir: &Path, size: u64) -> MithrilResult<()> {
162 let free_space = fs2::available_space(pathdir)?;
163 if free_space < size {
164 return Err(
165 CardanoDbDownloadCheckerError::NotEnoughSpaceForUncompressedData {
166 left_space: free_space as f64,
167 pathdir: pathdir.to_owned(),
168 db_size: size as f64,
169 }
170 .into(),
171 );
172 }
173 Ok(())
174 }
175}
176
177#[cfg(test)]
178mod test {
179 use mithril_common::test_utils::TempDir;
180
181 use super::*;
182
183 fn create_temporary_empty_directory(name: &str) -> PathBuf {
184 TempDir::create("client-cli-unpacker", name)
185 }
186
187 #[test]
188 fn create_directory_if_it_doesnt_exist() {
189 let pathdir =
190 create_temporary_empty_directory("directory_does_not_exist").join("target_directory");
191
192 CardanoDbDownloadChecker::ensure_dir_exist(&pathdir)
193 .expect("ensure_dir_exist should not fail");
194
195 assert!(pathdir.exists());
196 }
197
198 #[test]
199 fn return_error_if_path_is_a_file() {
200 let pathdir =
201 create_temporary_empty_directory("fail_if_pathdir_is_file").join("target_directory");
202 fs::File::create(&pathdir).unwrap();
203
204 CardanoDbDownloadChecker::check_path_is_an_empty_dir(&pathdir)
205 .expect_err("check_path_is_an_empty_dir should fail");
206 }
207
208 #[test]
209 fn return_ok_if_directory_exist_and_empty() {
210 let pathdir =
211 create_temporary_empty_directory("existing_directory").join("target_directory");
212 fs::create_dir_all(&pathdir).unwrap();
213
214 CardanoDbDownloadChecker::check_path_is_an_empty_dir(&pathdir)
215 .expect("check_path_is_an_empty_dir should not fail");
216 }
217
218 #[test]
219 fn return_error_if_directory_exists_and_not_empty() {
220 let pathdir = create_temporary_empty_directory("existing_directory_not_empty");
221 fs::create_dir_all(&pathdir).unwrap();
222 fs::File::create(pathdir.join("file.txt")).unwrap();
223
224 let error = CardanoDbDownloadChecker::check_path_is_an_empty_dir(&pathdir)
225 .expect_err("check_path_is_an_empty_dir should fail");
226
227 assert!(
228 matches!(
229 error.downcast_ref::<CardanoDbDownloadCheckerError>(),
230 Some(CardanoDbDownloadCheckerError::UnpackDirectoryNotEmpty(_))
231 ),
232 "Unexpected error: {error:?}"
233 );
234 }
235
236 #[test]
237 fn return_error_if_not_enough_available_space_for_archive() {
238 let pathdir = create_temporary_empty_directory("not_enough_available_space_for_archive")
239 .join("target_directory");
240 fs::create_dir_all(&pathdir).unwrap();
241 let archive_size = u64::MAX;
242
243 let error = CardanoDbDownloadChecker::check_disk_space_for_archive(
244 &pathdir,
245 archive_size,
246 CompressionAlgorithm::default(),
247 )
248 .expect_err("check_disk_space_for_archive should fail");
249
250 assert!(
251 matches!(
252 error.downcast_ref::<CardanoDbDownloadCheckerError>(),
253 Some(CardanoDbDownloadCheckerError::NotEnoughSpaceForArchive {
254 left_space: _,
255 pathdir: _,
256 archive_size: _
257 })
258 ),
259 "Unexpected error: {error:?}"
260 );
261 }
262
263 #[test]
264 fn return_ok_if_enough_available_space_for_archive() {
265 let pathdir = create_temporary_empty_directory("enough_available_space_for_archive")
266 .join("target_directory");
267 fs::create_dir_all(&pathdir).unwrap();
268 let archive_size = 3;
269
270 CardanoDbDownloadChecker::check_disk_space_for_archive(
271 &pathdir,
272 archive_size,
273 CompressionAlgorithm::default(),
274 )
275 .expect("check_disk_space_for_archive should not fail");
276 }
277
278 #[test]
279 fn check_disk_space_for_uncompressed_data_return_error_if_not_enough_available_space() {
280 let pathdir =
281 create_temporary_empty_directory("not_enough_available_space_for_uncompressed_data")
282 .join("target_directory");
283 fs::create_dir_all(&pathdir).unwrap();
284 let uncompressed_data_size = u64::MAX;
285
286 let error = CardanoDbDownloadChecker::check_disk_space_for_uncompressed_data(
287 &pathdir,
288 uncompressed_data_size,
289 )
290 .expect_err("check_disk_space_for_uncompressed_data should fail");
291
292 assert!(
293 matches!(
294 error.downcast_ref::<CardanoDbDownloadCheckerError>(),
295 Some(
296 CardanoDbDownloadCheckerError::NotEnoughSpaceForUncompressedData {
297 left_space: _,
298 pathdir: _,
299 db_size: _
300 }
301 )
302 ),
303 "Unexpected error: {error:?}"
304 );
305 }
306
307 #[test]
308 fn return_ok_if_enough_available_space_for_uncompressed_data() {
309 let pathdir =
310 create_temporary_empty_directory("enough_available_space_for_uncompressed_data")
311 .join("target_directory");
312 fs::create_dir_all(&pathdir).unwrap();
313 let uncompressed_data_size = 3;
314
315 CardanoDbDownloadChecker::check_disk_space_for_uncompressed_data(
316 &pathdir,
317 uncompressed_data_size,
318 )
319 .expect("check_disk_space_for_uncompressed_data should not fail");
320 }
321
322 #[test]
323 fn check_prerequisites_for_uncompressed_data_return_error_without_allow_override_and_directory_not_empty()
324 {
325 let pathdir = create_temporary_empty_directory(
326 "return_error_without_allow_override_and_directory_not_empty",
327 );
328 fs::create_dir_all(&pathdir).unwrap();
329 fs::File::create(pathdir.join("file.txt")).unwrap();
330
331 let error =
332 CardanoDbDownloadChecker::check_prerequisites_for_uncompressed_data(&pathdir, 0, false)
333 .expect_err("check_prerequisites_for_uncompressed_data should fail");
334
335 assert!(
336 matches!(
337 error.downcast_ref::<CardanoDbDownloadCheckerError>(),
338 Some(CardanoDbDownloadCheckerError::UnpackDirectoryNotEmpty(_))
339 ),
340 "Unexpected error: {error:?}"
341 );
342 }
343
344 #[test]
345 fn check_prerequisites_for_uncompressed_data_do_not_return_error_without_allow_override_and_directory_not_empty()
346 {
347 let pathdir = create_temporary_empty_directory(
348 "do_not_return_error_without_allow_override_and_directory_not_empty",
349 );
350 fs::create_dir_all(&pathdir).unwrap();
351 fs::File::create(pathdir.join("file.txt")).unwrap();
352
353 CardanoDbDownloadChecker::check_prerequisites_for_uncompressed_data(&pathdir, 0, true)
354 .expect("check_prerequisites_for_uncompressed_data should not fail");
355 }
356
357 #[cfg(not(target_os = "windows"))]
360 mod unix_only {
361 use super::*;
362
363 fn make_readonly(path: &Path) {
364 let mut perms = fs::metadata(path).unwrap().permissions();
365 perms.set_readonly(true);
366 fs::set_permissions(path, perms).unwrap();
367 }
368
369 #[test]
370 fn return_error_if_directory_could_not_be_created() {
371 let pathdir = create_temporary_empty_directory("read_only_directory");
372 let targetdir = pathdir.join("target_directory");
373 make_readonly(&pathdir);
374
375 let error = CardanoDbDownloadChecker::ensure_dir_exist(&targetdir)
376 .expect_err("ensure_dir_exist should fail");
377
378 assert!(
379 matches!(
380 error.downcast_ref::<CardanoDbDownloadCheckerError>(),
381 Some(CardanoDbDownloadCheckerError::UnpackDirectoryIsNotWritable(
382 _,
383 _
384 ))
385 ),
386 "Unexpected error: {error:?}"
387 );
388 }
389
390 #[test]
393 fn return_error_if_existing_directory_is_not_writable() {
394 let pathdir =
395 create_temporary_empty_directory("existing_directory_not_writable").join("db");
396 fs::create_dir(&pathdir).unwrap();
397 make_readonly(&pathdir);
398
399 let error = CardanoDbDownloadChecker::check_dir_writable(&pathdir)
400 .expect_err("check_dir_writable should fail");
401
402 assert!(
403 matches!(
404 error.downcast_ref::<CardanoDbDownloadCheckerError>(),
405 Some(CardanoDbDownloadCheckerError::UnpackDirectoryIsNotWritable(
406 _,
407 _
408 ))
409 ),
410 "Unexpected error: {error:?}"
411 );
412 }
413 }
414}