diff --git a/rust-install/src/utils/mod.rs b/rust-install/src/utils/mod.rs index a1abfac3de..85ddd0fb9b 100644 --- a/rust-install/src/utils/mod.rs +++ b/rust-install/src/utils/mod.rs @@ -22,6 +22,12 @@ pub enum Notification<'a> { CopyingDirectory(&'a Path, &'a Path), RemovingDirectory(&'a str, &'a Path), DownloadingFile(&'a hyper::Url, &'a Path), + /// Received the Content-Length of the to-be downloaded data. + DownloadContentLengthReceived(u64), + /// Received some data. + DownloadDataReceived(usize), + /// Download has finished. + DownloadFinished, NoCanonicalPath(&'a Path), } @@ -133,9 +139,12 @@ impl<'a> Notification<'a> { use self::Notification::*; match *self { CreatingDirectory(_, _) | RemovingDirectory(_, _) => NotificationLevel::Verbose, - LinkingDirectory(_, _) | CopyingDirectory(_, _) | DownloadingFile(_, _) => { - NotificationLevel::Normal - } + LinkingDirectory(_, _) | + CopyingDirectory(_, _) | + DownloadingFile(_, _) | + DownloadContentLengthReceived(_) | + DownloadDataReceived(_) | + DownloadFinished => NotificationLevel::Normal, NoCanonicalPath(_) => NotificationLevel::Warn, } } @@ -154,6 +163,9 @@ impl<'a> Display for Notification<'a> { write!(f, "removing {} directory: '{}'", name, path.display()) } DownloadingFile(url, _) => write!(f, "downloading file from: '{}'", url), + DownloadContentLengthReceived(len) => write!(f, "download size is: '{}'", len), + DownloadDataReceived(len) => write!(f, "received some data of size {}", len), + DownloadFinished => write!(f, "download finished"), NoCanonicalPath(path) => write!(f, "could not canonicalize path: '{}'", path.display()), } } @@ -409,7 +421,7 @@ pub fn download_file(url: hyper::Url, notify_handler: NotifyHandler) -> Result<()> { notify_handler.call(Notification::DownloadingFile(&url, path)); - raw::download_file(url.clone(), path, hasher).map_err(|e| { + raw::download_file(url.clone(), path, hasher, notify_handler).map_err(|e| { Error::DownloadingFile { url: url, path: PathBuf::from(path), diff --git a/rust-install/src/utils/raw.rs b/rust-install/src/utils/raw.rs index 0a3b17062c..b72471eee9 100644 --- a/rust-install/src/utils/raw.rs +++ b/rust-install/src/utils/raw.rs @@ -1,3 +1,4 @@ +use utils::NotifyHandler; use std::fs; use std::path::{Path, PathBuf}; @@ -169,8 +170,12 @@ impl fmt::Display for DownloadError { pub fn download_file>(url: hyper::Url, path: P, - mut hasher: Option<&mut Hasher>) + mut hasher: Option<&mut Hasher>, + notify_handler: NotifyHandler) -> DownloadResult<()> { + use hyper::header::ContentLength; + use utils::Notification; + let client = Client::new(); let mut res = try!(client.get(url).send().map_err(DownloadError::Network)); @@ -183,6 +188,10 @@ pub fn download_file>(url: hyper::Url, let mut file = try!(fs::File::create(path).map_err(DownloadError::File)); + if let Some(len) = res.headers.get::().cloned() { + notify_handler.call(Notification::DownloadContentLengthReceived(len.0)); + } + loop { let bytes_read = try!(io::Read::read(&mut res, &mut buffer) .map_err(hyper::Error::Io) @@ -195,8 +204,10 @@ pub fn download_file>(url: hyper::Url, } try!(io::Write::write_all(&mut file, &mut buffer[0..bytes_read]) .map_err(DownloadError::File)); + notify_handler.call(Notification::DownloadDataReceived(bytes_read)); } else { try!(file.sync_data().map_err(DownloadError::File)); + notify_handler.call(Notification::DownloadFinished); return Ok(()); } } diff --git a/src/config.rs b/src/config.rs index 7b1d6f5bc2..cf696164dd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -53,8 +53,7 @@ impl Cfg { // Set up the multirust home directory let multirust_dir = env::var_os("MULTIRUST_HOME") .and_then(utils::if_not_empty) - .map_or_else(|| data_dir.join(".multirust"), - PathBuf::from); + .map_or_else(|| data_dir.join(".multirust"), PathBuf::from); try!(utils::ensure_dir_exists("home", &multirust_dir, ntfy!(¬ify_handler))); @@ -87,8 +86,7 @@ impl Cfg { let dist_root_url = env::var("MULTIRUST_DIST_ROOT") .ok() .and_then(utils::if_not_empty) - .map_or(Cow::Borrowed(dist::DEFAULT_DIST_ROOT), - Cow::Owned); + .map_or(Cow::Borrowed(dist::DEFAULT_DIST_ROOT), Cow::Owned); Ok(Cfg { multirust_dir: multirust_dir, diff --git a/src/errors.rs b/src/errors.rs index fdd214bd67..a9cee9c83c 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -112,7 +112,9 @@ impl<'a> Display for Notification<'a> { to_ver) } MetadataUpgradeNotNeeded(ver) => { - write!(f, "nothing to upgrade: metadata version is already '{}'", ver) + write!(f, + "nothing to upgrade: metadata version is already '{}'", + ver) } WritingMetadataVersion(ver) => write!(f, "writing metadata version: '{}'", ver), ReadMetadataVersion(ver) => write!(f, "read metadata version: '{}'", ver), diff --git a/src/main.rs b/src/main.rs index 9aa4e00a9a..045ec9d073 100644 --- a/src/main.rs +++ b/src/main.rs @@ -75,27 +75,100 @@ fn info_fmt(args: fmt::Arguments) { } fn set_globals(m: Option<&ArgMatches>) -> Result { + use std::rc::Rc; + use std::cell::{Cell, RefCell}; + + struct DownloadDisplayer { + content_len: Cell>, + total_downloaded: Cell, + term: RefCell>, + } + + /// Human readable representation of data size in bytes + struct HumanReadable(T); + + impl + Clone> fmt::Display for HumanReadable { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + const KIB: f64 = 1024.0; + const MIB: f64 = 1048576.0; + let size: f64 = self.0.clone().into(); + + if size >= MIB { + write!(f, "{:.2} MiB", size / MIB) + } else if size >= KIB { + write!(f, "{:.2} KiB", size / KIB) + } else { + write!(f, "{} B", size) + } + } + } + + let download_displayer = Rc::new(DownloadDisplayer { + content_len: Cell::new(None), + total_downloaded: Cell::new(0), + term: RefCell::new(term::stdout().expect("Failed to open terminal.")), + }); + // Base config let verbose = m.map_or(false, |m| m.is_present("verbose")); Cfg::from_env(shared_ntfy!(move |n: Notification| { use multirust::notify::NotificationLevel::*; - match n.level() { - Verbose => { - if verbose { - println!("{}", n); - } + use rust_install::Notification as In; + use rust_install::utils::Notification as Un; + + match n { + Notification::Install(In::Utils(Un::DownloadContentLengthReceived(len))) => { + let dd = download_displayer.clone(); + dd.content_len.set(Some(len)); + dd.total_downloaded.set(0); } - Normal => { - println!("{}", n); - } - Info => { - info!("{}", n); + Notification::Install(In::Utils(Un::DownloadDataReceived(len))) => { + let dd = download_displayer.clone(); + let mut t = dd.term.borrow_mut(); + dd.total_downloaded.set(dd.total_downloaded.get() + len); + let total_downloaded = dd.total_downloaded.get(); + let total_h = HumanReadable(total_downloaded as f64); + + match dd.content_len.get() { + Some(content_len) => { + let percent = (total_downloaded as f64 / content_len as f64) * 100.; + let content_len_h = HumanReadable(content_len as f64); + let _ = write!(t, "{} / {} ({:.2}%)", total_h, content_len_h, percent); + } + None => { + let _ = write!(t, "{}", total_h); + } + } + // delete_line() doesn't seem to clear the line properly. + // Instead, let's just print some whitespace to clear it. + let _ = write!(t, " "); + let _ = t.flush(); + let _ = t.carriage_return(); } - Warn => { - warn!("{}", n); + Notification::Install(In::Utils(Un::DownloadFinished)) => { + let dd = download_displayer.clone(); + let _ = writeln!(dd.term.borrow_mut(), ""); } - Error => { - err!("{}", n); + n => { + match n.level() { + Verbose => { + if verbose { + println!("{}", n); + } + } + Normal => { + println!("{}", n); + } + Info => { + info!("{}", n); + } + Warn => { + warn!("{}", n); + } + Error => { + err!("{}", n); + } + } } } })) @@ -227,8 +300,11 @@ fn run_multirust() -> Result<()> { let cfg = try!(set_globals(Some(&app_matches))); match app_matches.subcommand_name() { - Some("upgrade-data") | Some("delete-data") | Some("install") | - Some("uninstall") | None => {} // Don't need consistent metadata + Some("upgrade-data") | + Some("delete-data") | + Some("install") | + Some("uninstall") | + None => {} // Don't need consistent metadata Some(_) => { try!(cfg.check_metadata_version()); } @@ -709,7 +785,7 @@ fn ask(question: &str) -> Option { } fn delete_data(cfg: &Cfg, m: &ArgMatches) -> Result<()> { - if !m.is_present("no-prompt") && + if !m.is_present("no-prompt") && !ask("This will delete all toolchains, overrides, aliases, and other multirust data \ associated with this user. Continue?") .unwrap_or(false) {