use super::header::{lossy_header, X_DATA_TYPE};
use super::parse_data_uri;
use crate::client::HttpClient;
use crate::data::{DataDirItem, DataFile, DataFileItem, DataItem, HasDataPath};
use crate::error::{ApiError, ErrorKind, Result, ResultExt};
use serde_json;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::vec::IntoIter;
use chrono::{DateTime, Utc};
use reqwest::StatusCode;
pub struct DataDir {
path: String,
client: HttpClient,
}
#[derive(Debug, Deserialize)]
struct DeletedResponse {
result: DirectoryDeleted,
}
#[derive(Debug, Deserialize)]
pub struct DirectoryDeleted {
pub deleted: u64,
#[serde(skip_deserializing)]
_dummy: (),
}
#[derive(Debug, Deserialize, Serialize)]
struct FolderItem {
pub name: String,
pub acl: Option<DataAcl>,
}
#[derive(Debug, Deserialize)]
struct FileItem {
pub filename: String,
pub size: u64,
pub last_modified: DateTime<Utc>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct DataAcl {
pub read: Vec<String>,
_dummy: (),
}
pub enum ReadAcl {
Private,
MyAlgorithms,
Public,
#[doc(hidden)]
__Nonexhaustive,
}
impl Default for DataAcl {
fn default() -> Self {
ReadAcl::MyAlgorithms.into()
}
}
impl From<ReadAcl> for DataAcl {
fn from(acl: ReadAcl) -> Self {
match acl {
ReadAcl::Private | ReadAcl::__Nonexhaustive => DataAcl {
read: vec![],
_dummy: (),
},
ReadAcl::MyAlgorithms => DataAcl {
read: vec!["algo://.my/*".into()],
_dummy: (),
},
ReadAcl::Public => DataAcl {
read: vec!["user://*".into()],
_dummy: (),
},
}
}
}
#[derive(Debug, Deserialize)]
struct DirectoryShow {
pub acl: Option<DataAcl>,
pub folders: Option<Vec<FolderItem>>,
pub files: Option<Vec<FileItem>>,
pub marker: Option<String>,
}
pub struct DirectoryListing<'a> {
pub acl: Option<DataAcl>,
dir: &'a DataDir,
folders: IntoIter<FolderItem>,
files: IntoIter<FileItem>,
marker: Option<String>,
query_count: u32,
}
impl<'a> DirectoryListing<'a> {
fn new(dir: &'a DataDir) -> DirectoryListing<'a> {
DirectoryListing {
acl: None,
dir: dir,
folders: Vec::new().into_iter(),
files: Vec::new().into_iter(),
marker: None,
query_count: 0,
}
}
}
impl<'a> Iterator for DirectoryListing<'a> {
type Item = Result<DataItem>;
fn next(&mut self) -> Option<Self::Item> {
match self.folders.next() {
Some(d) => Some(Ok(DataItem::Dir(DataDirItem {
dir: self.dir.child(&d.name),
}))),
None => {
match self.files.next() {
Some(f) => Some(Ok(DataItem::File(DataFileItem {
size: f.size,
last_modified: f.last_modified,
file: self.dir.child(&f.filename),
}))),
None => {
if self.query_count == 0 || self.marker.is_some() {
self.query_count += 1;
match get_directory(self.dir, self.marker.clone()) {
Ok(ds) => {
self.folders = ds.folders.unwrap_or_else(Vec::new).into_iter();
self.files = ds.files.unwrap_or_else(Vec::new).into_iter();
self.marker = ds.marker;
self.next()
}
Err(err) => Some(Err(err)),
}
} else {
None
}
}
}
}
}
}
}
fn get_directory(dir: &DataDir, marker: Option<String>) -> Result<DirectoryShow> {
let mut url = dir.to_url()?;
if let Some(ref m) = marker {
url.query_pairs_mut().append_pair("marker", m);
}
let mut res = dir
.client
.get(url)
.send()
.chain_err(|| ErrorKind::Http(format!("listing directory '{}'", dir.to_data_uri())))?;
if res.status().is_success() {
match res.headers().get(X_DATA_TYPE).map(lossy_header) {
Some(ref dt) if dt == "directory" => (),
data_type => {
let dt = data_type.unwrap_or_else(|| "unknown".to_string());
return Err(ErrorKind::UnexpectedDataType("directory", dt).into());
}
}
}
let mut res_json = String::new();
res.read_to_string(&mut res_json)
.chain_err(|| ErrorKind::Io(format!("listing directory '{}'", dir.to_data_uri())))?;
match res.status() {
status if status.is_success() => {
serde_json::from_str(&res_json).chain_err(|| ErrorKind::DecodeJson("directory listing"))
}
StatusCode::NOT_FOUND => Err(ErrorKind::NotFound(dir.to_url().unwrap()).into()),
status => Err(ApiError::from_json_or_status(&res_json, status).into()),
}
}
impl HasDataPath for DataDir {
#[doc(hidden)]
fn new(client: HttpClient, path: &str) -> Self {
DataDir {
client: client,
path: parse_data_uri(path).to_string(),
}
}
#[doc(hidden)]
fn path(&self) -> &str {
&self.path
}
#[doc(hidden)]
fn client(&self) -> &HttpClient {
&self.client
}
}
impl DataDir {
pub fn list(&self) -> DirectoryListing {
DirectoryListing::new(self)
}
pub fn create<Acl: Into<DataAcl>>(&self, acl: Acl) -> Result<()> {
let parent = self
.parent()
.ok_or_else(|| ErrorKind::InvalidDataUri(self.to_data_uri()))?;
let parent_url = parent.to_url()?;
let input_data = FolderItem {
name: self
.basename()
.ok_or_else(|| ErrorKind::InvalidDataUri(self.to_data_uri()))?
.into(),
acl: Some(acl.into()),
};
let mut res = self
.client
.post(parent_url)
.json(&input_data)
.send()
.chain_err(|| {
ErrorKind::Http(format!("creating directory '{}'", self.to_data_uri()))
})?;
match res.status() {
status if status.is_success() => Ok(()),
StatusCode::NOT_FOUND => Err(ErrorKind::NotFound(self.to_url().unwrap()).into()),
status => {
let mut res_json = String::new();
res.read_to_string(&mut res_json).chain_err(|| {
ErrorKind::Io(format!("creating directory '{}'", self.to_data_uri()))
})?;
Err(ApiError::from_json_or_status(&res_json, status).into())
}
}
}
pub fn delete(&self, force: bool) -> Result<DirectoryDeleted> {
let mut url = self.to_url()?;
if force {
url.query_pairs_mut().append_pair("force", "true");
}
let mut res = self.client.delete(url).send().chain_err(|| {
ErrorKind::Http(format!("deleting directory '{}'", self.to_data_uri()))
})?;
let mut res_json = String::new();
res.read_to_string(&mut res_json)
.chain_err(|| ErrorKind::Io(format!("deleting directory '{}'", self.to_data_uri())))?;
match res.status() {
status if status.is_success() => serde_json::from_str::<DeletedResponse>(&res_json)
.map(|res| res.result)
.chain_err(|| ErrorKind::DecodeJson("directory deletion response")),
StatusCode::NOT_FOUND => Err(ErrorKind::NotFound(self.to_url().unwrap()).into()),
status => Err(ApiError::from_json_or_status(&res_json, status).into()),
}
}
pub fn put_file<P: AsRef<Path>>(&self, file_path: P) -> Result<()> {
let path_ref = file_path.as_ref();
let file = File::open(path_ref).chain_err(|| {
ErrorKind::Io(format!("opening file for upload '{}'", path_ref.display()))
})?;
let filename = path_ref.file_name().unwrap().to_string_lossy();
let data_file: DataFile = self.child(&filename);
data_file.put(file)
}
pub fn child<T: HasDataPath>(&self, filename: &str) -> T {
let new_uri = match self.to_data_uri() {
ref uri if uri.ends_with('/') => format!("{}{}", uri, filename),
uri => format!("{}/{}", uri, filename),
};
T::new(self.client.clone(), &new_uri)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::HasDataPath;
use crate::Algorithmia;
fn mock_client() -> Algorithmia {
Algorithmia::client("").unwrap()
}
#[test]
fn test_to_url() {
let dir = mock_client().dir("data://anowell/foo");
assert_eq!(
dir.to_url().unwrap().path(),
"/v1/connector/data/anowell/foo"
);
}
#[test]
fn test_to_data_uri() {
let dir = mock_client().dir("/anowell/foo");
assert_eq!(dir.to_data_uri(), "data://anowell/foo".to_string());
}
#[test]
fn test_parent() {
let dir = mock_client().dir("data://anowell/foo");
let expected = mock_client().dir("data://anowell");
assert_eq!(dir.parent().unwrap().path, expected.path);
let dir = mock_client().dir("dropbox://anowell/foo");
let expected = mock_client().dir("dropbox://anowell");
assert_eq!(dir.parent().unwrap().path, expected.path);
let dir = mock_client().dir("data://anowell");
let expected = mock_client().dir("data://");
assert_eq!(dir.parent().unwrap().path, expected.path);
let dir = mock_client().dir("data://");
assert!(dir.parent().is_none());
}
#[test]
fn test_default_acl() {
let acl: DataAcl = DataAcl::default();
assert_eq!(acl.read, vec!["algo://.my/*".to_string()]);
}
#[test]
fn test_private_acl() {
let acl: DataAcl = ReadAcl::Private.into();
assert!(acl.read.is_empty());
}
#[test]
fn test_public_acl() {
let acl: DataAcl = ReadAcl::Public.into();
assert_eq!(acl.read, vec!["user://*".to_string()]);
}
#[test]
fn test_myalgos_acl() {
let acl: DataAcl = ReadAcl::MyAlgorithms.into();
assert_eq!(acl.read, vec!["algo://.my/*".to_string()]);
}
}