use aws_sdk_s3::{
operation::{
get_object::GetObjectError, list_objects_v2::ListObjectsV2Error, put_object::PutObjectError,
},
primitives::ByteStream,
Client,
};
use aws_smithy_runtime_api::client::orchestrator::HttpResponse;
use std::{
io,
path::{Path, PathBuf},
};
use tokio::{
fs::File,
io::{AsyncSeekExt, AsyncWriteExt},
};
use crate::error::S3FilesystemError;
pub const DEFAULT_DATA_STORE: &'static str = "target/temp";
#[derive(Debug, Clone)]
pub struct OpenOptions {
s3_client: Client,
bucket: String,
mount_path: PathBuf,
force_download: bool,
}
impl OpenOptions {
pub async fn new(bucket: String, client: Option<Client>) -> Self {
let s3_client = match client {
Some(x) => x,
None => {
let config = aws_config::load_from_env().await;
aws_sdk_s3::Client::new(&config)
}
};
OpenOptions {
s3_client,
bucket: bucket,
mount_path: DEFAULT_DATA_STORE.into(),
force_download: false,
}
}
pub fn mount_path<P>(mut self, folder_path: P) -> Self
where
P: Into<PathBuf>,
{
self.mount_path = folder_path.into();
self
}
pub fn force_download(mut self, download: bool) -> Self {
self.force_download = download;
self
}
}
impl OpenOptions {
pub async fn open_s3<P>(
&self,
path: P,
) -> Result<File, S3FilesystemError<GetObjectError, HttpResponse>>
where
P: AsRef<Path>,
{
let full_data_path = self.mount_path.join(&self.bucket).join(&path);
let s3_data_path = match path.as_ref().to_str() {
Some(path) => path.replace("\\", "/"),
None => {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid File Path").into())
}
};
let exists = std::fs::metadata(&full_data_path).is_ok();
if exists && !self.force_download {
return Ok(tokio::fs::OpenOptions::new()
.read(true)
.open(&full_data_path)
.await?);
}
match full_data_path.parent() {
Some(parent_path) => std::fs::create_dir_all(parent_path)?,
None => (),
}
let mut file = tokio::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&full_data_path)
.await?;
let get_object_builder = self.s3_client.get_object().bucket(&self.bucket);
let mut object = match get_object_builder.key(s3_data_path).send().await {
Ok(x) => x,
Err(e) => {
tokio::fs::remove_file(&full_data_path).await?;
return Err(e.into());
}
};
while let Some(bytes) = object.body.try_next().await? {
file.write(&bytes).await?;
}
file.seek(io::SeekFrom::Start(0)).await?;
return Ok(file);
}
pub async fn write_s3<P>(
&self,
path: P,
buf: &[u8],
) -> Result<File, S3FilesystemError<PutObjectError, HttpResponse>>
where
P: AsRef<Path>,
{
let full_data_path = self.mount_path.join(&self.bucket).join(&path);
match full_data_path.parent() {
Some(parent_path) => std::fs::create_dir_all(parent_path)?,
None => (),
}
let s3_data_path = match path.as_ref().to_str() {
Some(path) => path.replace("\\", "/"),
None => {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid File Path").into())
}
};
let mut file = tokio::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&full_data_path)
.await?;
file.write_all(buf).await?;
let byte_stream = ByteStream::from_path(&full_data_path).await?;
let put_object_builder = self.s3_client.put_object().bucket(&self.bucket);
return match put_object_builder
.key(s3_data_path)
.body(byte_stream)
.send()
.await
{
Ok(_) => Ok(file),
Err(e) => {
tokio::fs::remove_file(&full_data_path).await?;
return Err(e.into());
}
};
}
pub async fn walkdir<P>(
&self,
path: P,
) -> Result<Vec<DirEntry>, S3FilesystemError<ListObjectsV2Error, HttpResponse>>
where
P: AsRef<Path>,
{
let mut obj_req = self.s3_client.list_objects_v2().bucket(&self.bucket);
match path.as_ref().to_str() {
Some(path) => obj_req = obj_req.prefix(path),
None => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid filepath for S3. Please ensure it's UTF-8 only.",
)
.into())
}
}
let objects_res = match obj_req.send().await {
Ok(x) => x,
Err(e) => return Err(e.into()),
};
let mut data_to_return = Vec::new();
for s3_object in objects_res.contents() {
let filepath = match s3_object.key() {
Some(x) => x.to_string(),
None => continue,
};
data_to_return.push(DirEntry {
path: PathBuf::from(&filepath),
size: s3_object.size(),
folder: filepath.ends_with("/"),
});
}
return Ok(data_to_return);
}
}
#[derive(Debug, Clone)]
pub struct DirEntry {
pub path: PathBuf,
pub size: i64,
pub folder: bool,
}