use crate::util::{color_green, color_red, color_yellow};
use std::io::BufRead;
use crate::cli_args::*;
use crate::{IskraClient, IskraError};
use crate::util::clamp_timeout;
pub async fn run_cli() -> Result<(), IskraError> {
let args: Vec<_> = std::env::args().collect();
if args.iter().any(|a| a == "--help" || a == "-h") || args.len() == 1 {
crate::cli_args::print_iskra_help();
return Ok(());
}
let cli = match parse_iskra_cli_args(std::env::args_os()) {
Ok(cli) => cli,
Err(e) => {
eprintln!("\x1b[1;31mIskra CLI parse error:\x1b[0m {e}\n");
crate::cli_args::print_iskra_help();
return Err(IskraError::Other(format!("CLI parse error: {e}")));
}
};
let timeout = clamp_timeout(cli.timeout);
let client = if cli.no_decompress {
IskraClient::new_with_timeout_and_decompression(std::time::Duration::from_secs(timeout), false)?
} else {
IskraClient::new_with_timeout(std::time::Duration::from_secs(timeout))?
};
let verbose = cli.verbose;
let quiet = cli.quiet;
let fail_on_error = cli.fail;
return match &cli.subcommand {
IskraSubcommand::Burst { input, output_dir, concurrency, throttle, retries, backoff, summary } => {
use std::fs::File;
use std::io::{self, BufReader};
use crate::burst::{BurstRequest, BurstOptions, BurstEngine};
let reader: Box<dyn BufRead> = if input == "-" {
Box::new(BufReader::new(io::stdin()))
} else {
Box::new(BufReader::new(File::open(input).map_err(|e| IskraError::Other(e.to_string()))?))
};
let requests = BurstRequest::parse_batch(reader)
.map_err(|e| IskraError::Other(format!("Burst input parse error: {e}")))?;
if requests.is_empty() {
return Err(IskraError::Other("No burst requests found in input".to_string()));
}
let options = BurstOptions {
concurrency: *concurrency,
throttle_per_sec: *throttle,
retries: *retries,
backoff: if *backoff > 0 { Some(std::time::Duration::from_secs(*backoff)) } else { None },
summary: *summary,
timeout: Some(std::time::Duration::from_secs(timeout)),
output_dir: output_dir.clone(),
};
let engine = BurstEngine::new_with_timeout(requests, options, std::time::Duration::from_secs(cli.timeout));
let results = engine.run_with_verbosity(verbose, quiet).await;
if !summary && !quiet && !verbose {
let total = results.len();
let successes = results.iter().filter(|r| r.success).count();
let failures = total - successes;
println!("Burst complete: {} total | {} success | {} fail", total, successes, failures);
}
if fail_on_error && results.iter().any(|r| !r.success) {
return Err(IskraError::Other("One or more burst requests failed".to_string()));
}
Ok(())
},
IskraSubcommand::Get { url, header, query, output, range, resume } => {
let headers: Vec<(&str, &str)> = header.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
let queries: Vec<(&str, &str)> = query.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
if url.starts_with("file://") {
let path = &url[7..];
use std::fs;
match fs::read_to_string(path) {
Ok(contents) => {
if let Some(out_path) = output {
fs::write(&out_path, &contents).map_err(|e| IskraError::Io { source: e, path: out_path.clone() })?;
if !quiet { println!("Downloaded {} bytes to {}", contents.len(), out_path); }
} else {
if !quiet { println!("{}", contents); }
}
Ok(())
}
Err(e) => Err(IskraError::Io { source: e, path: path.to_string() })
}
} else if url.starts_with("http://") || url.starts_with("https://") {
if let Some(path) = output {
let downloaded = client.download_with_range("GET", url, None, &headers, &queries, std::path::Path::new(&path), range.clone(), *resume, None).await?;
if !quiet { println!("Downloaded {downloaded} bytes to {path}"); }
} else {
let resp = client.get(url, &headers, &queries).await?;
let status = resp.status();
let status_str = if status.is_success() {
color_green(&status.to_string())
} else if status.as_u16() == 408 {
color_yellow(&status.to_string())
} else {
color_red(&status.to_string())
};
if (cli.show_headers || !header.is_empty()) && !quiet {
println!("Headers:");
for (k, v) in resp.headers().iter() {
println!("{}: {}", k, v.to_str().unwrap_or("<binary>"));
}
}
let body = resp.text().await.map_err(|e| IskraError::Http { source: e, url: url.clone() })?;
if !quiet { println!("Status: {}\n{}", status_str, body); }
if fail_on_error && !status.is_success() {
return Err(IskraError::Status { status: status.as_u16(), url: url.clone() });
}
}
Ok(())
} else {
Err(IskraError::Config { msg: format!("Unsupported URL scheme for get: {}", url) })
}
},
IskraSubcommand::Post { url, body, header, query, output, range, resume } => {
let headers: Vec<(&str, &str)> = header.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
let queries: Vec<(&str, &str)> = query.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
if let Some(path) = output {
let downloaded = client.download_with_range("POST", url, Some(body), &headers, &queries, std::path::Path::new(&path), range.clone(), *resume, None).await?;
if !quiet { println!("Downloaded {downloaded} bytes to {path}"); }
} else {
let resp = client.post(url, body, &headers, &queries).await?;
let status = resp.status();
let status_str = if status.is_success() {
color_green(&status.to_string())
} else if status.as_u16() == 408 {
color_yellow(&status.to_string())
} else {
color_red(&status.to_string())
};
if (cli.show_headers || !header.is_empty()) && !quiet {
println!("Headers:");
for (k, v) in resp.headers().iter() {
println!("{}: {}", k, v.to_str().unwrap_or("<binary>"));
}
}
let body = resp.text().await.map_err(|e| IskraError::Http { source: e, url: url.clone() })?;
if !quiet { println!("Status: {}\n{}", status_str, body); }
if fail_on_error && !status.is_success() {
return Err(IskraError::Status { status: status.as_u16(), url: url.clone() });
}
}
Ok(())
},
IskraSubcommand::Put { url, body, header, query, output, range, resume } => {
let headers: Vec<(&str, &str)> = header.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
let queries: Vec<(&str, &str)> = query.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
if let Some(path) = output {
let downloaded = client.download_with_range("PUT", url, Some(body), &headers, &queries, std::path::Path::new(&path), range.clone(), *resume, None).await?;
if !quiet { println!("Downloaded {downloaded} bytes to {path}"); }
} else {
let resp = client.put(url, body, &headers, &queries).await?;
let status = resp.status();
let status_str = if status.is_success() {
color_green(&status.to_string())
} else if status.as_u16() == 408 {
color_yellow(&status.to_string())
} else {
color_red(&status.to_string())
};
if (cli.show_headers || !header.is_empty()) && !quiet {
println!("Headers:");
for (k, v) in resp.headers().iter() {
println!("{}: {}", k, v.to_str().unwrap_or("<binary>"));
}
}
let body = resp.text().await.map_err(|e| IskraError::Http { source: e, url: url.clone() })?;
if !quiet { println!("Status: {}\n{}", status_str, body); }
if fail_on_error && !status.is_success() {
return Err(IskraError::Status { status: status.as_u16(), url: url.clone() });
}
}
Ok(())
},
IskraSubcommand::Delete { url, header, query, output, range, resume } => {
let headers: Vec<(&str, &str)> = header.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
let queries: Vec<(&str, &str)> = query.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
if let Some(path) = output {
let downloaded = client.download_with_range("DELETE", url, None, &headers, &queries, std::path::Path::new(&path), range.clone(), *resume, None).await?;
if !quiet { println!("Downloaded {downloaded} bytes to {path}"); }
} else {
let resp = client.delete(url, &headers, &queries).await?;
let status = resp.status();
let status_str = if status.is_success() {
color_green(&status.to_string())
} else if status.as_u16() == 408 {
color_yellow(&status.to_string())
} else {
color_red(&status.to_string())
};
if (cli.show_headers || !header.is_empty()) && !quiet {
println!("Headers:");
for (k, v) in resp.headers().iter() {
println!("{}: {}", k, v.to_str().unwrap_or("<binary>"));
}
}
let body = resp.text().await.map_err(|e| IskraError::Http { source: e, url: url.clone() })?;
if !quiet { println!("Status: {}\n{}", status_str, body); }
if fail_on_error && !status.is_success() {
return Err(IskraError::Status { status: status.as_u16(), url: url.clone() });
}
}
Ok(())
},
IskraSubcommand::Custom { method, url, header, query, output, body, trailing_body, range, resume } => {
let headers: Vec<(&str, &str)> = header.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
let queries: Vec<(&str, &str)> = query.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
let body_str: Option<String> = if let Some(b) = body {
Some(b.clone())
} else if !trailing_body.is_empty() {
Some(trailing_body.join(" "))
} else {
None
};
if let Some(path) = output {
let downloaded = client.download_with_range(method, url, body_str.as_deref(), &headers, &queries, std::path::Path::new(&path), range.clone(), *resume, None).await?;
if !quiet { println!("Downloaded {downloaded} bytes to {path}"); }
} else {
let resp = client.request(method, url, body_str.as_deref(), &headers, &queries).await?;
let status = resp.status();
let status_str = if status.is_success() {
color_green(&status.to_string())
} else if status.as_u16() == 408 {
color_yellow(&status.to_string())
} else {
color_red(&status.to_string())
};
if (cli.show_headers || !header.is_empty()) && !quiet {
println!("Headers:");
for (k, v) in resp.headers().iter() {
println!("{}: {}", k, v.to_str().unwrap_or("<binary>"));
}
}
let body_text = resp.text().await.map_err(|e| IskraError::Http { source: e, url: url.clone() })?;
if !quiet { println!("Status: {}\n{}", status_str, body_text); }
if fail_on_error && !status.is_success() {
return Err(IskraError::Status { status: status.as_u16(), url: url.clone() });
}
}
Ok(())
}
}
}