|
| 1 | +use std::collections::HashMap; |
1 | 2 | use std::fs; |
2 | 3 | use std::ops; |
3 | 4 | use std::path::{Path, PathBuf}; |
| 5 | +use std::sync::Mutex; |
| 6 | +use std::time::{Duration, Instant}; |
4 | 7 |
|
5 | 8 | use anyhow::{Context, Result, anyhow}; |
| 9 | +use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; |
6 | 10 | use sha2::{Digest, Sha256}; |
7 | | -use tracing::debug; |
8 | | -use tracing::warn; |
| 11 | +use tracing::{debug, warn}; |
9 | 12 | use url::Url; |
10 | 13 |
|
11 | 14 | use crate::config::Cfg; |
12 | 15 | use crate::dist::temp; |
13 | | -use crate::download::download_file; |
14 | | -use crate::download::download_file_with_resume; |
| 16 | +use crate::download::{download_file, download_file_with_resume}; |
15 | 17 | use crate::errors::*; |
16 | 18 | use crate::notifications::Notification; |
17 | 19 | use crate::process::Process; |
@@ -201,6 +203,159 @@ impl<'a> DownloadCfg<'a> { |
201 | 203 | } |
202 | 204 | } |
203 | 205 |
|
| 206 | +pub(crate) struct Notifier { |
| 207 | + tracker: Mutex<DownloadTracker>, |
| 208 | +} |
| 209 | + |
| 210 | +impl Notifier { |
| 211 | + pub(crate) fn new(quiet: bool, process: &Process) -> Self { |
| 212 | + Self { |
| 213 | + tracker: Mutex::new(DownloadTracker::new(!quiet, process)), |
| 214 | + } |
| 215 | + } |
| 216 | + |
| 217 | + pub(crate) fn handle(&self, n: Notification<'_>) { |
| 218 | + self.tracker.lock().unwrap().handle_notification(&n); |
| 219 | + } |
| 220 | +} |
| 221 | + |
| 222 | +/// Tracks download progress and displays information about it to a terminal. |
| 223 | +/// |
| 224 | +/// *not* safe for tracking concurrent downloads yet - it is basically undefined |
| 225 | +/// what will happen. |
| 226 | +pub(crate) struct DownloadTracker { |
| 227 | + /// MultiProgress bar for the downloads. |
| 228 | + multi_progress_bars: MultiProgress, |
| 229 | + /// Mapping of URLs being downloaded to their corresponding progress bars. |
| 230 | + /// The `Option<Instant>` represents the instant where the download is being retried, |
| 231 | + /// allowing us delay the reappearance of the progress bar so that the user can see |
| 232 | + /// the message "retrying download" for at least a second. |
| 233 | + /// Without it, the progress bar would reappear immediately, not allowing the user to |
| 234 | + /// correctly see the message, before the progress bar starts again. |
| 235 | + file_progress_bars: HashMap<String, (ProgressBar, Option<Instant>)>, |
| 236 | +} |
| 237 | + |
| 238 | +impl DownloadTracker { |
| 239 | + /// Creates a new DownloadTracker. |
| 240 | + pub(crate) fn new(display_progress: bool, process: &Process) -> Self { |
| 241 | + let multi_progress_bars = MultiProgress::with_draw_target(if display_progress { |
| 242 | + process.progress_draw_target() |
| 243 | + } else { |
| 244 | + ProgressDrawTarget::hidden() |
| 245 | + }); |
| 246 | + |
| 247 | + Self { |
| 248 | + multi_progress_bars, |
| 249 | + file_progress_bars: HashMap::new(), |
| 250 | + } |
| 251 | + } |
| 252 | + |
| 253 | + pub(crate) fn handle_notification(&mut self, n: &Notification<'_>) { |
| 254 | + match *n { |
| 255 | + Notification::DownloadContentLengthReceived(content_len, url) => { |
| 256 | + if let Some(url) = url { |
| 257 | + self.content_length_received(content_len, url); |
| 258 | + } |
| 259 | + } |
| 260 | + Notification::DownloadDataReceived(data, url) => { |
| 261 | + if let Some(url) = url { |
| 262 | + self.data_received(data.len(), url); |
| 263 | + } |
| 264 | + } |
| 265 | + Notification::DownloadFinished(url) => { |
| 266 | + if let Some(url) = url { |
| 267 | + self.download_finished(url); |
| 268 | + } |
| 269 | + } |
| 270 | + Notification::DownloadFailed(url) => { |
| 271 | + self.download_failed(url); |
| 272 | + debug!("download failed"); |
| 273 | + } |
| 274 | + Notification::DownloadingComponent(component, _, _, url) => { |
| 275 | + self.create_progress_bar(component.to_owned(), url.to_owned()); |
| 276 | + } |
| 277 | + Notification::RetryingDownload(url) => { |
| 278 | + self.retrying_download(url); |
| 279 | + } |
| 280 | + } |
| 281 | + } |
| 282 | + |
| 283 | + /// Creates a new ProgressBar for the given component. |
| 284 | + pub(crate) fn create_progress_bar(&mut self, component: String, url: String) { |
| 285 | + let pb = ProgressBar::hidden(); |
| 286 | + pb.set_style( |
| 287 | + ProgressStyle::with_template( |
| 288 | + "{msg:>12.bold} [{bar:40}] {bytes}/{total_bytes} ({bytes_per_sec}, ETA: {eta})", |
| 289 | + ) |
| 290 | + .unwrap() |
| 291 | + .progress_chars("## "), |
| 292 | + ); |
| 293 | + pb.set_message(component); |
| 294 | + self.multi_progress_bars.add(pb.clone()); |
| 295 | + self.file_progress_bars.insert(url, (pb, None)); |
| 296 | + } |
| 297 | + |
| 298 | + /// Sets the length for a new ProgressBar and gives it a style. |
| 299 | + pub(crate) fn content_length_received(&mut self, content_len: u64, url: &str) { |
| 300 | + if let Some((pb, _)) = self.file_progress_bars.get(url) { |
| 301 | + pb.reset(); |
| 302 | + pb.set_length(content_len); |
| 303 | + } |
| 304 | + } |
| 305 | + |
| 306 | + /// Notifies self that data of size `len` has been received. |
| 307 | + pub(crate) fn data_received(&mut self, len: usize, url: &str) { |
| 308 | + let Some((pb, retry_time)) = self.file_progress_bars.get_mut(url) else { |
| 309 | + return; |
| 310 | + }; |
| 311 | + pb.inc(len as u64); |
| 312 | + if !retry_time.is_some_and(|instant| instant.elapsed() > Duration::from_secs(1)) { |
| 313 | + return; |
| 314 | + } |
| 315 | + *retry_time = None; |
| 316 | + pb.set_style( |
| 317 | + ProgressStyle::with_template( |
| 318 | + "{msg:>12.bold} [{bar:40}] {bytes}/{total_bytes} ({bytes_per_sec}, ETA: {eta})", |
| 319 | + ) |
| 320 | + .unwrap() |
| 321 | + .progress_chars("## "), |
| 322 | + ); |
| 323 | + } |
| 324 | + |
| 325 | + /// Notifies self that the download has finished. |
| 326 | + pub(crate) fn download_finished(&mut self, url: &str) { |
| 327 | + let Some((pb, _)) = self.file_progress_bars.get(url) else { |
| 328 | + return; |
| 329 | + }; |
| 330 | + pb.set_style( |
| 331 | + ProgressStyle::with_template("{msg:>12.bold} downloaded {total_bytes} in {elapsed}") |
| 332 | + .unwrap(), |
| 333 | + ); |
| 334 | + pb.finish(); |
| 335 | + } |
| 336 | + |
| 337 | + /// Notifies self that the download has failed. |
| 338 | + pub(crate) fn download_failed(&mut self, url: &str) { |
| 339 | + let Some((pb, _)) = self.file_progress_bars.get(url) else { |
| 340 | + return; |
| 341 | + }; |
| 342 | + pb.set_style( |
| 343 | + ProgressStyle::with_template("{msg:>12.bold} download failed after {elapsed}") |
| 344 | + .unwrap(), |
| 345 | + ); |
| 346 | + pb.finish(); |
| 347 | + } |
| 348 | + |
| 349 | + /// Notifies self that the download is being retried. |
| 350 | + pub(crate) fn retrying_download(&mut self, url: &str) { |
| 351 | + let Some((pb, retry_time)) = self.file_progress_bars.get_mut(url) else { |
| 352 | + return; |
| 353 | + }; |
| 354 | + *retry_time = Some(Instant::now()); |
| 355 | + pb.set_style(ProgressStyle::with_template("{msg:>12.bold} retrying download").unwrap()); |
| 356 | + } |
| 357 | +} |
| 358 | + |
204 | 359 | fn file_hash(path: &Path, notify_handler: &dyn Fn(Notification<'_>)) -> Result<String> { |
205 | 360 | let mut hasher = Sha256::new(); |
206 | 361 | let mut downloaded = utils::FileReaderWithProgress::new_file(path, notify_handler)?; |
|
0 commit comments