Implements progress reporting and pv support for the cli app.
This commit is contained in:
55
Cargo.lock
generated
55
Cargo.lock
generated
@@ -580,6 +580,19 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"unicode-width",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.2"
|
||||
@@ -1001,6 +1014,12 @@ version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.1"
|
||||
@@ -1876,6 +1895,19 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indicatif"
|
||||
version = "0.17.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
|
||||
dependencies = [
|
||||
"console",
|
||||
"number_prefix",
|
||||
"portable-atomic",
|
||||
"unicode-width",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "infer"
|
||||
version = "0.19.0"
|
||||
@@ -2363,11 +2395,18 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "number_prefix"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
|
||||
[[package]]
|
||||
name = "nut"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"indicatif",
|
||||
"nextcloud_client",
|
||||
"open",
|
||||
"reqwest 0.12.28",
|
||||
@@ -4476,6 +4515,12 @@ version = "1.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@@ -4688,6 +4733,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 = "web_atoms"
|
||||
version = "0.2.6"
|
||||
|
||||
16
Tasks.md
16
Tasks.md
@@ -69,7 +69,6 @@ This document defines the complete project roadmap and task tracking system for
|
||||
|
||||
| ID | Title | Status | Type |
|
||||
|---|---|---|---|
|
||||
| [NUT-011](#nut-011) | Implement CLI Progress Reporting + pv Support | Triage | Feature |
|
||||
| [NUT-012](#nut-012) | Implement GUI (Tauri) Frontend | Triage | Feature |
|
||||
| [NUT-013](#nut-013) | Implement GUI File Queue + Drag-and-Drop | Triage | Feature |
|
||||
| [NUT-014](#nut-014) | Implement GUI Credential Management UI | Triage | Feature |
|
||||
@@ -90,6 +89,7 @@ This document defines the complete project roadmap and task tracking system for
|
||||
| [NUT-008](#nut-008) | Implement CLI Frontend | Fixed | Feature |
|
||||
| [NUT-009](#nut-009) | Implement CLI Output Formatting Options | Fixed | Feature |
|
||||
| [NUT-010](#nut-010) | Implement CLI Multi-file Upload Support | Fixed | Feature |
|
||||
| [NUT-011](#nut-011) | Implement CLI Progress Reporting + pv Support | Fixed | Feature |
|
||||
|
||||
---
|
||||
|
||||
@@ -295,19 +295,21 @@ Support uploading multiple files in a single CLI invocation, including recursive
|
||||
- NUT-009
|
||||
|
||||
|
||||
<a id="nut-011" class="task" data-status="triage" data-task-type="feature"></a>
|
||||
<a id="nut-011" class="task" data-status="done" data-task-type="feature"></a>
|
||||
### Implement CLI Progress Reporting + pv Support
|
||||
**ID:** NUT-011
|
||||
**Status:** Triage
|
||||
**Status:** Fixed
|
||||
**Type:** Feature
|
||||
|
||||
**Description:**
|
||||
Integrate with `pv` for progress reporting and support streaming uploads.
|
||||
Integrate rich progress reporting with `indicatif` (speed, ETA, byte counters), pipe-friendly detection (e.g. `pv` and headless/script pipes), and manual size hints for stdin streams.
|
||||
|
||||
**Requirements:**
|
||||
- [ ] Detect file size
|
||||
- [ ] Report progress to stdout
|
||||
- [ ] Support piping from `pv`
|
||||
- [x] Detect file size & support `--size` hint for stdin streams
|
||||
- [x] Rich terminal progress bar with speed (MB/s), ETA, bytes transferred, and percentage
|
||||
- [x] Auto-detect TTY: silence progress bars when piped to downstream tools or files
|
||||
- [x] Support explicit `--no-progress` flag
|
||||
- [x] Support piping directly from `pv` or Unix pipelines without display conflicts
|
||||
|
||||
**Dependencies:**
|
||||
- NUT-003
|
||||
|
||||
@@ -12,3 +12,4 @@ open = "5.3"
|
||||
url = "2.5"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
indicatif = "0.17"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
|
||||
use serde::Serialize;
|
||||
use std::fs;
|
||||
use std::io::{self, Read};
|
||||
use std::io::{self, IsTerminal, Read};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -113,6 +114,14 @@ struct UploadArgs {
|
||||
#[arg(short = 'c', long)]
|
||||
continue_on_error: bool,
|
||||
|
||||
/// Disable terminal progress bars
|
||||
#[arg(long)]
|
||||
no_progress: bool,
|
||||
|
||||
/// Expected size in bytes when uploading via stdin (enables accurate progress)
|
||||
#[arg(long)]
|
||||
size: Option<u64>,
|
||||
|
||||
/// Upload content from standard input (stdin)
|
||||
#[arg(long)]
|
||||
stdin: bool,
|
||||
@@ -265,6 +274,10 @@ async fn handle_upload(args: UploadArgs) -> Result<()> {
|
||||
None => CredentialStore::create_client_for_default()?,
|
||||
};
|
||||
|
||||
let show_progress = !args.no_progress
|
||||
&& !args.format.is_machine_readable()
|
||||
&& io::stderr().is_terminal();
|
||||
|
||||
let mut results: Vec<UploadRecord> = Vec::new();
|
||||
|
||||
// Stdin Upload
|
||||
@@ -287,10 +300,10 @@ async fn handle_upload(args: UploadArgs) -> Result<()> {
|
||||
println!("\x1b[1;34m==>\x1b[0m Uploading {} bytes to '{}'...", len, remote_path);
|
||||
}
|
||||
|
||||
let progress_cb = if args.format.is_machine_readable() {
|
||||
None
|
||||
let progress_cb = if show_progress {
|
||||
Some(create_indicatif_progress_callback(args.size.or(Some(len))))
|
||||
} else {
|
||||
Some(create_progress_callback(len))
|
||||
None
|
||||
};
|
||||
|
||||
let cursor = io::Cursor::new(buffer);
|
||||
@@ -345,7 +358,7 @@ async fn handle_upload(args: UploadArgs) -> Result<()> {
|
||||
};
|
||||
|
||||
if !args.format.is_machine_readable() && total_files > 1 {
|
||||
println!("\n\x1b[1;35m[{}/{}]\x1b[0m Processing '{}'", i + 1, total_files, local_path.display());
|
||||
println!("\n\x1b[1;35m[{}/{}]\x1b[0m '{}'", i + 1, total_files, local_path.display());
|
||||
}
|
||||
|
||||
match upload_single_file(
|
||||
@@ -355,6 +368,7 @@ async fn handle_upload(args: UploadArgs) -> Result<()> {
|
||||
args.share,
|
||||
args.password.as_deref(),
|
||||
&args.format,
|
||||
show_progress,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -460,6 +474,7 @@ async fn upload_single_file(
|
||||
create_share: bool,
|
||||
password: Option<&str>,
|
||||
format: &OutputFormatArgs,
|
||||
show_progress: bool,
|
||||
) -> Result<UploadRecord> {
|
||||
let metadata = tokio::fs::metadata(local_path).await?;
|
||||
let file_size = metadata.len();
|
||||
@@ -468,10 +483,10 @@ async fn upload_single_file(
|
||||
println!("\x1b[1;34m==>\x1b[0m Uploading '{}' ({} bytes) -> '{}'...", local_path.display(), file_size, remote_path);
|
||||
}
|
||||
|
||||
let progress_cb = if format.is_machine_readable() {
|
||||
None
|
||||
let progress_cb = if show_progress {
|
||||
Some(create_indicatif_progress_callback(Some(file_size)))
|
||||
} else {
|
||||
Some(create_progress_callback(file_size))
|
||||
None
|
||||
};
|
||||
|
||||
let options = UploadOptions {
|
||||
@@ -581,21 +596,38 @@ fn render_upload_results(results: &[UploadRecord], format: &OutputFormatArgs) {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_progress_callback(total_bytes: u64) -> ProgressCallback {
|
||||
Arc::new(move |event| match event {
|
||||
ProgressEvent::Progress { bytes_transferred, total_bytes: total } => {
|
||||
let tot = total.unwrap_or(total_bytes);
|
||||
if tot > 0 {
|
||||
let percent = (bytes_transferred as f64 / tot as f64) * 100.0;
|
||||
print!("\r \x1b[1;33mProgress:\x1b[0m {:>3.0}% ({}/{} bytes)", percent, bytes_transferred, tot);
|
||||
} else {
|
||||
print!("\r \x1b[1;33mProgress:\x1b[0m {} bytes transferred", bytes_transferred);
|
||||
fn create_indicatif_progress_callback(total_bytes: Option<u64>) -> ProgressCallback {
|
||||
let pb = match total_bytes {
|
||||
Some(total) if total > 0 => {
|
||||
let pb = ProgressBar::new(total);
|
||||
pb.set_draw_target(ProgressDrawTarget::stderr());
|
||||
pb.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template(" [{elapsed_precise}] [{bar:35.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, ETA {eta})")
|
||||
.unwrap()
|
||||
.progress_chars("#>-"),
|
||||
);
|
||||
pb
|
||||
}
|
||||
let _ = io::Write::flush(&mut io::stdout());
|
||||
_ => {
|
||||
let pb = ProgressBar::new_spinner();
|
||||
pb.set_draw_target(ProgressDrawTarget::stderr());
|
||||
pb.set_style(
|
||||
ProgressStyle::default_spinner()
|
||||
.template(" [{elapsed_precise}] Uploaded {bytes} ({bytes_per_sec})")
|
||||
.unwrap(),
|
||||
);
|
||||
pb
|
||||
}
|
||||
};
|
||||
|
||||
Arc::new(move |event| match event {
|
||||
ProgressEvent::Progress { bytes_transferred, .. } => {
|
||||
pb.set_position(bytes_transferred);
|
||||
}
|
||||
ProgressEvent::Completed { total_bytes } => {
|
||||
print!("\r \x1b[1;33mProgress:\x1b[0m 100% ({} bytes) ", total_bytes);
|
||||
let _ = io::Write::flush(&mut io::stdout());
|
||||
pb.set_position(total_bytes);
|
||||
pb.finish_and_clear();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user