gog-sync 0.3.4

Synchronizes a GOG library with a local folder.
//! 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<curl::Error> for HttpError {
    fn from(e: curl::Error) -> Self {
        HttpError::CurlError(e)
    }
}

impl From<Utf8Error> for HttpError {
    fn from(e: Utf8Error) -> Self {
        HttpError::Utf8Error(e)
    }
}

impl From<io::Error> for HttpError {
    fn from(e: io::Error) -> Self {
        HttpError::IOError(e)
    }
}

impl From<url::ParseError> for HttpError {
    fn from(e: url::ParseError) -> Self {
        HttpError::UrlParseError(e)
    }
}

impl From<HttpError> 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<String, HttpError> {
        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<String, HttpError> {
        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<String, HttpError> {
        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())
    }
}