mithril_client/file_downloader/
interface.rs

1use std::path::Path;
2
3use anyhow::anyhow;
4use async_trait::async_trait;
5
6use mithril_common::{
7    entities::{
8        AncillaryLocation, CompressionAlgorithm, DigestLocation, FileUri, ImmutableFileNumber,
9    },
10    StdError, StdResult,
11};
12
13use crate::feedback::{MithrilEvent, MithrilEventCardanoDatabase};
14
15/// A file downloader URI
16#[derive(Debug, PartialEq, Eq, Clone)]
17pub enum FileDownloaderUri {
18    /// A single file URI
19    FileUri(FileUri),
20}
21
22impl FileDownloaderUri {
23    /// Get the URI as a string
24    pub fn as_str(&self) -> &str {
25        match self {
26            FileDownloaderUri::FileUri(file_uri) => file_uri.0.as_str(),
27        }
28    }
29}
30
31impl From<String> for FileDownloaderUri {
32    fn from(location: String) -> Self {
33        Self::FileUri(FileUri(location))
34    }
35}
36
37impl From<FileUri> for FileDownloaderUri {
38    fn from(file_uri: FileUri) -> Self {
39        Self::FileUri(file_uri)
40    }
41}
42
43impl TryFrom<AncillaryLocation> for FileDownloaderUri {
44    type Error = StdError;
45
46    fn try_from(location: AncillaryLocation) -> Result<Self, Self::Error> {
47        match location {
48            AncillaryLocation::CloudStorage {
49                uri,
50                compression_algorithm: _,
51            } => Ok(Self::FileUri(FileUri(uri))),
52            AncillaryLocation::Unknown => {
53                Err(anyhow!("Unknown location type to download ancillary"))
54            }
55        }
56    }
57}
58
59impl TryFrom<DigestLocation> for FileDownloaderUri {
60    type Error = StdError;
61
62    fn try_from(location: DigestLocation) -> Result<Self, Self::Error> {
63        match location {
64            DigestLocation::CloudStorage {
65                uri,
66                compression_algorithm: _,
67            }
68            | DigestLocation::Aggregator { uri } => Ok(Self::FileUri(FileUri(uri))),
69            DigestLocation::Unknown => Err(anyhow!("Unknown location type to download digest")),
70        }
71    }
72}
73
74/// A download event
75///
76/// The `download_id` is a unique identifier that allow
77/// [feedback receivers][crate::feedback::FeedbackReceiver] to track concurrent downloads.
78#[derive(Debug, Clone)]
79pub enum DownloadEvent {
80    /// Immutable file download
81    Immutable {
82        /// Unique download identifier
83        download_id: String,
84        /// Immutable file number
85        immutable_file_number: ImmutableFileNumber,
86    },
87    /// Ancillary file download
88    Ancillary {
89        /// Unique download identifier
90        download_id: String,
91    },
92    /// Digest file download
93    Digest {
94        /// Unique download identifier
95        download_id: String,
96    },
97    /// Database download of all immutable files together
98    Full {
99        /// Unique download identifier
100        download_id: String,
101        /// Digest of the downloaded snapshot
102        digest: String,
103    },
104    /// Download of the ancillary file associated with a full immutables download
105    FullAncillary {
106        /// Unique download identifier
107        download_id: String,
108    },
109}
110
111impl DownloadEvent {
112    /// Get the unique download identifier
113    pub fn download_id(&self) -> &str {
114        match self {
115            DownloadEvent::Immutable { download_id, .. }
116            | DownloadEvent::Ancillary { download_id }
117            | DownloadEvent::Digest { download_id }
118            | DownloadEvent::Full { download_id, .. }
119            | DownloadEvent::FullAncillary { download_id } => download_id,
120        }
121    }
122
123    /// Build a download started event
124    pub fn build_download_started_event(&self, size: u64) -> MithrilEvent {
125        match self {
126            DownloadEvent::Immutable {
127                download_id,
128                immutable_file_number,
129            } => MithrilEvent::CardanoDatabase(
130                MithrilEventCardanoDatabase::ImmutableDownloadStarted {
131                    download_id: download_id.to_string(),
132                    immutable_file_number: *immutable_file_number,
133                    size,
134                },
135            ),
136            DownloadEvent::Ancillary { download_id } => MithrilEvent::CardanoDatabase(
137                MithrilEventCardanoDatabase::AncillaryDownloadStarted {
138                    download_id: download_id.to_string(),
139                    size,
140                },
141            ),
142            DownloadEvent::Digest { download_id } => {
143                MithrilEvent::CardanoDatabase(MithrilEventCardanoDatabase::DigestDownloadStarted {
144                    download_id: download_id.to_string(),
145                    size,
146                })
147            }
148            DownloadEvent::Full {
149                download_id,
150                digest,
151            } => MithrilEvent::SnapshotDownloadStarted {
152                download_id: download_id.to_string(),
153                digest: digest.to_string(),
154                size,
155            },
156            DownloadEvent::FullAncillary { download_id } => {
157                MithrilEvent::SnapshotAncillaryDownloadStarted {
158                    download_id: download_id.to_string(),
159                    size,
160                }
161            }
162        }
163    }
164
165    /// Build a download started event
166    pub fn build_download_progress_event(
167        &self,
168        downloaded_bytes: u64,
169        total_bytes: u64,
170    ) -> MithrilEvent {
171        match self {
172            DownloadEvent::Immutable {
173                immutable_file_number,
174                download_id,
175            } => MithrilEvent::CardanoDatabase(
176                MithrilEventCardanoDatabase::ImmutableDownloadProgress {
177                    download_id: download_id.to_string(),
178                    downloaded_bytes,
179                    size: total_bytes,
180                    immutable_file_number: *immutable_file_number,
181                },
182            ),
183            DownloadEvent::Ancillary { download_id } => MithrilEvent::CardanoDatabase(
184                MithrilEventCardanoDatabase::AncillaryDownloadProgress {
185                    download_id: download_id.to_string(),
186                    downloaded_bytes,
187                    size: total_bytes,
188                },
189            ),
190            DownloadEvent::Digest { download_id } => {
191                MithrilEvent::CardanoDatabase(MithrilEventCardanoDatabase::DigestDownloadProgress {
192                    download_id: download_id.to_string(),
193                    downloaded_bytes,
194                    size: total_bytes,
195                })
196            }
197            DownloadEvent::Full { download_id, .. } => MithrilEvent::SnapshotDownloadProgress {
198                download_id: download_id.to_string(),
199                downloaded_bytes,
200                size: total_bytes,
201            },
202            DownloadEvent::FullAncillary { download_id } => {
203                MithrilEvent::SnapshotAncillaryDownloadProgress {
204                    download_id: download_id.to_string(),
205                    downloaded_bytes,
206                    size: total_bytes,
207                }
208            }
209        }
210    }
211
212    /// Build a download completed event
213    pub fn build_download_completed_event(&self) -> MithrilEvent {
214        match self {
215            DownloadEvent::Immutable {
216                download_id,
217                immutable_file_number,
218            } => MithrilEvent::CardanoDatabase(
219                MithrilEventCardanoDatabase::ImmutableDownloadCompleted {
220                    download_id: download_id.to_string(),
221                    immutable_file_number: *immutable_file_number,
222                },
223            ),
224            DownloadEvent::Ancillary { download_id } => MithrilEvent::CardanoDatabase(
225                MithrilEventCardanoDatabase::AncillaryDownloadCompleted {
226                    download_id: download_id.to_string(),
227                },
228            ),
229            DownloadEvent::Digest { download_id } => MithrilEvent::CardanoDatabase(
230                MithrilEventCardanoDatabase::DigestDownloadCompleted {
231                    download_id: download_id.to_string(),
232                },
233            ),
234            DownloadEvent::Full { download_id, .. } => MithrilEvent::SnapshotDownloadCompleted {
235                download_id: download_id.to_string(),
236            },
237            DownloadEvent::FullAncillary { download_id, .. } => {
238                MithrilEvent::SnapshotAncillaryDownloadCompleted {
239                    download_id: download_id.to_string(),
240                }
241            }
242        }
243    }
244}
245
246/// A file downloader
247#[cfg_attr(test, mockall::automock)]
248#[async_trait]
249pub trait FileDownloader: Sync + Send {
250    /// Download and unpack (if necessary) a file on the disk.
251    ///
252    async fn download_unpack(
253        &self,
254        location: &FileDownloaderUri,
255        file_size: u64,
256        target_dir: &Path,
257        compression_algorithm: Option<CompressionAlgorithm>,
258        download_event_type: DownloadEvent,
259    ) -> StdResult<()>;
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn download_event_type_builds_started_event() {
268        let download_event_type = DownloadEvent::Immutable {
269            download_id: "download-123".to_string(),
270            immutable_file_number: 123,
271        };
272        let event = download_event_type.build_download_started_event(1234);
273        assert_eq!(
274            MithrilEvent::CardanoDatabase(MithrilEventCardanoDatabase::ImmutableDownloadStarted {
275                immutable_file_number: 123,
276                download_id: "download-123".to_string(),
277                size: 1234,
278            }),
279            event,
280        );
281
282        let download_event_type = DownloadEvent::Ancillary {
283            download_id: "download-123".to_string(),
284        };
285        let event = download_event_type.build_download_started_event(1234);
286        assert_eq!(
287            MithrilEvent::CardanoDatabase(MithrilEventCardanoDatabase::AncillaryDownloadStarted {
288                download_id: "download-123".to_string(),
289                size: 1234,
290            }),
291            event,
292        );
293
294        let download_event_type = DownloadEvent::Digest {
295            download_id: "download-123".to_string(),
296        };
297        let event = download_event_type.build_download_started_event(1234);
298        assert_eq!(
299            MithrilEvent::CardanoDatabase(MithrilEventCardanoDatabase::DigestDownloadStarted {
300                download_id: "download-123".to_string(),
301                size: 1234,
302            }),
303            event,
304        );
305
306        let download_event_type = DownloadEvent::Full {
307            download_id: "download-123".to_string(),
308            digest: "digest-123".to_string(),
309        };
310        let event = download_event_type.build_download_started_event(1234);
311        assert_eq!(
312            MithrilEvent::SnapshotDownloadStarted {
313                digest: "digest-123".to_string(),
314                download_id: "download-123".to_string(),
315                size: 1234,
316            },
317            event,
318        );
319
320        let download_event_type = DownloadEvent::FullAncillary {
321            download_id: "download-123".to_string(),
322        };
323        let event = download_event_type.build_download_started_event(1234);
324        assert_eq!(
325            MithrilEvent::SnapshotAncillaryDownloadStarted {
326                download_id: "download-123".to_string(),
327                size: 1234,
328            },
329            event,
330        );
331    }
332
333    #[test]
334    fn download_event_type_builds_progress_event() {
335        let download_event_type = DownloadEvent::Immutable {
336            download_id: "download-123".to_string(),
337            immutable_file_number: 123,
338        };
339        let event = download_event_type.build_download_progress_event(123, 1234);
340        assert_eq!(
341            MithrilEvent::CardanoDatabase(MithrilEventCardanoDatabase::ImmutableDownloadProgress {
342                immutable_file_number: 123,
343                download_id: "download-123".to_string(),
344                downloaded_bytes: 123,
345                size: 1234,
346            }),
347            event,
348        );
349
350        let download_event_type = DownloadEvent::Ancillary {
351            download_id: "download-123".to_string(),
352        };
353        let event = download_event_type.build_download_progress_event(123, 1234);
354        assert_eq!(
355            MithrilEvent::CardanoDatabase(MithrilEventCardanoDatabase::AncillaryDownloadProgress {
356                download_id: "download-123".to_string(),
357                downloaded_bytes: 123,
358                size: 1234,
359            }),
360            event,
361        );
362
363        let download_event_type = DownloadEvent::Digest {
364            download_id: "download-123".to_string(),
365        };
366        let event = download_event_type.build_download_progress_event(123, 1234);
367        assert_eq!(
368            MithrilEvent::CardanoDatabase(MithrilEventCardanoDatabase::DigestDownloadProgress {
369                download_id: "download-123".to_string(),
370                downloaded_bytes: 123,
371                size: 1234,
372            }),
373            event,
374        );
375
376        let download_event_type = DownloadEvent::Full {
377            download_id: "download-123".to_string(),
378            digest: "whatever".to_string(),
379        };
380        let event = download_event_type.build_download_progress_event(123, 1234);
381        assert_eq!(
382            MithrilEvent::SnapshotDownloadProgress {
383                download_id: "download-123".to_string(),
384                downloaded_bytes: 123,
385                size: 1234,
386            },
387            event,
388        );
389
390        let download_event_type = DownloadEvent::FullAncillary {
391            download_id: "download-123".to_string(),
392        };
393        let event = download_event_type.build_download_progress_event(123, 1234);
394        assert_eq!(
395            MithrilEvent::SnapshotAncillaryDownloadProgress {
396                download_id: "download-123".to_string(),
397                downloaded_bytes: 123,
398                size: 1234,
399            },
400            event,
401        );
402    }
403
404    #[test]
405    fn file_downloader_uri_from_ancillary_location() {
406        let location = AncillaryLocation::CloudStorage {
407            uri: "http://whatever/ancillary-1".to_string(),
408            compression_algorithm: Some(CompressionAlgorithm::Gzip),
409        };
410        let file_downloader_uri: FileDownloaderUri = location.try_into().unwrap();
411
412        assert_eq!(
413            FileDownloaderUri::FileUri(FileUri("http://whatever/ancillary-1".to_string())),
414            file_downloader_uri
415        );
416    }
417    #[test]
418    fn file_downloader_uri_from_unknown_ancillary_location() {
419        let location = AncillaryLocation::Unknown;
420        let file_downloader_uri: StdResult<FileDownloaderUri> = location.try_into();
421
422        file_downloader_uri.expect_err("try_into should fail on Unknown ancillary location");
423    }
424
425    #[test]
426    fn file_downloader_uri_from_digest_location() {
427        let location = DigestLocation::CloudStorage {
428            uri: "http://whatever/digest-1".to_string(),
429            compression_algorithm: None,
430        };
431        let file_downloader_uri: FileDownloaderUri = location.try_into().unwrap();
432
433        assert_eq!(
434            FileDownloaderUri::FileUri(FileUri("http://whatever/digest-1".to_string())),
435            file_downloader_uri
436        );
437    }
438    #[test]
439    fn file_downloader_uri_from_unknown_digest_location() {
440        let location = DigestLocation::Unknown;
441        let file_downloader_uri: StdResult<FileDownloaderUri> = location.try_into();
442
443        file_downloader_uri.expect_err("try_into should fail on Unknown digest location");
444    }
445
446    #[test]
447    fn download_event_type_builds_completed_event() {
448        let download_event_type = DownloadEvent::Immutable {
449            download_id: "download-123".to_string(),
450            immutable_file_number: 123,
451        };
452        let event = download_event_type.build_download_completed_event();
453        assert_eq!(
454            MithrilEvent::CardanoDatabase(
455                MithrilEventCardanoDatabase::ImmutableDownloadCompleted {
456                    immutable_file_number: 123,
457                    download_id: "download-123".to_string()
458                }
459            ),
460            event,
461        );
462
463        let download_event_type = DownloadEvent::Ancillary {
464            download_id: "download-123".to_string(),
465        };
466        let event = download_event_type.build_download_completed_event();
467        assert_eq!(
468            MithrilEvent::CardanoDatabase(
469                MithrilEventCardanoDatabase::AncillaryDownloadCompleted {
470                    download_id: "download-123".to_string(),
471                }
472            ),
473            event,
474        );
475
476        let download_event_type = DownloadEvent::Digest {
477            download_id: "download-123".to_string(),
478        };
479        let event = download_event_type.build_download_completed_event();
480        assert_eq!(
481            MithrilEvent::CardanoDatabase(MithrilEventCardanoDatabase::DigestDownloadCompleted {
482                download_id: "download-123".to_string(),
483            }),
484            event,
485        );
486
487        let download_event_type = DownloadEvent::Full {
488            download_id: "download-123".to_string(),
489            digest: "whatever".to_string(),
490        };
491        let event = download_event_type.build_download_completed_event();
492        assert_eq!(
493            MithrilEvent::SnapshotDownloadCompleted {
494                download_id: "download-123".to_string(),
495            },
496            event,
497        );
498
499        let download_event_type = DownloadEvent::FullAncillary {
500            download_id: "download-123".to_string(),
501        };
502        let event = download_event_type.build_download_completed_event();
503        assert_eq!(
504            MithrilEvent::SnapshotAncillaryDownloadCompleted {
505                download_id: "download-123".to_string(),
506            },
507            event,
508        );
509    }
510}