use super::ObjectMetadata;
use chrono::prelude::*;
use libunftp::storage::{Error, ErrorKind, Fileinfo};
use serde::{de, Deserialize};
use std::fmt::Display;
use std::str::FromStr;
use std::time::SystemTime;
use std::{iter::Extend, path::PathBuf};
#[derive(Deserialize, Debug)]
pub(crate) struct ResponseBody {
items: Option<Vec<Item>>,
prefixes: Option<Vec<String>>,
next_page_token: Option<String>,
}
#[derive(Deserialize, Debug)]
pub(crate) struct Item {
name: String,
updated: DateTime<Utc>,
#[serde(default, deserialize_with = "item_size_deserializer")]
size: u64,
#[serde(default, rename = "md5Hash")]
md5_hash: String,
}
fn item_size_deserializer<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: FromStr,
T::Err: Display,
D: de::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(de::Error::custom)
}
impl ResponseBody {
pub(crate) fn list(self) -> Result<Vec<Fileinfo<PathBuf, ObjectMetadata>>, Error> {
let items: Vec<Fileinfo<PathBuf, ObjectMetadata>> = match &self.items {
Some(items) => match &self.prefixes {
Some(p) => items
.iter()
.filter(|item: &&Item| !item.name.ends_with('/') || p.contains(&item.name))
.map(|item: &Item| item.to_file_info().unwrap())
.collect(),
None => items
.iter()
.filter(|item: &&Item| !item.name.ends_with('/'))
.map(|item: &Item| item.to_file_info().unwrap())
.collect(),
},
None => vec![],
};
let prefixes_without_object = self.prefixes.map_or(vec![], |prefixes: Vec<String>| {
prefixes
.iter()
.filter(|prefix| self.items.as_ref().map_or(true, |it: &Vec<Item>| !it.iter().any(|i| i.name == **prefix)))
.map(|prefix| Fileinfo {
path: prefix.into(),
metadata: ObjectMetadata {
last_updated: SystemTime::now(),
is_file: false,
size: 0,
},
})
.collect()
});
let result: &mut Vec<Fileinfo<PathBuf, ObjectMetadata>> = &mut vec![];
result.extend(prefixes_without_object);
result.extend(items);
Ok(result.to_vec())
}
pub(crate) fn dir_exists(&self) -> bool {
self.items.is_some() || self.prefixes.is_some()
}
pub(crate) fn dir_empty(&self) -> bool {
match (self.next_page_token.as_ref(), self.prefixes.as_ref(), self.items.as_ref()) {
(Some(_), _, _) => false,
(_, Some(_), _) => false,
(_, _, Some(items)) => items.len() == 1 && items[0].name.ends_with('/'),
(_, _, _) => false,
}
}
}
impl Item {
pub(crate) fn to_metadata(&self) -> Result<ObjectMetadata, Error> {
Ok(ObjectMetadata {
size: self.size,
last_updated: self.updated.into(),
is_file: !self.name.ends_with('/'),
})
}
pub(crate) fn to_file_info(&self) -> Result<Fileinfo<PathBuf, ObjectMetadata>, Error> {
let path: PathBuf = PathBuf::from(self.name.clone());
let metadata: ObjectMetadata = self.to_metadata()?;
Ok(Fileinfo { path, metadata })
}
pub(crate) fn to_md5(&self) -> Result<String, Error> {
let md5 = base64::decode(&self.md5_hash).map_err(|e| Error::new(ErrorKind::LocalError, e))?;
Ok(md5.iter().map(|b| format!("{:02x}", b)).collect())
}
}
#[cfg(test)]
mod test {
use super::*;
use libunftp::storage::Metadata;
use std::time::SystemTime;
#[test]
fn to_metadata() {
let sys_time = SystemTime::now();
let date_time = DateTime::from(sys_time);
let item: Item = Item {
name: "".into(),
updated: date_time,
size: 50,
md5_hash: "".into(),
};
let metadata: ObjectMetadata = item.to_metadata().unwrap();
assert_eq!(metadata.size, 50);
assert_eq!(metadata.modified().unwrap(), sys_time);
assert_eq!(metadata.is_file, true);
}
#[test]
fn to_metadata_parse_error() {
let response: serde_json::error::Result<Item> = serde_json::from_str(r#"{"name":"", "updated":"2020-09-01T12:13:14Z", "size":8}"#);
assert_eq!(response.err().unwrap().is_data(), true);
}
}