From 3fa30db7d8cb14d5a8ef3028c2e42d74f1d615a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orhun=20Parmaks=C4=B1z?= Date: Sun, 8 May 2022 22:03:48 +0300 Subject: [PATCH] cli+github: show download progress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spinner implementation has been improved to show the remaining and total amount of bytes to download Signed-off-by: Orhun Parmaksız --- src/cli/handlers/download.rs | 23 ++++++++++++++++++----- src/cli/spinner.rs | 16 +++++++++++++--- src/github/error.rs | 4 ++++ src/github/mod.rs | 12 ++++++++---- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/cli/handlers/download.rs b/src/cli/handlers/download.rs index acf187d..255c447 100644 --- a/src/cli/handlers/download.rs +++ b/src/cli/handlers/download.rs @@ -1,4 +1,6 @@ +use std::cmp; use std::fs::File; +use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use crate::cli::get_env; @@ -107,11 +109,22 @@ impl DownloadHandler { ) -> Result<(), HandlerError> { let spinner = Spinner::download(&selected_asset.name, output_path); spinner.start(); - let mut stream = + let (mut stream, content_length) = github::download_asset(client, selected_asset).map_err(Self::download_error)?; + spinner.set_max_progress(content_length); let mut destination = Self::create_file(output_path)?; - std::io::copy(&mut stream, &mut destination) - .map_err(|x| Self::copy_err(&selected_asset.name, output_path, x))?; + let mut downloaded = 0; + let mut buffer = [0; 1024]; + while let Ok(bytes_read) = stream.read(&mut buffer) { + if bytes_read == 0 { + break; + } + destination + .write(&buffer[..bytes_read]) + .map_err(|x| Self::write_err(&selected_asset.name, output_path, x))?; + downloaded = cmp::min(downloaded + bytes_read as u64, content_length); + spinner.update_progress(downloaded); + } spinner.stop(); Ok(()) } @@ -151,9 +164,9 @@ impl DownloadHandler { }) } - pub fn copy_err(asset_name: &str, output_path: &Path, error: std::io::Error) -> HandlerError { + pub fn write_err(asset_name: &str, output_path: &Path, error: std::io::Error) -> HandlerError { HandlerError::new(format!( - "Error copying {} to {}: {}", + "Error saving {} to {}: {}", asset_name, output_path.display(), error diff --git a/src/cli/spinner.rs b/src/cli/spinner.rs index afb51d2..e108271 100644 --- a/src/cli/spinner.rs +++ b/src/cli/spinner.rs @@ -13,12 +13,12 @@ pub struct Spinner { } impl Spinner { - pub fn new(message: String, end_message: String) -> Self { + pub fn new(message: String, end_message: String, template: &str) -> Self { let pb = ProgressBar::new_spinner(); pb.set_style( ProgressStyle::default_spinner() .tick_strings(TICKS) - .template("{spinner:.blue} {msg}"), + .template(template), ); pb.set_message(message); Self { pb, end_message } @@ -38,6 +38,14 @@ impl Spinner { println!("{}", message); } + pub fn set_max_progress(&self, progress: u64) { + self.pb.set_length(progress); + } + + pub fn update_progress(&self, progress: u64) { + self.pb.set_position(progress); + } + pub fn download(download_asset: &str, output_path: &Path) -> Spinner { Spinner::new( format!("Downloading {}", Color::new(download_asset).bold()), @@ -45,6 +53,7 @@ impl Spinner { "Saved to: {}", Color::new(&format!("{}", output_path.display())).bold() ), + "{msg}\n{percent}% [{wide_bar}] {bytes}/{total_bytes} ({eta})", ) } @@ -52,10 +61,11 @@ impl Spinner { Spinner::new( "Installing".into(), format!("{}", Color::new("Installation completed!").green()), + "{spinner:.blue} {msg}", ) } pub fn no_messages() -> Spinner { - Spinner::new(String::new(), String::new()) + Spinner::new(String::new(), String::new(), "{spinner:.blue} {msg}") } } diff --git a/src/github/error.rs b/src/github/error.rs index 3ec6f81..5e98011 100644 --- a/src/github/error.rs +++ b/src/github/error.rs @@ -4,6 +4,7 @@ use std::fmt::Formatter; pub enum GithubError { Http(Box), JsonDeserialization(std::io::Error), + InvalidContentLength, RepositoryOrReleaseNotFound, RateLimitExceeded, Unauthorized, @@ -28,6 +29,9 @@ impl std::fmt::Display for GithubError { GithubError::JsonDeserialization(e) => { f.write_str(&format!("Error deserializing response: {}", e)) } + GithubError::InvalidContentLength => { + f.write_str("Content-Length header is missing or invalid") + } GithubError::RepositoryOrReleaseNotFound => { f.write_str("Repository or release not found") } diff --git a/src/github/mod.rs b/src/github/mod.rs index c66b3c6..d0a2723 100644 --- a/src/github/mod.rs +++ b/src/github/mod.rs @@ -49,11 +49,15 @@ fn deserialize(response: ureq::Response) -> Result { pub fn download_asset( client: &GithubClient, asset: &Asset, -) -> Result { - client +) -> Result<(impl Read + Send, u64), GithubError> { + let response = client .get(&asset.download_url) .set("Accept", "application/octet-stream") .call() - .map_err(GithubError::from) - .map(|response| response.into_reader()) + .map_err(GithubError::from)?; + let content_length = response + .header("Content-Length") + .and_then(|v| v.parse().ok()) + .ok_or(GithubError::InvalidContentLength)?; + Ok((response.into_reader(), content_length)) }