ass-rs 3.0.0

A library for working with Aptoma Smooth Storage
//!
//! Module for working with files in smooth storage
//!

use crate::{image_handling, AssClient, AssError, AssErrorKind, FileData, ImageData};
use reqwest::multipart::Form;
use reqwest::Url;
use serde_json::Value;
use std::path::PathBuf;

/// Search for files
pub async fn search(
    ass_client: &AssClient,
    queries: &[(&str, &str)],
) -> Result<Vec<FileData>, AssError> {
    let client = reqwest::Client::builder()
        .default_headers(ass_client.get_headers()?)
        .build()?;
    let url = Url::parse(&ass_client.url_string())?;
    let url = url.join("files")?;
    let url = Url::parse_with_params(url.as_str(), queries)?;

    let res = client.get(url).send().await?;

    let data: Vec<FileData> = res.json().await?;

    Ok(data)
}

/// Upload file without any headers
pub async fn upload_file<T: Into<PathBuf>>(
    ass_client: &AssClient,
    path: T,
    destination: &str,
) -> Result<FileData, AssError> {
    let path = path.into();
    let url = Url::parse(&ass_client.url_string())?;
    let url = url.join(&format!("files/{}", destination))?;
    let file_name = get_filename_from_path(&path)?;
    let url = url.join(file_name)?;

    let stream = std::fs::read(path)?;
    let form = Form::new().part("file", reqwest::multipart::Part::stream(stream));

    let client = reqwest::Client::builder()
        .default_headers(ass_client.get_headers()?)
        .build()?;

    let res = client.post(url).multipart(form).send().await?;

    let data: FileData = res.json().await?;
    Ok(data)
}

/// Upload file with defaults headers
pub async fn upload_file_with_headers<T: Into<PathBuf>>(
    ass_client: &AssClient,
    path: T,
    destination: &str,
    headers: &[(&str, &str)],
) -> Result<FileData, AssError> {
    let path = path.into();
    let url = Url::parse(&ass_client.url_string())?;
    let url = url.join(&format!("files/{}", destination))?;
    let file_name = get_filename_from_path(&path)?;
    let url = url.join(file_name)?;

    let stream = std::fs::read(path)?;
    let form = Form::new().part("file", reqwest::multipart::Part::stream(stream));

    let client = reqwest::Client::builder()
        .default_headers(ass_client.get_headers()?)
        .build()?;

    let mut builder = client.post(url).multipart(form);

    for (k, v) in headers {
        builder = builder.header(*k, *v);
    }

    let res = builder.send().await?;

    let data: FileData = res.json().await?;
    Ok(data)
}

/// Gets signed url for a file on the given path
pub fn get_file_url(ass_client: &AssClient, path: &str) -> Result<String, AssError> {
    let url = Url::parse(&ass_client.url_string())?;
    let url = url.join(&format!("users/{}/files/{}", ass_client.name, path))?;
    let url = ass_client.sign_url(&url.as_str())?;
    Ok(url.to_string())
}

/// Returns file information, queried by id
pub async fn get_file_information_by_id(
    ass_client: &AssClient,
    id: u64,
) -> Result<FileData, AssError> {
    let url = Url::parse(&ass_client.url_string())?;
    let url = url.join(&format!("files/{}", id))?;
    let client = reqwest::Client::builder()
        .default_headers(ass_client.get_headers()?)
        .build()?;
    let res = client.get(url).send().await?;
    let data: FileData = res.json().await?;
    Ok(data)
}

/// Returns file information, queried by path
pub async fn get_file_information_by_path(
    ass_client: &AssClient,
    path: &str,
) -> Result<FileData, AssError> {
    let url = Url::parse(&ass_client.url_string())?;
    let url = url.join(&format!("files/path/{}", path))?;
    let client = reqwest::Client::builder()
        .default_headers(ass_client.get_headers()?)
        .build()?;
    let res = client.get(url).send().await?;
    let data: FileData = res.json().await?;
    Ok(data)
}

/// Returns a default image rendition of a file
pub async fn get_file_rendition(
    ass_client: &AssClient,
    file_id: u64,
) -> Result<ImageData, AssError> {
    let url = Url::parse(&ass_client.url_string())?;
    let url = url.join(&format!("files/{}/image", file_id))?;

    let client = reqwest::Client::builder()
        .default_headers(ass_client.get_headers()?)
        .build()?;

    let res = client.get(url).send().await?;

    let data: Value = res.json().await?;
    image_handling::get_image_information(
        ass_client,
        data.get("image_id")
            .ok_or_else(|| AssError::from(AssErrorKind::JsonError))?
            .as_u64()
            .ok_or_else(|| AssError::from(AssErrorKind::JsonError))?,
    )
    .await
}

fn get_filename_from_path(path: &PathBuf) -> Result<&str, AssError> {
    path.file_name().and_then(|s| s.to_str()).ok_or_else(|| {
        AssError::invalid_file_name(
            "Error parsing filename".to_string(),
            path.to_str()
                .expect("PathBuf failed conversion to str")
                .to_string(),
        )
    })
}

#[cfg(test)]
mod tests {
    use crate::{file_handling, AssClient};
    use mockito;

    macro_rules! aw {
        ($e:expr) => {
            tokio_test::block_on($e)
        };
    }

    #[test]
    fn test_file_upload() {
        let _m = mockito::mock("POST", "/files/file-path/account.json")
            .match_header("Authorization", "bearer apikey")
            .with_status(201)
            .with_header("content-type", "application/json")
            .with_body("{\"id\": 2, \"user_id\": 3, \"path\": \"path\", \"md5\": \"checksum\", \"content_type\": \"text\", \"original_url\": \"url.com\", \"created\": \"2013-08-21T09:30:50.068Z\", \"updated\": \"2013-08-21T09:30:50.068Z\"}")
            .create();

        let url = &mockito::server_url();

        let ass_client =
            AssClient::create(url, "account", "apikey").expect("Could not get AssClient");

        let result = aw!(file_handling::upload_file(
            &ass_client,
            "./data/account.json",
            "file-path/"
        ))
        .expect("Could not get result");
        assert_eq!(result.id, 2);
    }

    #[test]
    fn test_file_upload_with_headers() {
        let _m = mockito::mock("POST", "/files/file-path/account.json")
            .match_header("Authorization", "bearer apikey")
            .match_header("Cache-Control", "max-age: 234")
            .with_status(201)
            .with_header("content-type", "application/json")
            .with_body("{\"id\": 2, \"user_id\": 3, \"path\": \"path\", \"md5\": \"checksum\", \"content_type\": \"text\", \"original_url\": \"url.com\", \"created\": \"2013-08-21T09:30:50.068Z\", \"updated\": \"2013-08-21T09:30:50.068Z\"}")
            .create();

        let url = &mockito::server_url();

        let ass_client =
            AssClient::create(url, "account", "apikey").expect("Could not get Account");

        let result = aw!(file_handling::upload_file_with_headers(
            &ass_client,
            "./data/account.json",
            "file-path/",
            &[("Cache-Control", "max-age: 234")],
        ))
        .expect("Could not get result");
        assert_eq!(result.id, 2);
    }
}