diff --git a/bin/oli/Cargo.lock b/bin/oli/Cargo.lock index 94450461c9ca..0ef096e042ec 100644 --- a/bin/oli/Cargo.lock +++ b/bin/oli/Cargo.lock @@ -742,6 +742,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width 0.1.14", + "windows-sys 0.52.0", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -970,6 +983,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "equivalent" version = "1.0.1" @@ -1512,6 +1531,19 @@ dependencies = [ "hashbrown 0.15.0", ] +[[package]] +name = "indicatif" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width 0.2.0", + "web-time", +] + [[package]] name = "inout" version = "0.1.3" @@ -1670,7 +1702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -1918,6 +1950,12 @@ dependencies = [ "libm", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "object" version = "0.36.5" @@ -1936,6 +1974,7 @@ dependencies = [ "clap", "dirs", "futures", + "indicatif", "opendal", "serde", "tempfile", @@ -2225,6 +2264,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + [[package]] name = "powerfmt" version = "0.2.0" @@ -3548,6 +3593,18 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "untrusted" version = "0.9.0" @@ -3723,6 +3780,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki" version = "0.22.4" diff --git a/bin/oli/Cargo.toml b/bin/oli/Cargo.toml index 97730b5d1416..19db567782e2 100644 --- a/bin/oli/Cargo.toml +++ b/bin/oli/Cargo.toml @@ -58,6 +58,7 @@ anyhow = "1" clap = { version = "4", features = ["cargo", "string", "derive", "deprecated"] } dirs = "5.0.1" futures = "0.3" +indicatif = "0.17.9" opendal = { version = "0.50.0", path = "../../core", features = [ # These are default features before v0.46. TODO: change to optional features "services-azblob", diff --git a/bin/oli/src/commands/cp.rs b/bin/oli/src/commands/cp.rs index e96f65097c3d..ecadddfa010f 100644 --- a/bin/oli/src/commands/cp.rs +++ b/bin/oli/src/commands/cp.rs @@ -15,6 +15,10 @@ // specific language governing permissions and limitations // under the License. +use futures::AsyncBufReadExt; +use indicatif::ProgressBar; +use indicatif::ProgressStyle; +use opendal::Metadata; use std::path::Path; use anyhow::Result; @@ -24,6 +28,20 @@ use futures::TryStreamExt; use crate::config::Config; use crate::params::config::ConfigParams; +/// Template for the progress bar display. +/// +/// The template includes: +/// - `{spinner:.green}`: A green spinner to indicate ongoing progress. +/// - `{elapsed_precise}`: The precise elapsed time. +/// - `{bar:40.cyan/blue}`: A progress bar with a width of 40 characters, +/// cyan for the completed portion and blue for the remaining portion. +/// - `{bytes}/{total_bytes}`: The number of bytes copied so far and the total bytes to be copied. +/// - `{eta}`: The estimated time of arrival (completion). +const PROGRESS_BAR_TEMPLATE: &str = + "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})"; + +const PROGRESS_CHARS: &str = "#>-"; + #[derive(Debug, clap::Parser)] #[command(name = "cp", about = "Copy object", disable_version_flag = true)] pub struct CopyCmd { @@ -53,8 +71,9 @@ impl CopyCmd { let buf_reader = reader .into_futures_async_read(0..src_meta.content_length()) .await?; - futures::io::copy_buf(buf_reader, &mut dst_w).await?; - // flush data + + let copy_progress = CopyProgress::new(&src_meta, src_path.clone()); + copy_progress.copy(buf_reader, &mut dst_w).await?; dst_w.close().await?; return Ok(()); } @@ -78,15 +97,59 @@ impl CopyCmd { .into_futures_async_read(0..meta.content_length()) .await?; + let copy_progress = CopyProgress::new(&meta, de.path().to_string()); let mut writer = dst_op .writer(&dst_root.join(fp).to_string_lossy()) .await? .into_futures_async_write(); - println!("Copying {}", de.path()); - futures::io::copy_buf(buf_reader, &mut writer).await?; + copy_progress.copy(buf_reader, &mut writer).await?; writer.close().await?; } Ok(()) } } + +/// Helper struct to display progress of a copy operation. +struct CopyProgress { + progress_bar: ProgressBar, + path: String, +} + +impl CopyProgress { + fn new(meta: &Metadata, path: String) -> Self { + let pb = ProgressBar::new(meta.content_length()); + pb.set_style( + ProgressStyle::default_bar() + .template(PROGRESS_BAR_TEMPLATE) + .expect("invalid template") + .progress_chars(PROGRESS_CHARS), + ); + Self { + progress_bar: pb, + path, + } + } + + async fn copy(&self, mut reader: R, writer: &mut W) -> std::io::Result + where + R: futures::AsyncBufRead + Unpin, + W: futures::AsyncWrite + Unpin + ?Sized, + { + let mut written = 0; + loop { + let buf = reader.fill_buf().await?; + if buf.is_empty() { + break; + } + writer.write_all(buf).await?; + let len = buf.len(); + reader.consume_unpin(len); + written += len as u64; + self.progress_bar.inc(len as u64); + } + self.progress_bar.finish_and_clear(); + println!("Finish {}", self.path); + Ok(written) + } +}