1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/*!
Simple functions intended to use in __Rust__ `build.rs` scripts for tasks which related to fetching from _HTTP_ and unrolling `.tar.gz` archives with precompiled binaries and etc.

```
use fetch_unroll::{
    Config,
    fetch_unroll,
};

let pack_url = format!(
    "{base}/{user}/{repo}/releases/download/{ver}/{pkg}_{prof}.tar.gz",
    base = "https://github.com",
    user = "katyo",
    repo = "oboe-rs",
    pkg = "liboboe-ext",
    ver = "0.1.0",
    prof = "release",
);

let dest_dir = "target/test_download";

// Creating destination directory
std::fs::create_dir_all(dest_dir).unwrap();

// Fetching and unrolling archive
fetch_unroll(pack_url, dest_dir, Config::default()).unwrap();
```
 */

use std::{
    path::Path,
    io::{Error as IoError, Cursor, Read},
    error::{Error as StdError},
    result::{Result as StdResult},
    fmt::{Display, Formatter, Result as FmtResult},
};

use http_req::{
    request::{get as http_get},
    error::{Error as HttpError},
};
use libflate::gzip::Decoder;
use tar::Archive;

/// Result type
pub type Result<T> = StdResult<T, Error>;

/// Status type
///
/// The result without payload
pub type Status = Result<()>;

/// Error type
#[derive(Debug)]
pub enum Error {
    /// Generic HTTP error
    Http(HttpError),

    /// Generic IO error
    Io(IoError),

    /// Redirect error
    Redirect(String),

    /// Invalid response status
    Status(&'static str),
}

impl StdError for Error {}

impl Display for Error {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        use self::Error::*;
        match self {
            Http(error) => {
                "Http error: ".fmt(f)?;
                error.fmt(f)
            },
            Io(error) => {
                "IO error: ".fmt(f)?;
                error.fmt(f)
            },
            Status(error) => {
                "Invalid status: ".fmt(f)?;
                error.fmt(f)
            },
            Redirect(href) => {
                "Redirect loop: \"".fmt(f)?;
                href.fmt(f)?;
                "\"".fmt(f)
            },
        }
    }
}

impl From<HttpError> for Error {
    fn from(error: HttpError) -> Self {
        Error::Http(error)
    }
}

impl From<IoError> for Error {
    fn from(error: IoError) -> Self {
        Error::Io(error)
    }
}

impl From<&'static str> for Error {
    fn from(error: &'static str) -> Self {
        Error::Status(error)
    }
}

impl From<String> for Error {
    fn from(error: String) -> Self {
        Error::Redirect(error)
    }
}

/// Configuration options
pub struct Config {
    /// The maximum number of redirects
    pub redirect_limit: usize,
}

/// Default limit for redirect
const DEFAULT_REDIRECT_LIMIT: usize = 20;

impl Default for Config {
    fn default() -> Self {
        Self {
            redirect_limit: DEFAULT_REDIRECT_LIMIT,
        }
    }
}

impl AsRef<Config> for Config {
    fn as_ref(&self) -> &Self {
        self
    }
}

/// Fetch archive from url and unroll to directory
pub fn fetch_unroll<U: AsRef<str>, D: AsRef<Path>, C: AsRef<Config>>(href: U, path: D, conf: C) -> Status {
    unroll(fetch(href, conf)?, path)
}

/// Fetch url with limited redirect
pub fn fetch<U: AsRef<str>, C: AsRef<Config>>(href: U, conf: C) -> Result<Cursor<Vec<u8>>> {
    let mut href = String::from(href.as_ref());
    let mut limit = conf.as_ref().redirect_limit;
    loop {
        return match fetch_raw(href) {
            Ok(body) => Ok(body),
            Err(Error::Redirect(location)) => {
                limit -= 1;
                if limit > 0 {
                    href = location;
                    continue;
                } else {
                    Err(Error::Redirect(location))
                }
            },
            Err(error) => Err(error),
        };
    }
}

/// Fetch url without redirects
fn fetch_raw<U: AsRef<str>>(href: U) -> Result<Cursor<Vec<u8>>> {
    let mut body = Vec::new();
    let response = http_get(href, &mut body).map_err(Error::from)?;

    let status_code = response.status_code();

    if status_code.is_redirect() {
        if let Some(href) = response.headers().get("Location") {
            return Err(Error::from(href.clone()));
        }
    }

    if status_code.is_success() {
        Ok(Cursor::new(body))
    } else {
        Err(Error::from(status_code.reason().unwrap_or("Wrong")))
    }
}

/// Unroll packed data (.tar.gz)
pub fn unroll<S: Read, D: AsRef<Path>>(pack: S, path: D) -> Status {
    let unpacker = Decoder::new(pack).map_err(Error::from)?;
    let mut extractor = Archive::new(unpacker);

    extractor.unpack(path).map_err(Error::from)
}