repo_backup/
utils.rs

1use failure::{Error, ResultExt};
2use hyperx::header::{Link, LinkValue, RelationType};
3use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, LINK, USER_AGENT};
4use reqwest::Client;
5use reqwest::StatusCode;
6use serde::Deserialize;
7use serde_json::{self, Value};
8use std::marker::PhantomData;
9use std::vec::IntoIter;
10
11/// A convenient command runner.
12///
13/// It behaves like the `format!()` macro, then splits the input string up like
14/// your shell would before running the command and inspecting its output to
15/// ensure everything was successful.
16///
17/// # Examples
18///
19/// ```rust,no_run
20/// #[macro_use]
21/// extern crate repo_backup;
22/// # extern crate failure;
23/// # #[macro_use]
24/// # extern crate log;
25///
26/// # fn run() -> Result<(), Box<::std::error::Error>> {
27/// let some_url = "https://github.com/Michael-F-Bryan/repo-backup";
28/// cmd!(in "/path/to/dir/"; "git clone {}", some_url)?;
29/// # Ok(())
30/// # }
31/// # fn main() { run().unwrap() }
32/// ```
33#[macro_export]
34macro_rules! cmd {
35    (in $cwd:expr; $format:expr, $arg:expr) => {
36        cmd!(in $cwd; format!($format, $arg))
37    };
38    (in $cwd:expr; $command:expr) => {{
39        use ::failure::ResultExt;
40
41        let command = String::from($command);
42        trace!("Executing `{}`", command);
43        let arguments: Vec<_> = command.split_whitespace().collect();
44
45        let mut cmd_builder = ::std::process::Command::new(&arguments[0]);
46        cmd_builder.current_dir($cwd);
47
48        for arg in &arguments[1..] {
49            cmd_builder.arg(arg);
50        }
51
52        cmd_builder.output()
53            .with_context(|_| format!("Unable to execute `{}`. Is {} installed?", command, &arguments[0]))
54            .map_err(::failure::Error::from)
55            .and_then(|output| {
56                // If the command runs then we need to do a bunch of error
57                // checking, making sure to let the user know why the command
58                // failed along with the command's stdout/stderr
59                if output.status.success() {
60                    Ok(())
61                } else {
62                    match output.status.code() {
63                        Some(code) => warn!("`{}` failed with return code {}", command, code),
64                        None => warn!("`{}` failed", command),
65                    }
66
67                    if !output.stderr.is_empty() {
68                        debug!("Stderr:");
69                        for line in String::from_utf8_lossy(&output.stderr).lines() {
70                            debug!("\t{}", line);
71                        }
72                    }
73                    if !output.stdout.is_empty() {
74                        debug!("Stdout:");
75                        for line in String::from_utf8_lossy(&output.stdout).lines() {
76                            debug!("\t{}", line);
77                        }
78                    }
79
80                    Err(::failure::err_msg(format!("`{}` failed", command)))
81                }
82            })
83    }};
84    ($format:expr, $($arg:expr),*) => {
85        cmd!(format!($format, $($arg),*))
86    };
87    ($command:expr) => {
88        cmd!(in "."; $command)
89    };
90}
91
92pub struct Paginated<I>
93where
94    I: for<'de> Deserialize<'de>,
95{
96    client: Client,
97    token: String,
98    _phantom: PhantomData<I>,
99    next_endpoint: Option<String>,
100    items: IntoIter<I>,
101}
102
103impl<I> Paginated<I>
104where
105    for<'de> I: Deserialize<'de>,
106{
107    pub fn new(token: &str, endpoint: &str) -> Self {
108        Paginated {
109            client: Client::new(),
110            token: token.to_string(),
111            _phantom: PhantomData,
112            next_endpoint: Some(String::from(endpoint)),
113            items: Vec::new().into_iter(),
114        }
115    }
116
117    fn send_request(&mut self, endpoint: &str) -> Result<Vec<I>, Error> {
118        debug!("Sending request to {:?}", endpoint);
119
120        let request = self
121            .client
122            .get(endpoint)
123            .header(CONTENT_TYPE, "application/json")
124            .header(USER_AGENT, "repo-backup")
125            .header(ACCEPT, "application/vnd.github.v3+json")
126            .header(AUTHORIZATION, format!("token {}", self.token))
127            .build()
128            .context("Generated invalid request. This is a bug.")?;
129
130        if log_enabled!(::log::Level::Trace) {
131            let redacted_header =
132                format!("Request Headers {:#?}", request.headers())
133                    .replace(&self.token, "...");
134
135            for line in redacted_header.lines() {
136                trace!("{}", line);
137            }
138        }
139
140        let mut response = self
141            .client
142            .execute(request)
143            .context("Unable to send request")?;
144
145        let raw: Value = response.json()?;
146        let status = response.status();
147        let headers = response.headers();
148        debug!("Received response ({})", status);
149
150        if log_enabled!(::log::Level::Trace) {
151            for line in format!("Response Headers {:#?}", headers).lines() {
152                trace!("{}", line);
153            }
154
155            // trace!("Body:");
156            // for line in serde_json::to_string_pretty(&raw).unwrap().lines() {
157            //     trace!("{}", line);
158            // }
159        }
160
161        let got = serde_json::from_value(raw)
162            .context("Unable to deserialize response")?;
163
164        if let Some(link) = headers
165            .get(LINK)
166            .and_then(|l| l.to_str().ok())
167            .and_then(|l| l.parse().ok())
168        {
169            self.next_endpoint = next_link(&link).map(|s| s.to_string());
170        }
171
172        if !status.is_success() {
173            warn!("Request failed with {}", status);
174
175            let err = FailedRequest {
176                status: status,
177                url: endpoint.to_string(),
178            };
179
180            return Err(err.into());
181        }
182
183        Ok(got)
184    }
185}
186
187impl<I> Iterator for Paginated<I>
188where
189    for<'de> I: Deserialize<'de>,
190{
191    type Item = Result<I, Error>;
192
193    fn next(&mut self) -> Option<Self::Item> {
194        if let Some(next_item) = self.items.next() {
195            return Some(Ok(next_item));
196        }
197
198        if let Some(next_endpoint) = self.next_endpoint.take() {
199            match self.send_request(&next_endpoint) {
200                Ok(values) => {
201                    self.items = values.into_iter();
202                    return self.items.next().map(|it| Ok(it));
203                }
204                Err(e) => {
205                    return Some(Err(e));
206                }
207            }
208        }
209
210        None
211    }
212}
213fn next_link(link: &Link) -> Option<&str> {
214    link.values()
215        .iter()
216        .filter_map(|v| if is_next(v) { Some(v) } else { None })
217        .map(|v| v.link())
218        .next()
219}
220
221fn is_next(link_value: &LinkValue) -> bool {
222    link_value
223        .rel()
224        .map(|relations| relations.iter().any(|rel| *rel == RelationType::Next))
225        .unwrap_or(false)
226}
227
228#[derive(Debug, Clone, PartialEq, Fail)]
229#[fail(display = "Request failed with {}", status)]
230pub struct FailedRequest {
231    status: StatusCode,
232    url: String,
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn get_next_link() {
241        let src = r#"<https://api.github.com/user/repos?page=2>; rel="next", <https://api.github.com/user/repos?page=3>; rel="last""#;
242        let link: Link = src.parse().unwrap();
243
244        let should_be = "https://api.github.com/user/repos?page=2";
245        let got = next_link(&link).unwrap();
246        assert_eq!(got, should_be);
247    }
248}