//! A thin wrapper around [curl-rust](https://crates.io/crates/curl) to make //! get requests and download files. use curl; use curl::easy::{Easy, List, WriteError}; use std::fmt; use std::fs; use std::fs::File; use std::io; use std::io::Write; use std::path::Path; use std::path::PathBuf; use std::str; use std::str::Utf8Error; use url; use url::Url; /// Wraps `curl::Error`, `Utf8Error`, `io::Error` and `url::ParseError`. #[derive(Debug)] pub enum HttpError { Error(&'static str), CurlError(curl::Error), Utf8Error(Utf8Error), IOError(io::Error), UrlParseError(url::ParseError), } impl fmt::Display for HttpError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { HttpError::Error(ref err) => fmt::Display::fmt(err, f), HttpError::CurlError(ref err) => fmt::Display::fmt(err, f), HttpError::Utf8Error(ref err) => fmt::Display::fmt(err, f), HttpError::IOError(ref err) => fmt::Display::fmt(err, f), HttpError::UrlParseError(ref err) => fmt::Display::fmt(err, f), } } } impl From for HttpError { fn from(e: curl::Error) -> Self { HttpError::CurlError(e) } } impl From for HttpError { fn from(e: Utf8Error) -> Self { HttpError::Utf8Error(e) } } impl From for HttpError { fn from(e: io::Error) -> Self { HttpError::IOError(e) } } impl From for HttpError { fn from(e: url::ParseError) -> Self { HttpError::UrlParseError(e) } } impl From for WriteError { fn from(_: HttpError) -> Self { WriteError::__Nonexhaustive } } /// Wraps curl-rust. pub struct Http { curl: Easy, } impl Http { /// Create a new instance of `Http`. /// Ensures that curl follows redirects. pub fn new() -> Http { let mut curl = Easy::new(); curl.follow_location(true).unwrap(); Http { curl: curl } } /// Make a get request and return the response. /// /// # Example /// ``` /// use http::Http; /// /// let mut http_client = Http::new(); /// http_client.get("https://discworld.com/"); /// ``` pub fn get(&mut self, uri: &str) -> Result { let mut data = Vec::new(); self.curl.url(uri)?; { let mut transfer = self.curl.transfer(); transfer .write_function(|new_data| { data.extend_from_slice(new_data); Ok(new_data.len()) })?; transfer.perform()?; } match str::from_utf8(data.as_slice()) { Ok(value) => Ok(value.to_owned()), Err(error) => Err(HttpError::Utf8Error(error)), } } /// Add a header to all future requests. /// /// # Example /// ``` /// use http::Http; /// /// let mut http_client = Http::new(); /// http_client.add_header("X-Clacks-Overhead: GNU Terry Pratchett"); /// ``` pub fn add_header(&mut self, header: &str) -> Result<(), HttpError> { let mut list = List::new(); list.append(header)?; match self.curl.http_headers(list) { Ok(_) => Ok(()), Err(error) => Err(HttpError::CurlError(error)), } } /// Find the filename for a download uri. /// /// Useful if the initial link gets redirected. /// /// The filename is taken from the last URI segment and returned in the result. /// # Example /// ``` /// use http::Http; /// use std::path::Path; /// /// let mut http_client = Http::new(); /// /// http_client.get_filename("https://example.com/sed"); /// ``` pub fn get_filename(&mut self, download_uri: &str) -> Result { self.curl.url(download_uri)?; self.curl.nobody(true)?; self.curl.perform()?; self.curl.nobody(false)?; let download_url_string = match self.curl.effective_url()? { Some(value) => value, None => return Err(HttpError::Error("Can't get effective download url.")), }; let download_url = Url::parse(download_url_string)?; let download_url_segments = match download_url.path_segments() { Some(value) => value, None => return Err(HttpError::Error("Can't parse download segments.")), }; let file_name = match download_url_segments.last() { Some(value) => value, None => return Err(HttpError::Error("No segments in download url.")), }; Ok(file_name.to_owned()) } /// Download a file to the specified folder without creating the folder. /// /// The filename is taken from the last URI segment and returned in the result. /// # Example /// ``` /// use http::Http; /// use std::path::Path; /// /// let mut http_client = Http::new(); /// /// let download_path = Path::new("/opt/bin"); /// http_client.download("https://example.com/sed", &download_path); /// ``` pub fn download(&mut self, download_uri: &str, download_dir: &PathBuf) -> Result { let download_path_tmp = Path::new(download_dir.as_os_str()).join(".progress"); let mut file_download = File::create(&download_path_tmp)?; self.curl.url(download_uri)?; { let mut transfer = self.curl.transfer(); transfer .write_function(|data| { match file_download.write(data) { Ok(_) => Ok(()), Err(error) => Err(HttpError::IOError(error)), }?; Ok(data.len()) })?; transfer.perform()?; } let download_url_string = match self.curl.effective_url()? { Some(value) => value, None => return Err(HttpError::Error("Can't get effective download url.")), }; let download_url = Url::parse(download_url_string)?; let download_url_segments = match download_url.path_segments() { Some(value) => value, None => return Err(HttpError::Error("Can't parse download segments.")), }; let file_name = match download_url_segments.last() { Some(value) => value, None => return Err(HttpError::Error("No segments in download url.")), }; let download_path = Path::new(download_dir.as_os_str()).join(file_name); match fs::rename(download_path_tmp, download_path) { Ok(_) => Ok(()), Err(error) => Err(HttpError::IOError(error)), }?; Ok(file_name.to_owned()) } }