use crate::{
endpoint::S3Endpoint,
error::Error,
list::{TreeItem, TreeItemKind, TreeItems, TreeList, strip_relative_path},
};
use async_trait::async_trait;
use aws_sdk_s3::types::{CommonPrefix, Object};
use std::{convert::TryFrom, time::SystemTime};
const PATH_SEPARATOR: char = '/';
#[async_trait]
impl TreeList for S3Endpoint {
async fn list(&self, root_path: &str, prefix: Option<&str>) -> Result<TreeItems, Error> {
let root_path = Self::get_root_path(root_path, PATH_SEPARATOR);
let prefix = Self::get_request_prefix("", prefix);
let delimiter = Self::get_delimiter(PATH_SEPARATOR);
let bucket = self.bucket().to_string();
let client = self.connection();
let response = client
.list_objects_v2()
.bucket(bucket.clone())
.delimiter(delimiter)
.prefix(prefix)
.send()
.await;
let list_result = response.map(|resp| {
let mut items =
Self::parse_response_common_prefixes(resp.common_prefixes, self.bucket(), &root_path);
items.append(&mut Self::parse_response_objects(
resp.contents,
self.bucket(),
&root_path,
));
items
});
let items = list_result.unwrap_or_else(|error| {
log::warn!("Cannot access {} content: {:?}", self.bucket(), error);
TreeItems::default()
});
Ok(items)
}
}
impl S3Endpoint {
fn get_request_prefix(root_path: &str, prefix: Option<&str>) -> String {
let absolute_prefix = Self::get_absolute_prefix(root_path, prefix, PATH_SEPARATOR);
format!(
"{}{}",
absolute_prefix
.trim_start_matches(PATH_SEPARATOR)
.trim_end_matches(PATH_SEPARATOR),
PATH_SEPARATOR
)
}
fn parse_response_common_prefixes(
common_prefixes: Option<Vec<CommonPrefix>>,
bucket: &str,
prefix: &str,
) -> TreeItems {
common_prefixes
.map(|common_prefixes| {
common_prefixes
.iter()
.filter_map(move |common_prefix| {
let item = TreeItem::try_from((prefix, common_prefix));
if let Err(error) = &item {
log::warn!("Cannot access entry in {bucket}: {error:?}");
}
item.ok()
})
.collect()
})
.unwrap_or_default()
}
fn parse_response_objects(objects: Option<Vec<Object>>, bucket: &str, prefix: &str) -> TreeItems {
objects
.map(|objects| {
Box::new(objects.iter().filter_map(move |object| {
let item = TreeItem::try_from((prefix, object));
if let Err(error) = &item {
log::warn!("Cannot access entry in {bucket}: {error:?}");
}
item.ok()
}))
.collect()
})
.unwrap_or_default()
}
}
impl TryFrom<(&str, &CommonPrefix)> for TreeItem {
type Error = Error;
fn try_from((prefix, common_prefix): (&str, &CommonPrefix)) -> Result<Self, Self::Error> {
let path = common_prefix
.prefix
.as_ref()
.ok_or_else(|| Error::Other(format!("No prefix in common prefix: {common_prefix:?}")))?;
let path = strip_relative_path(prefix, path, PATH_SEPARATOR);
let prefix = if prefix == PATH_SEPARATOR.to_string() {
prefix
} else {
prefix.trim_end_matches(PATH_SEPARATOR)
};
Ok(Self::new(
TreeItemKind::Folder,
&path,
prefix,
0,
SystemTime::UNIX_EPOCH,
None,
PATH_SEPARATOR,
))
}
}
impl TryFrom<(&str, &Object)> for TreeItem {
type Error = Error;
fn try_from((prefix, object): (&str, &Object)) -> Result<Self, Self::Error> {
let path = object.key.as_ref().ok_or_else(|| {
Error::Other(format!(
"Cannot retrieve 'key' value from S3 object: {object:?}"
))
})?;
let path = strip_relative_path(prefix, path, PATH_SEPARATOR);
let prefix = if prefix == PATH_SEPARATOR.to_string() {
prefix
} else {
prefix.trim_end_matches(PATH_SEPARATOR)
};
let size = object
.size
.as_ref()
.ok_or_else(|| {
Error::Other(format!(
"Cannot retrieve 'size' value from S3 object: {object:?}"
))
})
.map(|size| *size as u64)?;
let last_modified = object.last_modified.as_ref().ok_or_else(|| {
Error::Other(format!(
"Cannot retrieve 'last_modified' value from S3 object: {object:?}"
))
})?;
let last_update = SystemTime::try_from(*last_modified)?;
Ok(Self::new(
TreeItemKind::File,
&path,
prefix,
size,
last_update,
None,
PATH_SEPARATOR,
))
}
}
#[test]
pub fn test_get_root_path() {
assert_eq!("/", &S3Endpoint::get_root_path("", PATH_SEPARATOR));
assert_eq!("/", &S3Endpoint::get_root_path("/", PATH_SEPARATOR));
assert_eq!("/root", &S3Endpoint::get_root_path("root", PATH_SEPARATOR));
assert_eq!("/root", &S3Endpoint::get_root_path("root/", PATH_SEPARATOR));
assert_eq!("/root", &S3Endpoint::get_root_path("/root", PATH_SEPARATOR));
assert_eq!(
"/root",
&S3Endpoint::get_root_path("/root/", PATH_SEPARATOR)
);
assert_eq!(
"/root/path",
&S3Endpoint::get_root_path("root/path", PATH_SEPARATOR)
);
assert_eq!(
"/root/path",
&S3Endpoint::get_root_path("root/path/", PATH_SEPARATOR)
);
assert_eq!(
"/root/path",
&S3Endpoint::get_root_path("/root/path", PATH_SEPARATOR)
);
assert_eq!(
"/root/path",
&S3Endpoint::get_root_path("/root/path/", PATH_SEPARATOR)
);
}
#[test]
pub fn test_list_request_prefix() {
assert_eq!("/", &S3Endpoint::get_request_prefix("", None));
assert_eq!("/", &S3Endpoint::get_request_prefix("/", None));
assert_eq!("root/", &S3Endpoint::get_request_prefix("/root", None));
assert_eq!(
"root/path/",
&S3Endpoint::get_request_prefix("/root/path", None)
);
assert_eq!(
"prefix/",
&S3Endpoint::get_request_prefix("", Some("prefix"))
);
assert_eq!(
"prefix/",
&S3Endpoint::get_request_prefix("/", Some("prefix"))
);
assert_eq!(
"root/prefix/",
&S3Endpoint::get_request_prefix("/root", Some("prefix"))
);
assert_eq!(
"root/path/prefix/",
&S3Endpoint::get_request_prefix("/root/path", Some("prefix"))
);
}