use eyre::OptionExt;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::error::Error;
use std::io::IsTerminal;
use indicatif::{ProgressBar, ProgressStyle};
use std::path::Path;
use tokio::fs;
use tokio::io::{self};
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
use tokio_util::io::InspectReader;
use openstack_sdk::types::BoxedAsyncRead;
use structable::{StructTable, StructTableOptions};
use crate::error::OpenStackCliError;
#[derive(Deserialize, Default, Debug, Clone, Serialize)]
pub struct HashMapStringString(pub HashMap<String, String>);
impl StructTable for HashMapStringString {
fn instance_headers<O: StructTableOptions>(
&self,
_options: &O,
) -> Option<::std::vec::Vec<::std::string::String>> {
Some(self.0.keys().map(Into::into).collect())
}
fn data<O: StructTableOptions>(
&self,
_options: &O,
) -> ::std::vec::Vec<Option<::std::string::String>> {
self.0.values().map(|x| Some(x.into())).collect()
}
}
pub(crate) fn parse_key_val<T, U>(s: &str) -> Result<(T, U), Box<dyn Error + Send + Sync + 'static>>
where
T: std::str::FromStr,
T::Err: Error + Send + Sync + 'static,
U: std::str::FromStr,
U::Err: Error + Send + Sync + 'static,
{
let (k, v) = s
.split_once('=')
.ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
Ok((k.parse()?, v.parse()?))
}
pub(crate) fn parse_key_val_opt<T, U>(
s: &str,
) -> Result<(T, Option<U>), Box<dyn Error + Send + Sync + 'static>>
where
T: std::str::FromStr,
T::Err: Error + Send + Sync + 'static,
U: std::str::FromStr,
U::Err: Error + Send + Sync + 'static,
{
let (k, v) = s
.split_once('=')
.ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
let key = k.parse()?;
let val = (!v.is_empty()).then(|| v.parse()).transpose()?;
Ok((key, val))
}
pub(crate) fn parse_json(s: &str) -> Result<Value, Box<dyn Error + Send + Sync + 'static>>
where
{
Ok(serde_json::from_str(s)?)
}
pub(crate) async fn download_file(
dst_name: String,
size: u64,
data: BoxedAsyncRead,
) -> Result<(), OpenStackCliError> {
let progress_bar = ProgressBar::new(size);
let mut inspect_reader =
InspectReader::new(data.compat(), |bytes| progress_bar.inc(bytes.len() as u64));
if dst_name == "-" {
progress_bar.set_style(
ProgressStyle::default_bar()
.progress_chars("#>-")
.template("[{bar:40.cyan/blue}] {bytes}/{total_bytes} at {bytes_per_sec}")?,
);
let mut writer = io::stdout();
io::copy(&mut inspect_reader, &mut writer).await?;
} else {
let path = Path::new(&dst_name);
let fname = path
.file_name()
.ok_or_eyre("download file name must be known")?
.to_str()
.ok_or_eyre("download file name must be a string")?;
progress_bar.set_message(String::from(fname));
progress_bar.set_style(
ProgressStyle::default_bar()
.progress_chars("#>-")
.template(
"[{bar:40.cyan/blue}] {bytes}/{total_bytes} at {bytes_per_sec} - {msg}",
)?,
);
let mut writer = fs::File::create(path).await?;
io::copy(&mut inspect_reader, &mut writer).await?;
}
progress_bar.finish();
Ok(())
}
async fn build_upload_asyncread_from_stdin() -> Result<BoxedAsyncRead, OpenStackCliError> {
let progress_bar = ProgressBar::new(0);
progress_bar.set_style(
ProgressStyle::default_bar()
.progress_chars("#>-")
.template("[{bar:40.cyan/blue}] {bytes}/{total_bytes} at {bytes_per_sec}")?,
);
let inspect_reader = InspectReader::new(io::stdin(), move |bytes| {
progress_bar.inc(bytes.len() as u64)
});
Ok(BoxedAsyncRead::new(inspect_reader.compat()))
}
async fn build_upload_asyncread_from_file(
file_path: &str,
) -> Result<BoxedAsyncRead, OpenStackCliError> {
let progress_bar = ProgressBar::new(0);
progress_bar.set_style(
ProgressStyle::default_bar()
.progress_chars("#>-")
.template("[{bar:40.cyan/blue}] {bytes}/{total_bytes} at {bytes_per_sec}")?,
);
let reader = fs::File::open(&file_path).await?;
progress_bar.set_length(reader.metadata().await?.len());
let inspect_reader =
InspectReader::new(reader, move |bytes| progress_bar.inc(bytes.len() as u64));
Ok(BoxedAsyncRead::new(inspect_reader.compat()))
}
pub(crate) async fn build_upload_asyncread(
src_name: Option<String>,
) -> Result<BoxedAsyncRead, OpenStackCliError> {
if !std::io::stdin().is_terminal() && src_name.is_none() {
build_upload_asyncread_from_stdin().await
} else {
match src_name
.ok_or(OpenStackCliError::InputParameters(
"upload source name must be provided when stdin is not being piped".into(),
))?
.as_str()
{
"-" => build_upload_asyncread_from_stdin().await,
file_name => build_upload_asyncread_from_file(file_name).await,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_key_val() {
assert_eq!(
("foo".to_string(), "bar".to_string()),
parse_key_val::<String, String>("foo=bar").unwrap()
);
}
#[test]
fn test_parse_key_val_opt() {
assert_eq!(
("foo".to_string(), Some("bar".to_string())),
parse_key_val_opt::<String, String>("foo=bar").unwrap()
);
assert_eq!(
("foo".to_string(), None),
parse_key_val_opt::<String, String>("foo=").unwrap()
);
}
}