atlas_file_download/
lib.rs1#![allow(clippy::arithmetic_side_effects)]
2use {
3 console::Emoji,
4 indicatif::{ProgressBar, ProgressStyle},
5 log::*,
6 std::{
7 fs::{self, File},
8 io::{self, Read},
9 path::Path,
10 time::{Duration, Instant},
11 },
12};
13
14static TRUCK: Emoji = Emoji("🚚 ", "");
15static SPARKLE: Emoji = Emoji("✨ ", "");
16
17fn new_spinner_progress_bar() -> ProgressBar {
19 let progress_bar = ProgressBar::new(42);
20 progress_bar.set_style(
21 ProgressStyle::default_spinner()
22 .template("{spinner:.green} {wide_msg}")
23 .expect("ProgresStyle::template direct input to be correct"),
24 );
25 progress_bar.enable_steady_tick(Duration::from_millis(100));
26 progress_bar
27}
28
29#[derive(Debug)]
31pub struct DownloadProgressRecord {
32 pub elapsed_time: Duration,
34 pub last_elapsed_time: Duration,
36 pub last_throughput: f32,
38 pub total_throughput: f32,
40 pub total_bytes: usize,
42 pub current_bytes: usize,
44 pub percentage_done: f32,
46 pub estimated_remaining_time: f32,
48 pub notification_count: u64,
50}
51
52type DownloadProgressCallback<'a> = Box<dyn FnMut(&DownloadProgressRecord) -> bool + 'a>;
53pub type DownloadProgressCallbackOption<'a> = Option<DownloadProgressCallback<'a>>;
54
55pub fn download_file<'a, 'b>(
59 url: &str,
60 destination_file: &Path,
61 use_progress_bar: bool,
62 progress_notify_callback: &'a mut DownloadProgressCallbackOption<'b>,
63) -> Result<(), String> {
64 if destination_file.is_file() {
65 return Err(format!("{destination_file:?} already exists"));
66 }
67 let download_start = Instant::now();
68
69 fs::create_dir_all(destination_file.parent().expect("parent"))
70 .map_err(|err| err.to_string())?;
71
72 let mut temp_destination_file = destination_file.to_path_buf();
73 temp_destination_file.set_file_name(format!(
74 "tmp-{}",
75 destination_file
76 .file_name()
77 .expect("file_name")
78 .to_str()
79 .expect("to_str")
80 ));
81
82 let progress_bar = new_spinner_progress_bar();
83 if use_progress_bar {
84 progress_bar.set_message(format!("{TRUCK}Downloading {url}..."));
85 }
86
87 let response = reqwest::blocking::Client::new()
88 .get(url)
89 .send()
90 .and_then(|response| response.error_for_status())
91 .map_err(|err| {
92 progress_bar.finish_and_clear();
93 err.to_string()
94 })?;
95
96 let download_size = {
97 response
98 .headers()
99 .get(reqwest::header::CONTENT_LENGTH)
100 .and_then(|content_length| content_length.to_str().ok())
101 .and_then(|content_length| content_length.parse().ok())
102 .unwrap_or(0)
103 };
104
105 if use_progress_bar {
106 progress_bar.set_length(download_size);
107 progress_bar.set_style(
108 ProgressStyle::default_bar()
109 .template(
110 "{spinner:.green}{msg_wide}[{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})",
111 )
112 .expect("ProgresStyle::template direct input to be correct")
113 .progress_chars("=> "),
114 );
115 progress_bar.set_message(format!("{TRUCK}Downloading~ {url}"));
116 } else {
117 info!("Downloading {} bytes from {}", download_size, url);
118 }
119
120 struct DownloadProgress<'e, 'f, R> {
121 progress_bar: ProgressBar,
122 response: R,
123 last_print: Instant,
124 current_bytes: usize,
125 last_print_bytes: usize,
126 download_size: f32,
127 use_progress_bar: bool,
128 start_time: Instant,
129 callback: &'f mut DownloadProgressCallbackOption<'e>,
130 notification_count: u64,
131 }
132
133 impl<R: Read> Read for DownloadProgress<'_, '_, R> {
134 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
135 let n = self.response.read(buf)?;
136
137 self.current_bytes += n;
138 let total_bytes_f32 = self.current_bytes as f32;
139 let diff_bytes_f32 = (self.current_bytes - self.last_print_bytes) as f32;
140 let last_throughput = diff_bytes_f32 / self.last_print.elapsed().as_secs_f32();
141 let estimated_remaining_time = if last_throughput > 0_f32 {
142 (self.download_size - self.current_bytes as f32) / last_throughput
143 } else {
144 f32::MAX
145 };
146
147 let mut progress_record = DownloadProgressRecord {
148 elapsed_time: self.start_time.elapsed(),
149 last_elapsed_time: self.last_print.elapsed(),
150 last_throughput,
151 total_throughput: self.current_bytes as f32
152 / self.start_time.elapsed().as_secs_f32(),
153 total_bytes: self.download_size as usize,
154 current_bytes: self.current_bytes,
155 percentage_done: 100f32 * (total_bytes_f32 / self.download_size),
156 estimated_remaining_time,
157 notification_count: self.notification_count,
158 };
159 let mut to_update_progress = false;
160 if progress_record.last_elapsed_time.as_secs() > 5 {
161 self.last_print = Instant::now();
162 self.last_print_bytes = self.current_bytes;
163 to_update_progress = true;
164 self.notification_count += 1;
165 progress_record.notification_count = self.notification_count
166 }
167
168 if self.use_progress_bar {
169 self.progress_bar.inc(n as u64);
170 } else if to_update_progress {
171 info!(
172 "downloaded {} bytes {:.1}% {:.1} bytes/s",
173 self.current_bytes,
174 progress_record.percentage_done,
175 progress_record.last_throughput,
176 );
177 }
178
179 if let Some(callback) = self.callback {
180 if to_update_progress && !callback(&progress_record) {
181 info!("Download is aborted by the caller");
182 return Err(io::Error::other("Download is aborted by the caller"));
183 }
184 }
185
186 Ok(n)
187 }
188 }
189
190 let mut source = DownloadProgress::<'b, 'a> {
191 progress_bar,
192 response,
193 last_print: Instant::now(),
194 current_bytes: 0,
195 last_print_bytes: 0,
196 download_size: (download_size as f32).max(1f32),
197 use_progress_bar,
198 start_time: Instant::now(),
199 callback: progress_notify_callback,
200 notification_count: 0,
201 };
202
203 File::create(&temp_destination_file)
204 .and_then(|mut file| std::io::copy(&mut source, &mut file))
205 .map_err(|err| format!("Unable to write {temp_destination_file:?}: {err:?}"))?;
206
207 source.progress_bar.finish_and_clear();
208 info!(
209 " {}{}",
210 SPARKLE,
211 format!(
212 "Downloaded {} ({} bytes) in {:?}",
213 url,
214 download_size,
215 Instant::now().duration_since(download_start),
216 )
217 );
218
219 std::fs::rename(temp_destination_file, destination_file)
220 .map_err(|err| format!("Unable to rename: {err:?}"))?;
221
222 Ok(())
223}