use std::io::Write;
use crossterm::style::{Color, Print, ResetColor, SetForegroundColor, SetStyle};
use eyre::Result;
use futures::StreamExt;
use indicatif::{ProgressBar, ProgressState, ProgressStyle};
use reqwest::{header, Body};
use reqwest::{redirect::Policy, Request, RequestBuilder};
use serde_json::Value;
use tokio::fs::File;
use tokio_util::io::ReaderStream;
use crate::buffer::Buffer;
use crate::cli::utils;
use crate::output::format_by_ext;
use crate::styles::*;
use super::http_request::HttpRequest;
use super::payload::Payload;
pub trait Connector {
#[allow(async_fn_in_trait)]
async fn start(&self) -> Result<()>;
}
impl Connector for HttpRequest {
async fn start(&self) -> Result<()> {
let request: Request = self.into();
let content_type = request.headers()
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("application/octect-stream")
.to_string();
let policy: Policy = self.into();
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(!self.verify)
.redirect(policy)
.build()?;
let builder = RequestBuilder::from_parts(client.clone(), request);
let mut stdout = std::io::stdout();
let mut stderr = std::io::stderr();
if self.verbose {
if let Some(builder) = builder.try_clone() {
let request = builder.build()?;
if !self.verify {
crossterm::execute!(
stderr,
SetStyle(*STATUS_FAILURE_STYLE),
Print("Skipping SSL verification"),
SetStyle(*DEFAULT_STYLE),
Print("\n"),
)?;
}
crossterm::execute!(
stderr,
SetStyle(*METHOD_STYLE),
Print(request.method()),
Print(" "),
SetStyle(*DEFAULT_STYLE),
SetStyle(*URL_STYLE),
Print(request.url().to_string()),
SetStyle(*DEFAULT_STYLE),
Print("\n"),
)?;
for (name, value) in request.headers().iter() {
let value = value.to_str()?;
crossterm::execute!(
stderr,
SetStyle(*HEADER_NAME_STYLE),
Print(name),
Print(": "),
SetStyle(*DEFAULT_STYLE),
SetStyle(*HEADER_VALUE_STYLE),
Print(value),
SetStyle(*DEFAULT_STYLE),
Print("\n"),
)?;
}
eprintln!();
match &self.payload {
Payload::Content(content) => {
format_by_ext(
&content,
utils::extension_from_mime(&content_type),
&mut stderr,
)?;
eprintln!();
}
Payload::FileUpload(filename) => {
crossterm::execute!(
stderr,
SetStyle(*DEFAULT_STYLE),
Print("Content from file: "),
SetStyle(*STATUS_OTHER_STYLE),
Print(filename),
SetStyle(*DEFAULT_STYLE),
Print("\n"),
)?;
}
Payload::NoContent => (),
}
}
}
if self.dry_run {
if let Some(output) = &self.output {
crossterm::execute!(
stderr,
SetStyle(*DEFAULT_STYLE),
Print("save output into "),
SetStyle(*HEADER_VALUE_STYLE),
Print(output),
SetStyle(*DEFAULT_STYLE),
Print("\n"),
)?;
}
return Ok(());
}
let builder = match &self.payload {
Payload::Content(content) if content_type.contains("json") => {
let content: Value = serde_json::from_str(content)?;
builder.json(&content)
}
Payload::Content(content) => builder.body(content.to_owned()),
Payload::FileUpload(filename) => {
let file = File::open(filename).await?;
let stream = ReaderStream::new(file);
builder.body(Body::wrap_stream(stream))
}
Payload::NoContent => builder,
};
let response = builder.send().await?;
let status = response.status();
if self.verbose {
draw_line(&mut stderr)?;
match status.as_u16() / 100 {
2 => crossterm::execute!(
stderr,
SetStyle(*STATUS_SUCCESS_STYLE),
Print(status),
SetStyle(*DEFAULT_STYLE),
Print("\n"),
)?,
4|5 => crossterm::execute!(
stderr,
SetStyle(*STATUS_FAILURE_STYLE),
Print(status),
SetStyle(*DEFAULT_STYLE),
Print("\n"),
)?,
_ => crossterm::execute!(
stderr,
SetStyle(*STATUS_OTHER_STYLE),
Print(status),
SetStyle(*DEFAULT_STYLE),
Print("\n"),
)?,
}
for (name, value) in response.headers().iter() {
let value = value.to_str()?;
crossterm::execute!(
stderr,
SetStyle(*HEADER_NAME_STYLE),
Print(name),
Print(": "),
SetStyle(*DEFAULT_STYLE),
SetStyle(*HEADER_VALUE_STYLE),
Print(value),
SetStyle(*DEFAULT_STYLE),
Print("\n"),
)?;
}
}
eprintln!();
if self.fail {
let code = status.as_u16();
if (400..=599).contains(&code) {
crossterm::execute!(
stderr,
SetStyle(*STATUS_FAILURE_STYLE),
Print(status),
SetStyle(*DEFAULT_STYLE),
Print("\n"),
)?;
let mut stream = response.bytes_stream();
while let Some(item) = stream.next().await {
let chunk = item?;
let chunk = chunk.into_iter().collect::<Vec<u8>>();
stderr.write_all(chunk.as_slice())?;
stderr.flush()?;
}
stderr.write(&[10u8])?;
std::process::exit(code as i32 / 100);
}
}
let total_size: u64 = response.content_length().unwrap_or(0);
let pb = ProgressBar::new(total_size);
pb.set_style(
ProgressStyle::with_template(
"{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})"
)?.with_key(
"eta",
|state: &ProgressState, w: &mut dyn ::std::fmt::Write|
write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()
)
);
let content_type = response.headers()
.get(reqwest::header::CONTENT_TYPE)
.map(|value| value.to_str().unwrap_or_default())
.unwrap_or("text/plain")
.to_string();
let mut out: Box<dyn Write> = match &self.output {
Some(file) => Box::new(std::fs::File::create(file)?),
None => Box::new(Buffer::new(
&mut stdout,
self.url.path().to_lowercase(),
content_type,
)),
};
let mut downloaded: u64 = 0;
let mut stream = response.bytes_stream();
while let Some(item) = stream.next().await {
let chunk = item?;
let chunk = chunk.into_iter().collect::<Vec<u8>>();
out.write_all(chunk.as_slice())?;
downloaded = total_size.min(downloaded + chunk.len() as u64);
pb.set_position(downloaded);
}
out.flush()?;
pb.finish_and_clear();
Ok(())
}
}
fn draw_line(writer: &mut impl Write) -> Result<()> {
let width = match crossterm::terminal::size() {
Ok((width, _)) => width,
Err(_) => 80,
};
let line = "─".repeat(width as usize);
crossterm::execute!(
writer,
SetForegroundColor(Color::Black),
Print(line),
ResetColor,
)?;
Ok(())
}