mirror of
https://github.com/nushell/nushell.git
synced 2025-03-30 10:37:29 +02:00
src/main.rs has a dependency on BufferedReader which is currently located in nu_command. I am moving BufferedReader to a more relevant location (crate) which will allow / eliminate main's dependency on nu_command in a benchmark / testing environment... now that @rgwood has landed benches I want to start experimenting with benchmarks related to the parser. For benchmark purposes when dealing with parsing you need a very simple set of commands that show how well the parser is doing, in other words just the core commands... Not all of nu_command... Having a smaller nu binary when running the benchmark CI would enable building nushell quickly, yet still show us how well the parser is performing... Once this PR lands the only dependency main will have on nu_command is create_default_context --- meaning for benchmark purposes we can swap in a tiny crate of commands instead of the gigantic nu_command which has its "own" create_default_context... It will also enable other crates going forward to use BufferedReader. Right now it is not accessible to other lower level crates because it is located in a "top of the stack crate".
391 lines
13 KiB
Rust
391 lines
13 KiB
Rust
use base64::encode;
|
|
use nu_engine::CallExt;
|
|
use nu_protocol::ast::Call;
|
|
use nu_protocol::engine::{Command, EngineState, Stack};
|
|
use nu_protocol::util::BufferedReader;
|
|
use nu_protocol::RawStream;
|
|
|
|
use nu_protocol::{
|
|
Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value,
|
|
};
|
|
use reqwest::blocking::Response;
|
|
|
|
use std::collections::HashMap;
|
|
use std::io::BufReader;
|
|
|
|
use reqwest::StatusCode;
|
|
use std::path::PathBuf;
|
|
use std::str::FromStr;
|
|
use std::time::Duration;
|
|
|
|
#[derive(Clone)]
|
|
pub struct SubCommand;
|
|
|
|
impl Command for SubCommand {
|
|
fn name(&self) -> &str {
|
|
"fetch"
|
|
}
|
|
|
|
fn signature(&self) -> Signature {
|
|
Signature::build("fetch")
|
|
.input_output_types(vec![(Type::Nothing, Type::Any)])
|
|
.required(
|
|
"URL",
|
|
SyntaxShape::String,
|
|
"the URL to fetch the contents from",
|
|
)
|
|
.named(
|
|
"user",
|
|
SyntaxShape::Any,
|
|
"the username when authenticating",
|
|
Some('u'),
|
|
)
|
|
.named(
|
|
"password",
|
|
SyntaxShape::Any,
|
|
"the password when authenticating",
|
|
Some('p'),
|
|
)
|
|
.named(
|
|
"timeout",
|
|
SyntaxShape::Int,
|
|
"timeout period in seconds",
|
|
Some('t'),
|
|
)
|
|
.named(
|
|
"headers",
|
|
SyntaxShape::Any,
|
|
"custom headers you want to add ",
|
|
Some('H'),
|
|
)
|
|
.switch(
|
|
"raw",
|
|
"fetch contents as text rather than a table",
|
|
Some('r'),
|
|
)
|
|
.filter()
|
|
.category(Category::Network)
|
|
}
|
|
|
|
fn usage(&self) -> &str {
|
|
"Fetch the contents from a URL."
|
|
}
|
|
|
|
fn extra_usage(&self) -> &str {
|
|
"Performs HTTP GET operation."
|
|
}
|
|
|
|
fn search_terms(&self) -> Vec<&str> {
|
|
vec![
|
|
"network", "get", "pull", "request", "http", "download", "curl", "wget",
|
|
]
|
|
}
|
|
|
|
fn run(
|
|
&self,
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
call: &Call,
|
|
input: PipelineData,
|
|
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
|
run_fetch(engine_state, stack, call, input)
|
|
}
|
|
|
|
fn examples(&self) -> Vec<Example> {
|
|
vec![
|
|
Example {
|
|
description: "Fetch content from example.com",
|
|
example: "fetch https://www.example.com",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "Fetch content from example.com, with username and password",
|
|
example: "fetch -u myuser -p mypass https://www.example.com",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "Fetch content from example.com, with custom header",
|
|
example: "fetch -H [my-header-key my-header-value] https://www.example.com",
|
|
result: None,
|
|
},
|
|
]
|
|
}
|
|
}
|
|
|
|
struct Arguments {
|
|
url: Value,
|
|
raw: bool,
|
|
user: Option<String>,
|
|
password: Option<String>,
|
|
timeout: Option<Value>,
|
|
headers: Option<Value>,
|
|
}
|
|
|
|
fn run_fetch(
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
call: &Call,
|
|
_input: PipelineData,
|
|
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
|
let args = Arguments {
|
|
url: call.req(engine_state, stack, 0)?,
|
|
raw: call.has_flag("raw"),
|
|
user: call.get_flag(engine_state, stack, "user")?,
|
|
password: call.get_flag(engine_state, stack, "password")?,
|
|
timeout: call.get_flag(engine_state, stack, "timeout")?,
|
|
headers: call.get_flag(engine_state, stack, "headers")?,
|
|
};
|
|
helper(engine_state, stack, args)
|
|
}
|
|
|
|
// Helper function that actually goes to retrieve the resource from the url given
|
|
// The Option<String> return a possible file extension which can be used in AutoConvert commands
|
|
fn helper(
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
args: Arguments,
|
|
) -> std::result::Result<PipelineData, ShellError> {
|
|
// There is no need to error-check this, as the URL is already guaranteed by basic nu command argument type checks.
|
|
let url_value = args.url;
|
|
|
|
let span = url_value.span()?;
|
|
let requested_url = url_value.as_string()?;
|
|
let url = match url::Url::parse(&requested_url) {
|
|
Ok(u) => u,
|
|
Err(_e) => {
|
|
return Err(ShellError::TypeMismatch(
|
|
"Incomplete or incorrect URL. Expected a full URL, e.g., https://www.example.com"
|
|
.to_string(),
|
|
span,
|
|
));
|
|
}
|
|
};
|
|
let user = args.user.clone();
|
|
let password = args.password;
|
|
let timeout = args.timeout;
|
|
let headers = args.headers;
|
|
let raw = args.raw;
|
|
let login = match (user, password) {
|
|
(Some(user), Some(password)) => Some(encode(format!("{}:{}", user, password))),
|
|
(Some(user), _) => Some(encode(format!("{}:", user))),
|
|
(_, Some(password)) => Some(encode(format!(":{}", password))),
|
|
_ => None,
|
|
};
|
|
|
|
let client = http_client();
|
|
let mut request = client.get(url);
|
|
|
|
if let Some(timeout) = timeout {
|
|
let val = timeout.as_i64()?;
|
|
if val.is_negative() || val < 1 {
|
|
return Err(ShellError::TypeMismatch(
|
|
"Timeout value must be an integer and larger than 0".to_string(),
|
|
// timeout is already guaranteed to not be an error
|
|
timeout.expect_span(),
|
|
));
|
|
}
|
|
|
|
request = request.timeout(Duration::from_secs(val as u64));
|
|
}
|
|
|
|
if let Some(login) = login {
|
|
request = request.header("Authorization", format!("Basic {}", login));
|
|
}
|
|
|
|
if let Some(headers) = headers {
|
|
let mut custom_headers: HashMap<String, Value> = HashMap::new();
|
|
|
|
match &headers {
|
|
Value::List { vals: table, .. } => {
|
|
if table.len() == 1 {
|
|
// single row([key1 key2]; [val1 val2])
|
|
match &table[0] {
|
|
Value::Record { cols, vals, .. } => {
|
|
for (k, v) in cols.iter().zip(vals.iter()) {
|
|
custom_headers.insert(k.to_string(), v.clone());
|
|
}
|
|
}
|
|
|
|
x => {
|
|
return Err(ShellError::CantConvert(
|
|
"string list or single row".into(),
|
|
x.get_type().to_string(),
|
|
headers.span().unwrap_or_else(|_| Span::new(0, 0)),
|
|
None,
|
|
));
|
|
}
|
|
}
|
|
} else {
|
|
// primitive values ([key1 val1 key2 val2])
|
|
for row in table.chunks(2) {
|
|
if row.len() == 2 {
|
|
custom_headers.insert(row[0].as_string()?, row[1].clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
x => {
|
|
return Err(ShellError::CantConvert(
|
|
"string list or single row".into(),
|
|
x.get_type().to_string(),
|
|
headers.span().unwrap_or_else(|_| Span::new(0, 0)),
|
|
None,
|
|
));
|
|
}
|
|
};
|
|
|
|
for (k, v) in &custom_headers {
|
|
if let Ok(s) = v.as_string() {
|
|
request = request.header(k, s);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Explicitly turn 4xx and 5xx statuses into errors.
|
|
match request.send().and_then(|r| r.error_for_status()) {
|
|
Ok(resp) => match resp.headers().get("content-type") {
|
|
Some(content_type) => {
|
|
let content_type = content_type.to_str().map_err(|e| {
|
|
ShellError::GenericError(
|
|
e.to_string(),
|
|
"".to_string(),
|
|
None,
|
|
Some("MIME type were invalid".to_string()),
|
|
Vec::new(),
|
|
)
|
|
})?;
|
|
let content_type = mime::Mime::from_str(content_type).map_err(|_| {
|
|
ShellError::GenericError(
|
|
format!("MIME type unknown: {}", content_type),
|
|
"".to_string(),
|
|
None,
|
|
Some("given unknown MIME type".to_string()),
|
|
Vec::new(),
|
|
)
|
|
})?;
|
|
let ext = match (content_type.type_(), content_type.subtype()) {
|
|
(mime::TEXT, mime::PLAIN) => {
|
|
let path_extension = url::Url::parse(&requested_url)
|
|
.map_err(|_| {
|
|
ShellError::GenericError(
|
|
format!("Cannot parse URL: {}", requested_url),
|
|
"".to_string(),
|
|
None,
|
|
Some("cannot parse".to_string()),
|
|
Vec::new(),
|
|
)
|
|
})?
|
|
.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())
|
|
});
|
|
path_extension
|
|
}
|
|
_ => Some(content_type.subtype().to_string()),
|
|
};
|
|
|
|
let output = response_to_buffer(resp, engine_state, span);
|
|
|
|
if raw {
|
|
return Ok(output);
|
|
}
|
|
|
|
if let Some(ext) = ext {
|
|
match engine_state.find_decl(format!("from {}", ext).as_bytes(), &[]) {
|
|
Some(converter_id) => engine_state.get_decl(converter_id).run(
|
|
engine_state,
|
|
stack,
|
|
&Call::new(span),
|
|
output,
|
|
),
|
|
None => Ok(output),
|
|
}
|
|
} else {
|
|
Ok(output)
|
|
}
|
|
}
|
|
None => Ok(response_to_buffer(resp, engine_state, span)),
|
|
},
|
|
Err(e) if e.is_timeout() => Err(ShellError::NetworkFailure(
|
|
format!("Request to {} has timed out", requested_url),
|
|
span,
|
|
)),
|
|
Err(e) if e.is_status() => match e.status() {
|
|
Some(err_code) if err_code == StatusCode::NOT_FOUND => Err(ShellError::NetworkFailure(
|
|
format!("Requested file not found (404): {:?}", requested_url),
|
|
span,
|
|
)),
|
|
Some(err_code) if err_code == StatusCode::MOVED_PERMANENTLY => {
|
|
Err(ShellError::NetworkFailure(
|
|
format!("Resource moved permanently (301): {:?}", requested_url),
|
|
span,
|
|
))
|
|
}
|
|
Some(err_code) if err_code == StatusCode::BAD_REQUEST => {
|
|
Err(ShellError::NetworkFailure(
|
|
format!("Bad request (400) to {:?}", requested_url),
|
|
span,
|
|
))
|
|
}
|
|
Some(err_code) if err_code == StatusCode::FORBIDDEN => Err(ShellError::NetworkFailure(
|
|
format!("Access forbidden (403) to {:?}", requested_url),
|
|
span,
|
|
)),
|
|
_ => Err(ShellError::NetworkFailure(
|
|
format!(
|
|
"Cannot make request to {:?}. Error is {:?}",
|
|
requested_url,
|
|
e.to_string()
|
|
),
|
|
span,
|
|
)),
|
|
},
|
|
Err(e) => Err(ShellError::NetworkFailure(
|
|
format!(
|
|
"Cannot make request to {:?}. Error is {:?}",
|
|
requested_url,
|
|
e.to_string()
|
|
),
|
|
span,
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn response_to_buffer(
|
|
response: Response,
|
|
engine_state: &EngineState,
|
|
span: Span,
|
|
) -> nu_protocol::PipelineData {
|
|
let buffered_input = BufReader::new(response);
|
|
|
|
PipelineData::ExternalStream {
|
|
stdout: Some(RawStream::new(
|
|
Box::new(BufferedReader {
|
|
input: buffered_input,
|
|
}),
|
|
engine_state.ctrlc.clone(),
|
|
span,
|
|
)),
|
|
stderr: None,
|
|
exit_code: None,
|
|
span,
|
|
metadata: None,
|
|
trim_end_newline: false,
|
|
}
|
|
}
|
|
|
|
// Only panics if the user agent is invalid but we define it statically so either
|
|
// it always or never fails
|
|
#[allow(clippy::unwrap_used)]
|
|
fn http_client() -> reqwest::blocking::Client {
|
|
reqwest::blocking::Client::builder()
|
|
.user_agent("nushell")
|
|
.build()
|
|
.unwrap()
|
|
}
|