use client::HttpClient;
use error::{self, ApiError, Error, ErrorKind, Result, ResultExt};
use data::{DataItem, DataDirItem, DataFileItem, HasDataPath, DataFile};
use super::parse_data_uri;
use super::header::XDataType;
use json;
use std::io::Read;
use std::fs::File;
use std::path::Path;
use std::vec::IntoIter;
use chrono::{DateTime, UTC};
use reqwest::header::ContentType;
use reqwest::StatusCode;
#[cfg(feature="with-rustc-serialize")]
use rustc_serialize::{Decodable, Decoder};
pub struct DataDir {
path: String,
client: HttpClient,
}
#[cfg_attr(feature="with-serde", derive(Deserialize))]
#[cfg_attr(feature="with-rustc-serialize", derive(RustcDecodable))]
struct DeletedResponse {
result: DirectoryDeleted,
}
#[cfg_attr(feature="with-serde", derive(Deserialize))]
#[cfg_attr(feature="with-rustc-serialize", derive(RustcDecodable))]
#[derive(Debug)]
pub struct DirectoryDeleted {
pub deleted: u64,
#[cfg_attr(feature="with-serde", serde(skip_deserializing))]
_dummy: (),
}
#[cfg_attr(feature="with-serde", derive(Deserialize, Serialize))]
#[cfg_attr(feature="with-rustc-serialize", derive(RustcDecodable, RustcEncodable))]
#[derive(Debug)]
struct FolderItem {
pub name: String,
pub acl: Option<DataAcl>,
}
#[cfg_attr(feature="with-serde", derive(Deserialize))]
#[derive(Debug)]
struct FileItem {
pub filename: String,
pub size: u64,
pub last_modified: DateTime<UTC>,
}
#[cfg(feature="with-rustc-serialize")]
#[deprecated(since="2.1.0", note="rustc-serialize has been deprecated")]
impl Decodable for FileItem {
fn decode<D: Decoder>(d: &mut D) -> ::std::result::Result<FileItem, D::Error> {
use std::error::Error;
d.read_struct("root", 0, |d| {
Ok(FileItem {
filename: d.read_struct_field("filename", 0, |d| Decodable::decode(d))?,
size: d.read_struct_field("size", 0, |d| Decodable::decode(d))?,
last_modified: {
let json_str: String =
d.read_struct_field("last_modified", 0, |d| Decodable::decode(d))?;
match json_str.parse() {
Ok(datetime) => datetime,
Err(err) => return Err(d.error(err.description())),
}
},
})
})
}
}
#[cfg_attr(feature="with-serde", derive(Deserialize, Serialize))]
#[cfg_attr(feature="with-rustc-serialize", derive(RustcDecodable, RustcEncodable))]
#[derive(Debug)]
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: (),
}
}
}
}
}
#[cfg_attr(feature="with-serde", derive(Deserialize))]
#[cfg_attr(feature="with-rustc-serialize", derive(RustcDecodable))]
#[derive(Debug)]
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 req = dir.client.get(url);
let mut res = req.send()
.chain_err(|| ErrorKind::Http(format!("listing directory '{}'", dir.to_data_uri())))?;
if res.status().is_success() {
if let Some(data_type) = res.headers().get::<XDataType>() {
if "directory" != data_type.as_str() {
return Err(ErrorKind::UnexpectedDataType("directory", data_type.to_string())
.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() => {
json::decode_str(&res_json).chain_err(|| ErrorKind::DecodeJson("directory listing"))
}
StatusCode::NotFound => Err(ErrorKind::NotFound(dir.to_url().unwrap()).into()),
status => {
let api_error = ApiError {
message: status.to_string(),
stacktrace: None,
};
Err(Error::from(ErrorKind::Api(api_error))).chain_err(|| error::decode(&res_json))
}
}
}
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 raw_input = json::encode(&input_data).chain_err(|| ErrorKind::EncodeJson("directory creation parameters"))?;
let req = self.client
.post(parent_url)
.header(ContentType(mime!(Application / Json)))
.body(raw_input);
let mut res = req.send()
.chain_err(|| ErrorKind::Http(format!("creating directory '{}'", self.to_data_uri())))?;
if res.status().is_success() {
Ok(())
} else {
let mut res_json = String::new();
res.read_to_string(&mut res_json)
.chain_err(|| {
ErrorKind::Io(format!("creating directory '{}'", self.to_data_uri()))
})?;
let api_error = ApiError {
message: res.status().to_string(),
stacktrace: None,
};
Err(Error::from(ErrorKind::Api(api_error))).chain_err(|| error::decode(&res_json))
}
}
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 req = self.client.delete(url);
let mut res = req.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() => {
json::decode_str::<DeletedResponse>(&res_json)
.map(|res| res.result)
.chain_err(|| ErrorKind::DecodeJson("directory deletion response"))
}
StatusCode::NotFound => Err(ErrorKind::NotFound(self.to_url().unwrap()).into()),
status => {
let api_error = ApiError {
message: status.to_string(),
stacktrace: None,
};
Err(Error::from(ErrorKind::Api(api_error))).chain_err(|| error::decode(&res_json))
}
}
}
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 data::HasDataPath;
use super::*;
use Algorithmia;
fn mock_client() -> Algorithmia {
Algorithmia::client("")
}
#[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()]);
}
}