kodumaro_http_cli/
lib.rs

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}