getit 0.1.0

An easy to use file getter for local and remote files
Documentation
use bytes::Bytes;
use reqwest;
use std::fs;
use std::io::prelude::*;
use std::path::PathBuf;
use url::Url;

pub async fn get(url_str: &str) -> Result<Bytes, String> {
    println!("Fetching URL: {}", url_str);
    if let Ok(url) = Url::parse(url_str) {
        match url.scheme() {
            "http" | "https" => get_http(url_str).await,
            #[cfg(feature = "ftp")]
            "ftp" | "ftps" => get_ftp(url_str).await,
            "file" => {
                let mut path = PathBuf::new();
                if let Some(host) = url.host() {
                    path.push(host.to_string());
                }
                let path_str = url.path();
                get_file(path_str).await
            }
            _ => Err(format!("Unsupported scheme: {}", url.scheme())),
        }
    } else {
        get_file(url_str).await
    }
}

async fn get_file(path_str: &str) -> Result<Bytes, String> {
    let mut path = PathBuf::new();
    if let Some(stripped_path_str) = path_str.strip_prefix("/") {
        path.push(stripped_path_str);
    } else {
        path.push(path_str);
    }
    println!("Path: {}", path.display());
    if !path.exists() {
        return Err(format!("File does not exist: {}", path.display()));
    }
    let mut file = match fs::File::open(&path) {
        Ok(file) => file,
        Err(err) => return Err(format!("Failed to open file: {}", err)),
    };

    let mut contents = Vec::new();
    match file.read_to_end(&mut contents) {
        Ok(_) => Ok(Bytes::copy_from_slice(&contents)),
        Err(err) => Err(format!("Failed to read file: {}", err)),
    }
}

async fn get_http(url: &str) -> Result<Bytes, String> {
    let response = match reqwest::get(url).await {
        Ok(response) => response,
        Err(err) => return Err(format!("Request failed: {}", err)),
    };
    response
        .bytes()
        .await
        .map_err(|err| format!("Failed to read response: {}", err))
}

#[cfg(feature = "ftp")]
async fn get_ftp(url: &str) -> Result<Bytes, String> {
    use suppaftp;
    let url_parts = Url::parse(url).map_err(|err| format!("Invalid URL: {}", err))?;
    let path = &url_parts.path();

    use suppaftp::native_tls::TlsConnector;
    use suppaftp::{NativeTlsConnector, NativeTlsFtpStream};

    if let None = url_parts.host() {
        return Err(format!("Invalid FTP URL: missing host"));
    }
    let host;
    if let url::Host::Domain(inner_host) = url_parts.host().unwrap() {
        host = inner_host;
    } else {
        return Err(format!("Invalid FTP URL: missing host"));
    }

    let port;
    if let Some(port_result) = url_parts.port() {
        port = port_result;
    } else {
        port = 21;
    }

    let ftp_stream = NativeTlsFtpStream::connect(format!("{}:{}", host, port))
        .map_err(|err| format!("Failed to connect to FTP server: {}", err))?;

    let tls_connector = NativeTlsConnector::from(TlsConnector::new().unwrap());

    let secured_ftp_stream = ftp_stream.into_secure(tls_connector, host);
    let mut ftp_stream = match secured_ftp_stream {
        Ok(secured_ftp_stream) => secured_ftp_stream,
        Err(err) => {
            println!("Failed to secure FTP connection: {}", err);
            println!("Attempting unsecured connection...");
            NativeTlsFtpStream::connect(format!("{}:{}", host, port))
                .map_err(|err| format!("Failed to connect to FTP server: {}", err))?
        }
    };

    let _ = ftp_stream.login(url_parts.username(), url_parts.password().unwrap_or(""));

    let data = ftp_stream
        .retr_as_buffer(path)
        .map_err(|err| format!("Error retrieving file: {}", err))?;

    if ftp_stream.quit().is_err() {
        Err("Failed to close FTP connection".to_string())
    } else {
        Ok(Bytes::from(data.into_inner()))
    }
}

#[cfg(test)]
mod tests {
    use std::env;

    use super::*;
    use tokio;

    #[tokio::test]
    async fn get_http_test() {
        let url = "http://example.com";
        println!("url: {}", url);
        let result = get(url).await;
        match result {
            Ok(bytes) => assert!(bytes.len() > 0),
            Err(err) => panic!("Error: {}", err),
        }
    }

    #[tokio::test]
    async fn get_https_test() {
        let url = "https://example.com";
        println!("url: {}", url);
        let result = get(url).await;
        match result {
            Ok(bytes) => assert!(bytes.len() > 0),
            Err(err) => panic!("Error: {}", err),
        }
    }

    #[cfg(feature = "ftp")]
    #[tokio::test]
    async fn get_ftp_test() {
        let url = "ftp://anonymous:guest@ftp.x.org:21/pub/current/index.html";
        println!("url: {}", url);
        let result = get(url).await;
        match result {
            Ok(bytes) => assert!(bytes.len() > 0),
            Err(err) => panic!("Error: {}", err),
        }
    }

    #[tokio::test]
    async fn get_file_test() {
        let result = get("Cargo.toml").await;
        if result.is_err() {
            panic!("Error: {}", result.err().unwrap());
        }
    }

    #[tokio::test]
    async fn get_file_url_test() {
        let current_directory = env::current_dir().unwrap();
        let path = current_directory.join("Cargo.toml");
        let url = Url::from_file_path(path.as_path()).unwrap();
        let result = get(&url.to_string()).await;
        if result.is_err() {
            panic!("Error: {}", result.err().unwrap());
        }
    }
}