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