use async_trait::async_trait;
use futures::future;
use futures::stream::StreamExt;
use rusoto_s3::{
CopyObjectRequest, Delete, DeleteObjectsRequest, GetObjectRequest, GetObjectTaggingRequest,
Object, ObjectIdentifier, PutObjectAclRequest, PutObjectTaggingRequest, S3Client, Tagging, S3,
};
use std::process::Command;
use std::process::ExitStatus;
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use anyhow::Error;
use dyn_clone::DynClone;
use indicatif::{ProgressBar, ProgressStyle};
use crate::arg::*;
use crate::error::*;
use crate::utils::combine_keys;
impl Cmd {
pub fn downcast(self) -> Box<dyn RunCommand> {
match self {
Cmd::Print(l) => Box::new(l),
Cmd::Ls(l) => Box::new(l),
Cmd::Exec(l) => Box::new(l),
Cmd::Delete(l) => Box::new(l),
Cmd::Download(l) => Box::new(l),
Cmd::Tags(l) => Box::new(l),
Cmd::LsTags(l) => Box::new(l),
Cmd::Public(l) => Box::new(l),
Cmd::Copy(l) => Box::new(l),
Cmd::Move(l) => Box::new(l),
Cmd::Nothing(l) => Box::new(l),
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct ExecStatus {
pub status: ExitStatus,
pub runcommand: String,
}
#[async_trait]
pub trait RunCommand: DynClone {
async fn execute(
&self,
client: &S3Client,
region: &str,
path: &S3Path,
list: &[Object],
) -> Result<(), Error>;
}
dyn_clone::clone_trait_object!(RunCommand);
impl FastPrint {
#[inline]
fn print_object<I: Write>(
&self,
io: &mut I,
bucket: &str,
object: &Object,
) -> std::io::Result<()> {
writeln!(
io,
"s3://{}/{}",
bucket,
object.key.as_ref().unwrap_or(&"".to_string())
)
}
}
#[async_trait]
impl RunCommand for FastPrint {
async fn execute(
&self,
_c: &S3Client,
_r: &str,
path: &S3Path,
list: &[Object],
) -> Result<(), Error> {
let mut stdout = std::io::stdout();
for x in list {
self.print_object(&mut stdout, &path.bucket, x)?
}
Ok(())
}
}
impl AdvancedPrint {
#[inline]
fn print_object<I: Write>(
&self,
io: &mut I,
bucket: &str,
object: &Object,
) -> std::io::Result<()> {
writeln!(
io,
"{0} {1:?} {2} {3} s3://{4}/{5} {6}",
object.e_tag.as_ref().unwrap_or(&"NoEtag".to_string()),
object.owner.as_ref().map(|x| x.display_name.as_ref()),
object.size.as_ref().unwrap_or(&0),
object
.last_modified
.as_ref()
.unwrap_or(&"NoTime".to_string()),
bucket,
object.key.as_ref().unwrap_or(&"".to_string()),
object
.storage_class
.as_ref()
.unwrap_or(&"NoStorage".to_string()),
)
}
}
#[async_trait]
impl RunCommand for AdvancedPrint {
async fn execute(
&self,
_c: &S3Client,
_r: &str,
path: &S3Path,
list: &[Object],
) -> Result<(), Error> {
let mut stdout = std::io::stdout();
for x in list {
self.print_object(&mut stdout, &path.bucket, x)?
}
Ok(())
}
}
impl Exec {
#[inline]
fn exec<I: Write>(&self, io: &mut I, key: &str) -> Result<ExecStatus, Error> {
let command_str = self.utility.replace("{}", key);
let mut command_args = command_str.split(' ');
let command_name = command_args.next().ok_or(FunctionError::CommandlineParse)?;
let mut command = Command::new(command_name);
for arg in command_args {
command.arg(arg);
}
let output = command.output()?;
let output_str = String::from_utf8_lossy(&output.stdout);
writeln!(io, "{}", &output_str)?;
Ok(ExecStatus {
status: output.status,
runcommand: command_str.clone(),
})
}
}
#[async_trait]
impl RunCommand for Exec {
async fn execute(
&self,
_: &S3Client,
_r: &str,
path: &S3Path,
list: &[Object],
) -> Result<(), Error> {
let mut stdout = std::io::stdout();
for x in list {
let key = x.key.as_deref().unwrap_or("");
let path = format!("s3://{}/{}", &path.bucket, key);
self.exec(&mut stdout, &path)?;
}
Ok(())
}
}
#[async_trait]
impl RunCommand for MultipleDelete {
async fn execute(
&self,
client: &S3Client,
_r: &str,
path: &S3Path,
list: &[Object],
) -> Result<(), Error> {
let key_list: Vec<_> = list
.iter()
.flat_map(|x| match x.key.as_ref() {
Some(key) => Some(ObjectIdentifier {
key: key.to_string(),
version_id: None,
}),
_ => None,
})
.collect();
let request = DeleteObjectsRequest {
bucket: path.bucket.to_string(),
delete: Delete {
objects: key_list,
quiet: None,
},
..Default::default()
};
let result = client.delete_objects(request).await;
match result {
Ok(r) => {
if let Some(deleted_list) = r.deleted {
for object in deleted_list {
println!(
"deleted: s3://{}/{}",
&path.bucket,
object.key.as_ref().unwrap_or(&"".to_string())
);
}
}
}
Err(e) => eprintln!("{}", e),
}
Ok(())
}
}
#[async_trait]
impl RunCommand for SetTags {
async fn execute(
&self,
client: &S3Client,
_r: &str,
path: &S3Path,
list: &[Object],
) -> Result<(), Error> {
for object in list {
let key = object.key.as_ref().ok_or(FunctionError::ObjectFieldError)?;
let tags = Tagging {
tag_set: self.tags.iter().map(|x| x.clone().into()).collect(),
};
let request = PutObjectTaggingRequest {
bucket: path.bucket.to_owned(),
key: key.to_owned(),
tagging: tags,
..Default::default()
};
client.put_object_tagging(request).await?;
println!("tags are set for: s3://{}/{}", &path.bucket, &key);
}
Ok(())
}
}
#[async_trait]
impl RunCommand for ListTags {
async fn execute(
&self,
client: &S3Client,
_r: &str,
path: &S3Path,
list: &[Object],
) -> Result<(), Error> {
for object in list {
let key = object.key.as_ref().ok_or(FunctionError::ObjectFieldError)?;
let request = GetObjectTaggingRequest {
bucket: path.bucket.to_string(),
key: key.to_owned(),
..Default::default()
};
let tag_output = client.get_object_tagging(request).await?;
let tags: String = tag_output
.tag_set
.into_iter()
.map(|x| format!("{}:{}", x.key, x.value))
.collect::<Vec<String>>()
.join(",");
println!(
"s3://{}/{} {}",
&path.bucket,
object.key.as_ref().unwrap_or(&"".to_string()),
tags,
);
}
Ok(())
}
}
#[inline]
fn generate_s3_url(region: &str, bucket: &str, key: &str) -> String {
match region {
"us-east-1" => format!("https://{}.s3.amazonaws.com/{}", bucket, key),
_ => format!("https://{}.s3-{}.amazonaws.com/{}", bucket, region, key),
}
}
#[async_trait]
impl RunCommand for SetPublic {
async fn execute(
&self,
client: &S3Client,
region: &str,
path: &S3Path,
list: &[Object],
) -> Result<(), Error> {
for object in list {
let key = object.key.as_ref().ok_or(FunctionError::ObjectFieldError)?;
let request = PutObjectAclRequest {
bucket: path.bucket.to_owned(),
key: key.to_owned(),
acl: Some("public-read".to_string()),
..Default::default()
};
client.put_object_acl(request).await?;
let url = generate_s3_url(region, &path.bucket, key);
println!("{} {}", key, url);
}
Ok(())
}
}
#[async_trait]
impl RunCommand for Download {
async fn execute(
&self,
client: &S3Client,
_r: &str,
path: &S3Path,
list: &[Object],
) -> Result<(), Error> {
for object in list {
let key = object.key.as_ref().ok_or(FunctionError::ObjectFieldError)?;
let request = GetObjectRequest {
bucket: path.bucket.to_owned(),
key: key.to_owned(),
..Default::default()
};
let size = (*object
.size
.as_ref()
.ok_or(FunctionError::ObjectFieldError)
.unwrap()) as u64;
let file_path = Path::new(&self.destination).join(key);
let dir_path = file_path.parent().ok_or(FunctionError::ParentPathParse)?;
let mut count: u64 = 0;
let pb = ProgressBar::new(size);
pb.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
.progress_chars("#>-"));
println!(
"downloading: s3://{}/{} => {}",
&path.bucket,
&key,
file_path
.to_str()
.ok_or(FunctionError::FileNameParseError)
.unwrap()
);
if file_path.exists() && !self.force {
return Ok(());
}
let stream = client
.get_object(request)
.await?
.body
.ok_or(FunctionError::S3FetchBodyError)?;
fs::create_dir_all(&dir_path)?;
let mut output = File::create(&file_path)?;
stream
.for_each(|buf| {
let b = buf.unwrap();
output.write_all(&b).unwrap();
count += b.len() as u64;
pb.set_position(count);
future::ready(())
})
.await;
}
Ok(())
}
}
#[async_trait]
impl RunCommand for S3Copy {
async fn execute(
&self,
client: &S3Client,
_r: &str,
path: &S3Path,
list: &[Object],
) -> Result<(), Error> {
for object in list {
let key = object.key.as_ref().ok_or(FunctionError::ObjectFieldError)?;
let target = combine_keys(self.flat, key, &self.destination.prefix);
let source_path = format!("{0}/{1}", &path.bucket, key);
println!(
"copying: s3://{0} => s3://{1}/{2}",
source_path, &self.destination.bucket, target,
);
let request = CopyObjectRequest {
bucket: self.destination.bucket.clone(),
key: target.to_owned(),
copy_source: source_path,
..Default::default()
};
client.copy_object(request).await?;
}
Ok(())
}
}
#[async_trait]
impl RunCommand for S3Move {
async fn execute(
&self,
client: &S3Client,
_r: &str,
path: &S3Path,
list: &[Object],
) -> Result<(), Error> {
for object in list {
let key = object.key.as_ref().ok_or(FunctionError::ObjectFieldError)?;
let target = combine_keys(self.flat, key, &self.destination.prefix);
let source_path = format!("{0}/{1}", &path.bucket, key);
println!(
"moving: s3://{0} => s3://{1}/{2}",
source_path, &self.destination.bucket, target,
);
let request = CopyObjectRequest {
bucket: self.destination.bucket.to_owned(),
key: target.to_owned(),
copy_source: source_path,
..Default::default()
};
client.copy_object(request).await?;
}
let key_list: Vec<_> = list
.iter()
.flat_map(|x| match x.key.as_ref() {
Some(key) => Some(ObjectIdentifier {
key: key.to_string(),
version_id: None,
}),
_ => None,
})
.collect();
let request = DeleteObjectsRequest {
bucket: path.bucket.clone(),
delete: Delete {
objects: key_list,
quiet: None,
},
..Default::default()
};
client.delete_objects(request).await?;
Ok(())
}
}
#[async_trait]
impl RunCommand for DoNothing {
async fn execute(
&self,
_c: &S3Client,
_r: &str,
_p: &S3Path,
_l: &[Object],
) -> Result<(), Error> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use remove_dir_all::remove_dir_all;
use rusoto_core::Region;
use rusoto_mock::*;
use std::fs::File;
use std::io::prelude::*;
use tempfile::Builder;
#[test]
fn test_advanced_print_object() -> Result<(), Error> {
let mut buf = Vec::new();
let cmd = AdvancedPrint {};
let bucket = "test";
let object = Object {
e_tag: Some("9d48114aa7c18f9d68aa20086dbb7756".to_string()),
key: Some("somepath/otherpath".to_string()),
last_modified: Some("2017-07-19T19:04:17.000Z".to_string()),
owner: None,
size: Some(4_997_288),
storage_class: Some("STANDARD".to_string()),
};
cmd.print_object(&mut buf, bucket, &object)?;
let out = std::str::from_utf8(&buf)?;
assert!(out.contains("9d48114aa7c18f9d68aa20086dbb7756"));
assert!(out.contains("None"));
assert!(out.contains("4997288"));
assert!(out.contains("2017-07-19T19:04:17.000Z"));
assert!(out.contains("s3://test/somepath/otherpath"));
assert!(out.contains("STANDARD"));
Ok(())
}
#[test]
fn test_fast_print_object() -> Result<(), Error> {
let mut buf = Vec::new();
let cmd = FastPrint {};
let bucket = "test";
let object = Object {
e_tag: Some("9d48114aa7c18f9d68aa20086dbb7756".to_string()),
key: Some("somepath/otherpath".to_string()),
last_modified: Some("2017-07-19T19:04:17.000Z".to_string()),
owner: None,
size: Some(4_997_288),
storage_class: Some("STANDARD".to_string()),
};
cmd.print_object(&mut buf, bucket, &object)?;
let out = std::str::from_utf8(&buf)?;
assert!(out.contains("s3://test/somepath/otherpath"));
Ok(())
}
#[test]
fn test_exec() -> Result<(), Error> {
let mut buf = Vec::new();
let cmd = Exec {
utility: "echo test {}".to_owned(),
};
let path = "s3://test/somepath/otherpath";
cmd.exec(&mut buf, path)?;
let out = std::str::from_utf8(&buf)?;
assert!(out.contains("test"));
assert!(out.contains("s3://test/somepath/otherpath"));
Ok(())
}
#[tokio::test]
async fn test_advanced_print() -> Result<(), Error> {
let object = Object {
e_tag: Some("9d48114aa7c18f9d68aa20086dbb7756".to_string()),
key: Some("somepath/otherpath".to_string()),
last_modified: Some("2017-07-19T19:04:17.000Z".to_string()),
owner: None,
size: Some(4_997_288),
storage_class: Some("STANDARD".to_string()),
};
let cmd = Cmd::Print(AdvancedPrint {}).downcast();
let region = "us-east-1";
let client = S3Client::new(Region::UsEast1);
let path = S3Path {
bucket: "test".to_owned(),
prefix: None,
};
cmd.execute(&client, region, &path, &[object]).await?;
Ok(())
}
#[tokio::test]
async fn test_fastprint() -> Result<(), Error> {
let object = Object {
e_tag: Some("9d48114aa7c18f9d68aa20086dbb7756".to_string()),
key: Some("somepath/otherpath".to_string()),
last_modified: Some("2017-07-19T19:04:17.000Z".to_string()),
owner: None,
size: Some(4_997_288),
storage_class: Some("STANDARD".to_string()),
};
let cmd = Cmd::Ls(FastPrint {}).downcast();
let region = "us-east-1";
let client = S3Client::new(Region::UsEast1);
let path = S3Path {
bucket: "test".to_owned(),
prefix: None,
};
cmd.execute(&client, region, &path, &[object]).await?;
Ok(())
}
#[tokio::test]
async fn smoke_donothing() -> Result<(), Error> {
let object = Object {
e_tag: Some("9d48114aa7c18f9d68aa20086dbb7756".to_string()),
key: Some("somepath/otherpath".to_string()),
last_modified: Some("2017-07-19T19:04:17.000Z".to_string()),
owner: None,
size: Some(4_997_288),
storage_class: Some("STANDARD".to_string()),
};
let cmd = Cmd::Nothing(DoNothing {}).downcast();
let region = "us-east-1";
let client = S3Client::new(Region::UsEast1);
let path = S3Path {
bucket: "test".to_owned(),
prefix: None,
};
cmd.execute(&client, region, &path, &[object]).await
}
#[tokio::test]
async fn smoke_exec() -> Result<(), Error> {
let object = Object {
e_tag: Some("9d48114aa7c18f9d68aa20086dbb7756".to_string()),
key: Some("somepath/otherpath".to_string()),
last_modified: Some("2017-07-19T19:04:17.000Z".to_string()),
owner: None,
size: Some(4_997_288),
storage_class: Some("STANDARD".to_string()),
};
let cmd = Cmd::Exec(Exec {
utility: "echo {}".to_owned(),
})
.downcast();
let region = "us-east-1";
let client = S3Client::new(Region::UsEast1);
let path = S3Path {
bucket: "test".to_owned(),
prefix: None,
};
cmd.execute(&client, region, &path, &[object]).await
}
#[tokio::test]
async fn smoke_s3_delete() -> Result<(), Error> {
let mock = MockRequestDispatcher::with_status(200).with_body(
r#"
<?xml version="1.0" encoding="UTF8"?>
<DeleteResult xmlns="http://s3.amazonaws.com/doc/20060301/">
<Deleted>
<Key>sample1.txt</Key>
</Deleted>
<Deleted>
<Key>sample2.txt</Key>
</Deleted>
</DeleteResult>"#,
);
let client = S3Client::new_with(mock, MockCredentialsProvider, Region::UsEast1);
let objects = &[
Object {
e_tag: Some("9d48114aa7c18f9d68aa20086dbb7756".to_string()),
key: Some("sample1.txt".to_string()),
last_modified: Some("2017-07-19T19:04:17.000Z".to_string()),
owner: None,
size: Some(4997288),
storage_class: Some("STANDARD".to_string()),
},
Object {
e_tag: Some("9d48114aa7c18f9d68aa20086dbb7756".to_string()),
key: Some("sample2.txt".to_string()),
last_modified: Some("2017-07-19T19:04:17.000Z".to_string()),
owner: None,
size: Some(4997288),
storage_class: Some("STANDARD".to_string()),
},
];
let cmd = Cmd::Delete(MultipleDelete {}).downcast();
let path = "s3://testbucket".parse()?;
let res = cmd.execute(&client, "us-east-1", &path, objects).await;
assert!(res.is_ok());
Ok(())
}
#[tokio::test]
async fn smoke_s3_set_public() -> Result<(), Error> {
let mock = MockRequestDispatcher::with_status(200);
let client = S3Client::new_with(mock, MockCredentialsProvider, Region::UsEast1);
let objects = &[
Object {
e_tag: Some("9d48114aa7c18f9d68aa20086dbb7756".to_string()),
key: Some("sample1.txt".to_string()),
last_modified: Some("2017-07-19T19:04:17.000Z".to_string()),
owner: None,
size: Some(4997288),
storage_class: Some("STANDARD".to_string()),
},
Object {
e_tag: Some("9d48114aa7c18f9d68aa20086dbb7756".to_string()),
key: Some("sample2.txt".to_string()),
last_modified: Some("2017-07-19T19:04:17.000Z".to_string()),
owner: None,
size: Some(4997288),
storage_class: Some("STANDARD".to_string()),
},
];
let cmd = Cmd::Public(SetPublic {}).downcast();
let path = "s3://testbucket".parse()?;
let res = cmd.execute(&client, "us-east-1", &path, objects).await;
assert!(res.is_ok());
Ok(())
}
#[tokio::test]
async fn smoke_s3_download() -> Result<(), Error> {
let test_data = "testdata";
let mock = MockRequestDispatcher::with_status(200).with_body(test_data);
let client = S3Client::new_with(mock, MockCredentialsProvider, Region::UsEast1);
let filename = "sample1.txt";
let objects = &[Object {
e_tag: Some("9d48114aa7c18f9d68aa20086dbb7756".to_string()),
key: Some(filename.to_string()),
last_modified: Some("2017-07-19T19:04:17.000Z".to_string()),
owner: None,
size: Some(4997288),
storage_class: Some("STANDARD".to_string()),
}];
let target = Builder::new()
.prefix("s3_download")
.tempdir()
.unwrap()
.path()
.to_path_buf();
let cmd = Cmd::Download(Download {
destination: target.to_str().unwrap().to_owned(),
force: false,
})
.downcast();
let path = "s3://testbucket".parse()?;
let res = cmd.execute(&client, "us-east-1", &path, objects).await;
assert!(res.is_ok());
let file = target.join(&filename);
let mut f = File::open(file).expect("file not found");
let mut contents = String::new();
f.read_to_string(&mut contents)
.expect("something went wrong reading the file");
assert_eq!(contents, test_data);
remove_dir_all(&target).unwrap();
Ok(())
}
#[tokio::test]
async fn smoke_s3_set_tags() -> Result<(), Error> {
let mock = MockRequestDispatcher::with_status(200);
let client = S3Client::new_with(mock, MockCredentialsProvider, Region::UsEast1);
let objects = &[
Object {
e_tag: Some("9d48114aa7c18f9d68aa20086dbb7756".to_string()),
key: Some("sample1.txt".to_string()),
last_modified: Some("2017-07-19T19:04:17.000Z".to_string()),
owner: None,
size: Some(4997288),
storage_class: Some("STANDARD".to_string()),
},
Object {
e_tag: Some("9d48114aa7c18f9d68aa20086dbb7756".to_string()),
key: Some("sample2.txt".to_string()),
last_modified: Some("2017-07-19T19:04:17.000Z".to_string()),
owner: None,
size: Some(4997288),
storage_class: Some("STANDARD".to_string()),
},
];
let tags = vec![
FindTag {
key: "testkey1".to_owned(),
value: "testvalue1".to_owned(),
},
FindTag {
key: "testkey2".to_owned(),
value: "testvalue2".to_owned(),
},
];
let cmd = Cmd::Tags(SetTags { tags }).downcast();
let path = "s3://testbucket".parse()?;
let res = cmd.execute(&client, "us-east-1", &path, objects).await;
assert!(res.is_ok());
Ok(())
}
#[tokio::test]
async fn smoke_s3_list_tags() -> Result<(), Error> {
let mock = MockRequestDispatcher::with_status(200);
let client = S3Client::new_with(mock, MockCredentialsProvider, Region::UsEast1);
let objects = &[
Object {
e_tag: Some("9d48114aa7c18f9d68aa20086dbb7756".to_string()),
key: Some("sample1.txt".to_string()),
last_modified: Some("2017-07-19T19:04:17.000Z".to_string()),
owner: None,
size: Some(4997288),
storage_class: Some("STANDARD".to_string()),
},
Object {
e_tag: Some("9d48114aa7c18f9d68aa20086dbb7756".to_string()),
key: Some("sample2.txt".to_string()),
last_modified: Some("2017-07-19T19:04:17.000Z".to_string()),
owner: None,
size: Some(4997288),
storage_class: Some("STANDARD".to_string()),
},
];
let cmd = Cmd::LsTags(ListTags {}).downcast();
let path = "s3://testbucket".parse()?;
let res = cmd.execute(&client, "us-east-1", &path, objects).await;
assert!(res.is_ok());
Ok(())
}
#[tokio::test]
async fn smoke_s3_copy() -> Result<(), Error> {
let mock = MockRequestDispatcher::with_status(200);
let client = S3Client::new_with(mock, MockCredentialsProvider, Region::UsEast1);
let object = [Object {
e_tag: Some("9d48114aa7c18f9d68aa20086dbb7756".to_string()),
key: Some("path/key".to_string()),
last_modified: Some("2017-07-19T19:04:17.000Z".to_string()),
owner: None,
size: Some(4997288),
storage_class: Some("STANDARD".to_string()),
}];
let cmd = Cmd::Copy(S3Copy {
destination: ("s3://test/1").parse()?,
flat: false,
})
.downcast();
let copy_path = "s3://test/1".parse()?;
let res = cmd.execute(&client, "us-east-1", ©_path, &object).await;
assert!(res.is_ok());
let cmd_flat = S3Copy {
destination: ("s3://test/1").parse()?,
flat: true,
};
let res = cmd_flat
.execute(&client, "us-east-1", ©_path, &object)
.await;
assert!(res.is_ok());
Ok(())
}
#[tokio::test]
async fn smoke_s3_move() -> Result<(), Error> {
let mock = MockRequestDispatcher::with_status(200);
let client = S3Client::new_with(mock, MockCredentialsProvider, Region::UsEast1);
let object = [Object {
e_tag: Some("9d48114aa7c18f9d68aa20086dbb7756".to_string()),
key: Some("key".to_string()),
last_modified: Some("2017-07-19T19:04:17.000Z".to_string()),
owner: None,
size: Some(4997288),
storage_class: Some("STANDARD".to_string()),
}];
let cmd = Cmd::Move(S3Move {
destination: ("s3://test/1").parse()?,
flat: false,
})
.downcast();
let copy_path = "s3://test/1".parse()?;
let res = cmd.execute(&client, "us-east-1", ©_path, &object).await;
assert!(res.is_ok());
let cmd_flat = S3Move {
destination: ("s3://test/1").parse()?,
flat: true,
};
let res = cmd_flat
.execute(&client, "us-east-1", ©_path, &object)
.await;
assert!(res.is_ok());
Ok(())
}
#[test]
fn test_generate_s3_url() {
assert_eq!(
&generate_s3_url("us-east-1", "test-bucket", "somepath/somekey"),
"https://test-bucket.s3.amazonaws.com/somepath/somekey",
);
assert_eq!(
&generate_s3_url("eu-west-1", "test-bucket", "somepath/somekey"),
"https://test-bucket.s3-eu-west-1.amazonaws.com/somepath/somekey",
);
}
}