kodumaro_http_cli/request/
connect.rs1use std::io::Write;
2
3use crossterm::style::{Color, Print, ResetColor, SetForegroundColor, SetStyle};
4use eyre::Result;
5use futures::StreamExt;
6use indicatif::{ProgressBar, ProgressState, ProgressStyle};
7use reqwest::{header, Body};
8use reqwest::{redirect::Policy, Request, RequestBuilder};
9use serde_json::Value;
10use tokio::fs::File;
11use tokio_util::io::ReaderStream;
12
13use crate::buffer::Buffer;
14use crate::cli::utils;
15use crate::output::format_by_ext;
16use crate::styles::*;
17
18use super::http_request::HttpRequest;
19use super::payload::Payload;
20
21pub trait Connector {
22
23 #[allow(async_fn_in_trait)]
24 async fn start(&self) -> Result<()>;
25}
26
27impl Connector for HttpRequest {
28
29 async fn start(&self) -> Result<()> {
30 let request: Request = self.into();
31 let content_type = request.headers()
32 .get(header::CONTENT_TYPE)
33 .and_then(|v| v.to_str().ok())
34 .unwrap_or("application/octect-stream")
35 .to_string();
36 let policy: Policy = self.into();
37 let client = reqwest::Client::builder()
38 .danger_accept_invalid_certs(!self.verify)
39 .redirect(policy)
40 .build()?;
41
42 let builder = RequestBuilder::from_parts(client.clone(), request);
43 let mut stdout = std::io::stdout();
44 let mut stderr = std::io::stderr();
45
46 if self.verbose {
47 if let Some(builder) = builder.try_clone() {
48 let request = builder.build()?;
49
50 if !self.verify {
51 crossterm::execute!(
52 stderr,
53 SetStyle(*STATUS_FAILURE_STYLE),
54 Print("Skipping SSL verification"),
55 SetStyle(*DEFAULT_STYLE),
56 Print("\n"),
57 )?;
58 }
59
60 crossterm::execute!(
61 stderr,
62 SetStyle(*METHOD_STYLE),
63 Print(request.method()),
64 Print(" "),
65 SetStyle(*DEFAULT_STYLE),
66 SetStyle(*URL_STYLE),
67 Print(request.url().to_string()),
68 SetStyle(*DEFAULT_STYLE),
69 Print("\n"),
70 )?;
71 for (name, value) in request.headers().iter() {
72 let value = value.to_str()?;
73 crossterm::execute!(
74 stderr,
75 SetStyle(*HEADER_NAME_STYLE),
76 Print(name),
77 Print(": "),
78 SetStyle(*DEFAULT_STYLE),
79 SetStyle(*HEADER_VALUE_STYLE),
80 Print(value),
81 SetStyle(*DEFAULT_STYLE),
82 Print("\n"),
83 )?;
84 }
85
86 eprintln!();
87 match &self.payload {
88 Payload::Content(content) => {
89 format_by_ext(
90 &content,
91 utils::extension_from_mime(&content_type),
92 &mut stderr,
93 )?;
94 eprintln!();
95 }
96 Payload::FileUpload(filename) => {
97 crossterm::execute!(
98 stderr,
99 SetStyle(*DEFAULT_STYLE),
100 Print("Content from file: "),
101 SetStyle(*STATUS_OTHER_STYLE),
102 Print(filename),
103 SetStyle(*DEFAULT_STYLE),
104 Print("\n"),
105 )?;
106 }
107 Payload::NoContent => (),
108 }
109 }
110 }
111 if self.dry_run {
112 if let Some(output) = &self.output {
113 crossterm::execute!(
114 stderr,
115 SetStyle(*DEFAULT_STYLE),
116 Print("save output into "),
117 SetStyle(*HEADER_VALUE_STYLE), Print(output),
119 SetStyle(*DEFAULT_STYLE),
120 Print("\n"),
121 )?;
122 }
123 return Ok(());
124 }
125
126 let builder = match &self.payload {
127 Payload::Content(content) if content_type.contains("json") => {
128 let content: Value = serde_json::from_str(content)?;
129 builder.json(&content)
130 }
131 Payload::Content(content) => builder.body(content.to_owned()),
132 Payload::FileUpload(filename) => {
133 let file = File::open(filename).await?;
134 let stream = ReaderStream::new(file);
135 builder.body(Body::wrap_stream(stream))
136 }
137 Payload::NoContent => builder,
138 };
139
140 let response = builder.send().await?;
141 let status = response.status();
142
143 if self.verbose {
144 draw_line(&mut stderr)?;
145
146 match status.as_u16() / 100 {
147 2 => crossterm::execute!(
148 stderr,
149 SetStyle(*STATUS_SUCCESS_STYLE),
150 Print(status),
151 SetStyle(*DEFAULT_STYLE),
152 Print("\n"),
153 )?,
154 4|5 => crossterm::execute!(
155 stderr,
156 SetStyle(*STATUS_FAILURE_STYLE),
157 Print(status),
158 SetStyle(*DEFAULT_STYLE),
159 Print("\n"),
160 )?,
161 _ => crossterm::execute!(
162 stderr,
163 SetStyle(*STATUS_OTHER_STYLE),
164 Print(status),
165 SetStyle(*DEFAULT_STYLE),
166 Print("\n"),
167 )?,
168 }
169 for (name, value) in response.headers().iter() {
170 let value = value.to_str()?;
171 crossterm::execute!(
172 stderr,
173 SetStyle(*HEADER_NAME_STYLE),
174 Print(name),
175 Print(": "),
176 SetStyle(*DEFAULT_STYLE),
177 SetStyle(*HEADER_VALUE_STYLE),
178 Print(value),
179 SetStyle(*DEFAULT_STYLE),
180 Print("\n"),
181 )?;
182 }
183 }
184 eprintln!();
185
186 if self.fail {
187 let code = status.as_u16();
188 if (400..=599).contains(&code) {
189 crossterm::execute!(
190 stderr,
191 SetStyle(*STATUS_FAILURE_STYLE),
192 Print(status),
193 SetStyle(*DEFAULT_STYLE),
194 Print("\n"),
195 )?;
196
197 let mut stream = response.bytes_stream();
198 while let Some(item) = stream.next().await {
199 let chunk = item?;
200 let chunk = chunk.into_iter().collect::<Vec<u8>>();
201 stderr.write_all(chunk.as_slice())?;
202 stderr.flush()?;
203 }
204 stderr.write(&[10u8])?;
205 std::process::exit(code as i32 / 100);
206 }
207 }
208
209 let total_size: u64 = response.content_length().unwrap_or(0);
210 let pb = ProgressBar::new(total_size);
211 pb.set_style(
212 ProgressStyle::with_template(
213 "{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})"
214 )?.with_key(
215 "eta",
216 |state: &ProgressState, w: &mut dyn ::std::fmt::Write|
217 write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()
218 )
219 );
220
221 let content_type = response.headers()
222 .get(reqwest::header::CONTENT_TYPE)
223 .map(|value| value.to_str().unwrap_or_default())
224 .unwrap_or("text/plain")
225 .to_string();
226
227 let mut out: Box<dyn Write> = match &self.output {
228 Some(file) => Box::new(std::fs::File::create(file)?),
229 None => Box::new(Buffer::new(
230 &mut stdout,
231 self.url.path().to_lowercase(),
232 content_type,
233 )),
234 };
235
236 let mut downloaded: u64 = 0;
237 let mut stream = response.bytes_stream();
238
239 while let Some(item) = stream.next().await {
240 let chunk = item?;
241 let chunk = chunk.into_iter().collect::<Vec<u8>>();
242 out.write_all(chunk.as_slice())?;
243 downloaded = total_size.min(downloaded + chunk.len() as u64);
244 pb.set_position(downloaded);
245 }
246 out.flush()?;
247 pb.finish_and_clear();
248
249 Ok(())
250 }
251}
252
253fn draw_line(writer: &mut impl Write) -> Result<()> {
254 let width = match crossterm::terminal::size() {
255 Ok((width, _)) => width,
256 Err(_) => 80,
257 };
258 let line = "─".repeat(width as usize);
259 crossterm::execute!(
260 writer,
261 SetForegroundColor(Color::Black),
262 Print(line),
263 ResetColor,
264 )?;
265 Ok(())
266}