kodumaro_http_cli/request/
connect.rs

1use 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),  /* XXX: create dedicated style */
118                    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}