use std::io::IsTerminal as _;
use crate::{UploadError, UploadResult};
pub trait ProgressListener: Send + Sync {
fn on_progress(&self, uploaded: u64, total: u64);
fn on_complete(&self, result: &UploadResult);
fn on_error(&self, error: &UploadError);
}
pub struct NoopProgressListener;
impl ProgressListener for NoopProgressListener {
fn on_progress(&self, _uploaded: u64, _total: u64) {}
fn on_complete(&self, _result: &UploadResult) {}
fn on_error(&self, _error: &UploadError) {}
}
pub struct StderrProgressListener {
start: std::time::Instant,
}
impl Default for StderrProgressListener {
fn default() -> Self {
Self::new()
}
}
impl StderrProgressListener {
pub fn new() -> Self {
Self {
start: std::time::Instant::now(),
}
}
}
impl ProgressListener for StderrProgressListener {
fn on_progress(&self, uploaded: u64, total: u64) {
if total > 0 {
let pct = (uploaded as f64 / total as f64) * 100.0;
let elapsed = self.start.elapsed().as_secs_f64();
let speed_str = if elapsed > 0.0 && uploaded > 0 {
let speed = uploaded as f64 / elapsed;
format_speed(speed)
} else {
"--".to_string()
};
let eta_str = if uploaded > 0 && uploaded < total && elapsed > 0.0 {
let speed = uploaded as f64 / elapsed;
let remaining_bytes = total - uploaded;
let eta_secs = remaining_bytes as f64 / speed;
format_duration(eta_secs)
} else {
"--".to_string()
};
if std::io::stderr().is_terminal() {
eprint!(
"\r {:>6.2}% {}/s ETA {} ({}/{})",
pct, speed_str, eta_str, uploaded, total
);
} else {
eprintln!(
" {:>6.2}% {}/s ETA {} ({}/{})",
pct, speed_str, eta_str, uploaded, total
);
}
}
}
fn on_complete(&self, result: &UploadResult) {
let elapsed = self.start.elapsed();
if std::io::stderr().is_terminal() {
eprintln!(
"\n {} uploaded to {}: {}",
format_duration(elapsed.as_secs_f64()),
result.workspace,
result.url
);
} else {
eprintln!(
"[complete] {}: {} ({})",
result.workspace,
result.url,
format_duration(elapsed.as_secs_f64())
);
}
}
fn on_error(&self, error: &UploadError) {
eprintln!(" Upload failed: {}", error);
}
}
fn format_speed(bytes_per_sec: f64) -> String {
if bytes_per_sec >= 1_000_000_000.0 {
format!("{:.1} GB", bytes_per_sec / 1_000_000_000.0)
} else if bytes_per_sec >= 1_000_000.0 {
format!("{:.1} MB", bytes_per_sec / 1_000_000.0)
} else if bytes_per_sec >= 1_000.0 {
format!("{:.0} KB", bytes_per_sec / 1_000.0)
} else {
format!("{:.0} B", bytes_per_sec)
}
}
fn format_duration(secs: f64) -> String {
if secs.is_nan() || secs.is_infinite() || secs < 0.0 {
return "--".to_string();
}
let total_secs = secs as u64;
if total_secs < 60 {
format!("{}s", total_secs)
} else if total_secs < 3600 {
format!("{}m {}s", total_secs / 60, total_secs % 60)
} else {
format!("{}h {}m", total_secs / 3600, (total_secs % 3600) / 60)
}
}