diff --git a/Cargo.lock b/Cargo.lock index 30d881b60a..006f28d853 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7920,6 +7920,7 @@ dependencies = [ "tokio-stream", "toml", "tracing", + "url", "zstd", ] diff --git a/crates/cli/commands/Cargo.toml b/crates/cli/commands/Cargo.toml index 590b2352bc..2b044f26ba 100644 --- a/crates/cli/commands/Cargo.toml +++ b/crates/cli/commands/Cargo.toml @@ -83,6 +83,7 @@ backon.workspace = true secp256k1 = { workspace = true, features = ["global-context", "std", "recovery"] } tokio-stream.workspace = true reqwest.workspace = true +url.workspace = true metrics.workspace = true # io diff --git a/crates/cli/commands/src/download.rs b/crates/cli/commands/src/download.rs index 20bc7081f0..05d5d730dd 100644 --- a/crates/cli/commands/src/download.rs +++ b/crates/cli/commands/src/download.rs @@ -16,6 +16,7 @@ use std::{ use tar::Archive; use tokio::task; use tracing::info; +use url::Url; use zstd::stream::read::Decoder as ZstdDecoder; const BYTE_UNITS: [&str; 4] = ["B", "KB", "MB", "GB"]; @@ -170,12 +171,14 @@ struct DownloadProgress { downloaded: u64, total_size: u64, last_displayed: Instant, + started_at: Instant, } impl DownloadProgress { /// Creates new progress tracker with given total size fn new(total_size: u64) -> Self { - Self { downloaded: 0, total_size, last_displayed: Instant::now() } + let now = Instant::now(); + Self { downloaded: 0, total_size, last_displayed: now, started_at: now } } /// Converts bytes to human readable format (B, KB, MB, GB) @@ -191,6 +194,18 @@ impl DownloadProgress { format!("{:.2} {}", size, BYTE_UNITS[unit_index]) } + /// Format duration as human readable string + fn format_duration(duration: Duration) -> String { + let secs = duration.as_secs(); + if secs < 60 { + format!("{secs}s") + } else if secs < 3600 { + format!("{}m {}s", secs / 60, secs % 60) + } else { + format!("{}h {}m", secs / 3600, (secs % 3600) / 60) + } + } + /// Updates progress bar fn update(&mut self, chunk_size: u64) -> Result<()> { self.downloaded += chunk_size; @@ -201,8 +216,24 @@ impl DownloadProgress { let formatted_total = Self::format_size(self.total_size); let progress = (self.downloaded as f64 / self.total_size as f64) * 100.0; + // Calculate ETA based on current speed + let elapsed = self.started_at.elapsed(); + let eta = if self.downloaded > 0 { + let remaining = self.total_size.saturating_sub(self.downloaded); + let speed = self.downloaded as f64 / elapsed.as_secs_f64(); + if speed > 0.0 { + Duration::from_secs_f64(remaining as f64 / speed) + } else { + Duration::ZERO + } + } else { + Duration::ZERO + }; + let eta_str = Self::format_duration(eta); + + // Pad with spaces to clear any previous longer line print!( - "\rDownloading and extracting... {progress:.2}% ({formatted_downloaded} / {formatted_total})", + "\rDownloading and extracting... {progress:.2}% ({formatted_downloaded} / {formatted_total}) ETA: {eta_str} ", ); io::stdout().flush()?; self.last_displayed = Instant::now(); @@ -246,12 +277,18 @@ enum CompressionFormat { impl CompressionFormat { /// Detect compression format from file extension fn from_url(url: &str) -> Result { - if url.ends_with(EXTENSION_TAR_LZ4) { + let path = + Url::parse(url).map(|u| u.path().to_string()).unwrap_or_else(|_| url.to_string()); + + if path.ends_with(EXTENSION_TAR_LZ4) { Ok(Self::Lz4) - } else if url.ends_with(EXTENSION_TAR_ZSTD) { + } else if path.ends_with(EXTENSION_TAR_ZSTD) { Ok(Self::Zstd) } else { - Err(eyre::eyre!("Unsupported file format. Expected .tar.lz4 or .tar.zst, got: {}", url)) + Err(eyre::eyre!( + "Unsupported file format. Expected .tar.lz4 or .tar.zst, got: {}", + path + )) } } }