use base64::encode; use mime::Mime; use nu_errors::{CoerceInto, ShellError}; use nu_protocol::{ CallInfo, CommandAction, Primitive, ReturnSuccess, ReturnValue, UnspannedPathMember, UntaggedValue, Value, }; use nu_source::{AnchorLocation, Tag, TaggedItem}; use num_traits::cast::ToPrimitive; use std::path::PathBuf; use std::str::FromStr; #[derive(Clone)] pub enum HeaderKind { ContentType(String), ContentLength(String), } #[derive(Default)] pub struct Post { pub path: Option, pub has_raw: bool, pub body: Option, pub user: Option, pub password: Option, pub headers: Vec, pub tag: Tag, } impl Post { pub fn new() -> Post { Post { path: None, has_raw: false, body: None, user: None, password: None, headers: vec![], tag: Tag::default(), } } pub fn setup(&mut self, call_info: CallInfo) -> ReturnValue { self.path = Some({ let file = call_info.args.nth(0).ok_or_else(|| { ShellError::labeled_error( "No file or directory specified", "for command", &call_info.name_tag, ) })?; file.clone() }); self.has_raw = call_info.args.has("raw"); self.body = { let file = call_info.args.nth(1).ok_or_else(|| { ShellError::labeled_error("No body specified", "for command", &call_info.name_tag) })?; Some(file.clone()) }; self.user = match call_info.args.get("user") { Some(user) => Some(user.as_string()?), None => None, }; self.password = match call_info.args.get("password") { Some(password) => Some(password.as_string()?), None => None, }; self.headers = get_headers(&call_info)?; self.tag = call_info.name_tag; ReturnSuccess::value(UntaggedValue::nothing().into_untagged_value()) } } pub async fn post_helper( path: &Value, has_raw: bool, body: &Value, user: Option, password: Option, headers: &[HeaderKind], ) -> ReturnValue { let path_tag = path.tag.clone(); let path_str = path.as_string()?; let (file_extension, contents, contents_tag) = post(&path_str, body, user, password, headers, path_tag.clone()).await?; let file_extension = if has_raw { None } else { // If the extension could not be determined via mimetype, try to use the path // extension. Some file types do not declare their mimetypes (such as bson files). file_extension.or_else(|| path_str.split('.').last().map(String::from)) }; let tagged_contents = contents.into_value(&contents_tag); if let Some(extension) = file_extension { Ok(ReturnSuccess::Action(CommandAction::AutoConvert( tagged_contents, extension, ))) } else { ReturnSuccess::value(tagged_contents) } } pub async fn post( location: &str, body: &Value, user: Option, password: Option, headers: &[HeaderKind], tag: Tag, ) -> Result<(Option, UntaggedValue, Tag), ShellError> { if location.starts_with("http:") || location.starts_with("https:") { let login = match (user, password) { (Some(user), Some(password)) => Some(encode(&format!("{}:{}", user, password))), (Some(user), _) => Some(encode(&format!("{}:", user))), _ => None, }; let response = match body { Value { value: UntaggedValue::Primitive(Primitive::String(body_str)), .. } => { let mut s = surf::post(location).body(body_str.to_string()); if let Some(login) = login { s = s.header("Authorization", format!("Basic {}", login)); } for h in headers { s = match h { HeaderKind::ContentType(ct) => s.header("Content-Type", ct), HeaderKind::ContentLength(cl) => s.header("Content-Length", cl), }; } s.await } Value { value: UntaggedValue::Primitive(Primitive::Binary(b)), .. } => { let mut s = surf::post(location).body(&b[..]); if let Some(login) = login { s = s.header("Authorization", format!("Basic {}", login)); } s.await } Value { value, tag } => { match value_to_json_value(&value.clone().into_untagged_value()) { Ok(json_value) => match serde_json::to_string(&json_value) { Ok(result_string) => { let mut s = surf::post(location).body(result_string); if let Some(login) = login { s = s.header("Authorization", format!("Basic {}", login)); } s.await } _ => { return Err(ShellError::labeled_error( "Could not automatically convert table", "needs manual conversion", tag, )); } }, _ => { return Err(ShellError::labeled_error( "Could not automatically convert table", "needs manual conversion", tag, )); } } } }; match response { Ok(mut r) => match r.header("content-type") { Some(content_type) => { let content_type = Mime::from_str(content_type.as_str()).map_err(|_| { ShellError::labeled_error( format!("Unknown MIME type: {}", content_type), "unknown MIME type", &tag, ) })?; match (content_type.type_(), content_type.subtype()) { (mime::APPLICATION, mime::XML) => Ok(( Some("xml".to_string()), UntaggedValue::string(r.body_string().await.map_err(|_| { ShellError::labeled_error( "Could not load text from remote url", "could not load", &tag, ) })?), Tag { anchor: Some(AnchorLocation::Url(location.to_string())), span: tag.span, }, )), (mime::APPLICATION, mime::JSON) => Ok(( Some("json".to_string()), UntaggedValue::string(r.body_string().await.map_err(|_| { ShellError::labeled_error( "Could not load text from remote url", "could not load", &tag, ) })?), Tag { anchor: Some(AnchorLocation::Url(location.to_string())), span: tag.span, }, )), (mime::APPLICATION, mime::OCTET_STREAM) => { let buf: Vec = r.body_bytes().await.map_err(|_| { ShellError::labeled_error( "Could not load binary file", "could not load", &tag, ) })?; Ok(( None, UntaggedValue::binary(buf), Tag { anchor: Some(AnchorLocation::Url(location.to_string())), span: tag.span, }, )) } (mime::IMAGE, image_ty) => { let buf: Vec = r.body_bytes().await.map_err(|_| { ShellError::labeled_error( "Could not load image file", "could not load", &tag, ) })?; Ok(( Some(image_ty.to_string()), UntaggedValue::binary(buf), Tag { anchor: Some(AnchorLocation::Url(location.to_string())), span: tag.span, }, )) } (mime::TEXT, mime::HTML) => Ok(( Some("html".to_string()), UntaggedValue::string(r.body_string().await.map_err(|_| { ShellError::labeled_error( "Could not load text from remote url", "could not load", &tag, ) })?), Tag { anchor: Some(AnchorLocation::Url(location.to_string())), span: tag.span, }, )), (mime::TEXT, mime::PLAIN) => { let path_extension = url::Url::parse(location) .map_err(|_| { ShellError::labeled_error( format!("could not parse URL: {}", location), "could not parse URL", &tag, ) })? .path_segments() .and_then(|segments| segments.last()) .and_then(|name| if name.is_empty() { None } else { Some(name) }) .and_then(|name| { PathBuf::from(name) .extension() .map(|name| name.to_string_lossy().to_string()) }); Ok(( path_extension, UntaggedValue::string(r.body_string().await.map_err(|_| { ShellError::labeled_error( "Could not load text from remote url", "could not load", &tag, ) })?), Tag { anchor: Some(AnchorLocation::Url(location.to_string())), span: tag.span, }, )) } (ty, sub_ty) => Ok(( None, UntaggedValue::string(format!( "Not yet supported MIME type: {} {}", ty, sub_ty )), Tag { anchor: Some(AnchorLocation::Url(location.to_string())), span: tag.span, }, )), } } None => Ok(( None, UntaggedValue::string("No content type found".to_owned()), Tag { anchor: Some(AnchorLocation::Url(location.to_string())), span: tag.span, }, )), }, Err(_) => Err(ShellError::labeled_error( "URL could not be opened", "url not found", tag, )), } } else { Err(ShellError::labeled_error( "Expected a url", "needs a url", tag, )) } } // FIXME FIXME FIXME // Ultimately, we don't want to duplicate to-json here, but we need to because there isn't an easy way to call into it, yet pub fn value_to_json_value(v: &Value) -> Result { Ok(match &v.value { UntaggedValue::Primitive(Primitive::Boolean(b)) => serde_json::Value::Bool(*b), UntaggedValue::Primitive(Primitive::Filesize(b)) => serde_json::Value::Number( serde_json::Number::from(b.to_u64().expect("What about really big numbers")), ), UntaggedValue::Primitive(Primitive::Duration(i)) => serde_json::Value::Number( serde_json::Number::from_f64( i.to_f64().expect("TODO: What about really big decimals?"), ) .ok_or_else(|| { ShellError::labeled_error( "Can not convert big decimal to f64", "cannot convert big decimal to f64", &v.tag, ) })?, ), UntaggedValue::Primitive(Primitive::Date(d)) => serde_json::Value::String(d.to_string()), UntaggedValue::Primitive(Primitive::EndOfStream) => serde_json::Value::Null, UntaggedValue::Primitive(Primitive::BeginningOfStream) => serde_json::Value::Null, UntaggedValue::Primitive(Primitive::Decimal(f)) => serde_json::Value::Number( serde_json::Number::from_f64( f.to_f64().expect("TODO: What about really big decimals?"), ) .ok_or_else(|| { ShellError::labeled_error( "Can not convert big decimal to f64", "cannot convert big decimal to f64", &v.tag, ) })?, ), UntaggedValue::Primitive(Primitive::Int(i)) => { serde_json::Value::Number(serde_json::Number::from(*i)) } UntaggedValue::Primitive(Primitive::BigInt(i)) => { serde_json::Value::Number(serde_json::Number::from(CoerceInto::::coerce_into( i.tagged(&v.tag), "converting to JSON number", )?)) } UntaggedValue::Primitive(Primitive::Nothing) => serde_json::Value::Null, UntaggedValue::Primitive(Primitive::GlobPattern(s)) => serde_json::Value::String(s.clone()), UntaggedValue::Primitive(Primitive::String(s)) => serde_json::Value::String(s.clone()), UntaggedValue::Primitive(Primitive::ColumnPath(path)) => serde_json::Value::Array( path.iter() .map(|x| match &x.unspanned { UnspannedPathMember::String(string) => { Ok(serde_json::Value::String(string.clone())) } UnspannedPathMember::Int(int) => { Ok(serde_json::Value::Number(serde_json::Number::from(*int))) } }) .collect::, ShellError>>()?, ), UntaggedValue::Primitive(Primitive::FilePath(s)) => { serde_json::Value::String(s.display().to_string()) } UntaggedValue::Table(l) => serde_json::Value::Array(json_list(l)?), #[cfg(feature = "dataframe")] UntaggedValue::DataFrame(_) => { return Err(ShellError::labeled_error( "Cannot convert data struct", "Cannot convert data struct", &v.tag, )) } UntaggedValue::Error(e) => return Err(e.clone()), UntaggedValue::Block(_) | UntaggedValue::Primitive(Primitive::Range(_)) => { serde_json::Value::Null } UntaggedValue::Primitive(Primitive::Binary(b)) => { let mut output = vec![]; for item in b.iter() { output.push(serde_json::Value::Number( serde_json::Number::from_f64(*item as f64).ok_or_else(|| { ShellError::labeled_error( "Cannot create number from from f64", "cannot created number from f64", &v.tag, ) })?, )); } serde_json::Value::Array(output) } UntaggedValue::Row(o) => { let mut m = serde_json::Map::new(); for (k, v) in o.entries.iter() { m.insert(k.clone(), value_to_json_value(v)?); } serde_json::Value::Object(m) } }) } fn json_list(input: &[Value]) -> Result, ShellError> { let mut out = vec![]; for value in input { out.push(value_to_json_value(value)?); } Ok(out) } fn get_headers(call_info: &CallInfo) -> Result, ShellError> { let mut headers = vec![]; match extract_header_value(call_info, "content-type") { Ok(h) => { if let Some(ct) = h { headers.push(HeaderKind::ContentType(ct)) } } Err(e) => { return Err(e); } }; match extract_header_value(call_info, "content-length") { Ok(h) => { if let Some(cl) = h { headers.push(HeaderKind::ContentLength(cl)) } } Err(e) => { return Err(e); } }; Ok(headers) } fn extract_header_value(call_info: &CallInfo, key: &str) -> Result, ShellError> { if call_info.args.has(key) { let tagged = call_info.args.get(key); let val = match tagged { Some(Value { value: UntaggedValue::Primitive(Primitive::String(s)), .. }) => s.clone(), Some(Value { tag, .. }) => { return Err(ShellError::labeled_error( format!("{} not in expected format. Expected string.", key), "post error", tag, )); } _ => { return Err(ShellError::labeled_error( format!("{} not in expected format. Expected string.", key), "post error", Tag::unknown(), )); } }; return Ok(Some(val)); } Ok(None) }