use clap::{Parser, Subcommand};
use crate::{IskraClient, IskraError};
use crate::util::clamp_timeout;
#[derive(Subcommand, Debug)]
pub enum Commands {
Custom {
method: String,
url: String,
#[arg(short, long, value_parser = parse_header, num_args = 0.., value_name = "key:value")]
header: Vec<(String, String)>,
#[arg(short = 'q', long, value_parser = parse_query, num_args = 0.., value_name = "key=value")]
query: Vec<(String, String)>,
#[arg(short, long, value_name = "file")]
output: Option<String>,
#[arg(long, value_name = "start-end", value_parser = parse_range)]
range: Option<(u64, Option<u64>)>,
#[arg(long, help = "Resume download if file exists")]
resume: bool,
#[arg(long, value_name = "body")]
body: Option<String>,
#[arg(long, help = "Exit with non-zero code if response is not 2xx")]
fail: bool,
},
Get {
url: String,
#[arg(short, long, value_parser = parse_header, num_args = 0.., value_name = "key:value")]
header: Vec<(String, String)>,
#[arg(short = 'q', long, value_parser = parse_query, num_args = 0.., value_name = "key=value")]
query: Vec<(String, String)>,
#[arg(short, long, value_name = "file")]
output: Option<String>,
#[arg(long, value_name = "start-end", value_parser = parse_range)]
range: Option<(u64, Option<u64>)>,
#[arg(long, help = "Resume download if file exists")]
resume: bool,
#[arg(long, help = "Exit with non-zero code if response is not 2xx")]
fail: bool,
},
Post {
url: String,
body: String,
#[arg(short, long, value_parser = parse_header, num_args = 0.., value_name = "key:value")]
header: Vec<(String, String)>,
#[arg(short = 'q', long, value_parser = parse_query, num_args = 0.., value_name = "key=value")]
query: Vec<(String, String)>,
#[arg(short, long, value_name = "file")]
output: Option<String>,
#[arg(long, value_name = "start-end", value_parser = parse_range)]
range: Option<(u64, Option<u64>)>,
#[arg(long, help = "Resume download if file exists")]
resume: bool,
#[arg(long, help = "Exit with non-zero code if response is not 2xx")]
fail: bool,
},
Put {
url: String,
body: String,
#[arg(short, long, value_parser = parse_header, num_args = 0.., value_name = "key:value")]
header: Vec<(String, String)>,
#[arg(short = 'q', long, value_parser = parse_query, num_args = 0.., value_name = "key=value")]
query: Vec<(String, String)>,
#[arg(short, long, value_name = "file")]
output: Option<String>,
#[arg(long, value_name = "start-end", value_parser = parse_range)]
range: Option<(u64, Option<u64>)>,
#[arg(long, help = "Resume download if file exists")]
resume: bool,
#[arg(long, help = "Exit with non-zero code if response is not 2xx")]
fail: bool,
},
Delete {
url: String,
#[arg(short, long, value_parser = parse_header, num_args = 0.., value_name = "key:value")]
header: Vec<(String, String)>,
#[arg(short = 'q', long, value_parser = parse_query, num_args = 0.., value_name = "key=value")]
query: Vec<(String, String)>,
#[arg(short, long, value_name = "file")]
output: Option<String>,
#[arg(long, value_name = "start-end", value_parser = parse_range)]
range: Option<(u64, Option<u64>)>,
#[arg(long, help = "Resume download if file exists")]
resume: bool,
#[arg(long, help = "Exit with non-zero code if response is not 2xx")]
fail: bool,
}
}
#[derive(Parser, Debug)]
#[command(name = "iskra")]
#[command(about = "Iskra: Secure, modern, Rust-native data transfer tool", long_about = None)]
pub struct Cli {
#[arg(long, value_name = "seconds", default_value_t = 30)]
pub timeout: u64,
#[arg(long, help = "Disable automatic response decompression (gzip, deflate, brotli)")]
pub no_decompress: bool,
#[command(subcommand)]
pub command: Commands,
}
pub async fn run_cli() -> Result<(), IskraError> {
let cli = Cli::parse();
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))?
};
match cli.command {
Commands::Get { url, header, query, output, range, resume, fail } => {
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("GET", &url, None, &headers, &queries, std::path::Path::new(&path), range, resume, None).await?;
println!("Downloaded {downloaded} bytes to {path}");
} else {
let resp = client.get(&url, &headers, &queries).await?;
let status = resp.status();
let body = resp.text().await.map_err(|e| IskraError::Http { source: e, url: url.clone() })?;
println!("Status: {}\n{}", status, body);
if fail && !status.is_success() {
eprintln!("Request failed with status: {}", status);
std::process::exit(1);
}
}
}
Commands::Post { url, body, header, query, output, range, resume, fail } => {
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, resume, None).await?;
println!("Downloaded {downloaded} bytes to {path}");
} else {
let resp = client.post(&url, &body, &headers, &queries).await?;
let status = resp.status();
let body = resp.text().await.map_err(|e| IskraError::Http { source: e, url: url.clone() })?;
println!("Status: {}\n{}", status, body);
if fail && !status.is_success() {
eprintln!("Request failed with status: {}", status);
std::process::exit(1);
}
}
}
Commands::Put { url, body, header, query, output, range, resume, fail } => {
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, resume, None).await?;
println!("Downloaded {downloaded} bytes to {path}");
} else {
let resp = client.put(&url, &body, &headers, &queries).await?;
let status = resp.status();
let body = resp.text().await.map_err(|e| IskraError::Http { source: e, url: url.clone() })?;
println!("Status: {}\n{}", status, body);
if fail && !status.is_success() {
eprintln!("Request failed with status: {}", status);
std::process::exit(1);
}
}
}
Commands::Delete { url, header, query, output, range, resume, fail } => {
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, resume, None).await?;
println!("Downloaded {downloaded} bytes to {path}");
} else {
let resp = client.delete(&url, &headers, &queries).await?;
let status = resp.status();
let body = resp.text().await.map_err(|e| IskraError::Http { source: e, url: url.clone() })?;
println!("Status: {}\n{}", status, body);
if fail && !status.is_success() {
eprintln!("Request failed with status: {}", status);
std::process::exit(1);
}
}
}
Commands::Custom { method, url, header, query, output, body, range, resume, fail } => {
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(&method, &url, body.as_deref(), &headers, &queries, std::path::Path::new(&path), range, resume, None).await?;
println!("Downloaded {downloaded} bytes to {path}");
} else {
let resp = client.request(&method, &url, body.as_deref(), &headers, &queries).await?;
let status = resp.status();
let body_text = resp.text().await.map_err(|e| IskraError::Http { source: e, url: url.clone() })?;
println!("Status: {}\n{}", status, body_text);
if fail && !status.is_success() {
eprintln!("Request failed with status: {}", status);
std::process::exit(1);
}
}
}
}
Ok(())
}
fn parse_range(s: &str) -> Result<(u64, Option<u64>), String> {
let parts: Vec<&str> = s.split('-').collect();
if parts.len() != 2 {
return Err("Range must be in start-end format (end optional)".to_string());
}
let start = parts[0].trim().parse::<u64>().map_err(|_| "Invalid start of range".to_string())?;
let end = if parts[1].trim().is_empty() {
None
} else {
Some(parts[1].trim().parse::<u64>().map_err(|_| "Invalid end of range".to_string())?)
};
Ok((start, end))
}
fn parse_header(s: &str) -> Result<(String, String), String> {
let parts: Vec<&str> = s.splitn(2, ':').collect();
if parts.len() != 2 {
return Err("Header must be in key:value format".to_string());
}
Ok((parts[0].trim().to_string(), parts[1].trim().to_string()))
}
fn parse_query(s: &str) -> Result<(String, String), String> {
let parts: Vec<&str> = s.splitn(2, '=').collect();
if parts.len() != 2 {
return Err("Query must be in key=value format".to_string());
}
Ok((parts[0].trim().to_string(), parts[1].trim().to_string()))
}