Skip to content

Commit

Permalink
Merge branch 'aalekhpatel07-list-buckets'
Browse files Browse the repository at this point in the history
  • Loading branch information
durch committed Oct 15, 2023
2 parents 41395fc + e046de0 commit c235062
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 21 deletions.
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,13 @@ All runtimes support either `native-tls` or `rustls-tls`, there are features for

#### Buckets

| | |
|----------|------------------------------------------------------------------------------------|
| `create` | [async](https://docs.rs/rust-s3/latest/s3/bucket/struct.Bucket.html#method.create) |
| `delete` | [async](https://docs.rs/rust-s3/latest/s3/bucket/struct.Bucket.html#method.delete) |
| | |
|----------|-----------------------------------------------------------------------------------------|
| `create` | [async](https://docs.rs/rust-s3/latest/s3/bucket/struct.Bucket.html#method.create) |
| `delete` | [async](https://docs.rs/rust-s3/latest/s3/bucket/struct.Bucket.html#method.delete) |
| `list` | [async](https://docs.rs/rust-s3/latest/s3/bucket/struct.Bucket.html#method.list_buckets)|
| `exists` | [async](https://docs.rs/rust-s3/latest/s3/bucket/struct.Bucket.html#method.exists)|


#### Presign

Expand Down
34 changes: 23 additions & 11 deletions examples/minio-tokio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,32 @@
use awscreds::Credentials;
use awsregion::Region;
use s3::error::S3Error;
use s3::Bucket;
use s3::{Bucket, BucketConfiguration};

#[tokio::main]
async fn main() -> Result<(), S3Error> {
// This requires a running minio server at localhost:9000
let bucket = Bucket::new(
"test-rust-s3",
Region::Custom {
region: "eu-central-1".to_owned(),
endpoint: "http://localhost:9000".to_owned(),
},
Credentials::default()?,
)?
.with_path_style();

let bucket_name = "test-rust-s3";
let region = Region::Custom {
region: "eu-central-1".to_owned(),
endpoint: "http://localhost:9000".to_owned(),
};
let credentials = Credentials::default()?;

let mut bucket =
Bucket::new(bucket_name, region.clone(), credentials.clone())?.with_path_style();

if !bucket.exists().await? {
bucket = Bucket::create_with_path_style(
bucket_name,
region,
credentials,
BucketConfiguration::default(),
)
.await?
.bucket;
}

let s3_path = "test.file";
let test = b"I'm going to S3!";
Expand All @@ -29,7 +41,7 @@ async fn main() -> Result<(), S3Error> {
assert_eq!(test, response_data.as_slice());

let response_data = bucket
.get_object_range(s3_path, 100, Some(1000))
.get_object_range(s3_path, 0, Some(1000))
.await
.unwrap();
assert_eq!(response_data.status_code(), 206);
Expand Down
98 changes: 96 additions & 2 deletions s3/src/bucket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,100 @@ impl Bucket {
})
}

/// Get a list of all existing buckets in the region
/// that are accessible by the given credentials.
/// ```no_run
/// use s3::{Bucket, BucketConfiguration};
/// use s3::creds::Credentials;
/// use s3::region::Region;
/// use anyhow::Result;
///
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let region = Region::Custom {
/// region: "eu-central-1".to_owned(),
/// endpoint: "http://localhost:9000".to_owned()
/// };
/// let credentials = Credentials::default()?;
///
/// // Async variant with `tokio` or `async-std` features
/// let response = Bucket::list_buckets(region, credentials).await?;
///
/// // `sync` feature will produce an identical method
/// #[cfg(feature = "sync")]
/// let response = Bucket::list_buckets(region, credentials)?;
///
/// // Blocking variant, generated with `blocking` feature in combination
/// // with `tokio` or `async-std` features.
/// #[cfg(feature = "blocking")]
/// let response = Bucket::list_buckets_blocking(region, credentials)?;
///
/// let found_buckets = response.bucket_names().collect::<Vec<String>>();
/// println!("found buckets: {:#?}", found_buckets);
/// # Ok(())
/// # }
/// ```
#[maybe_async::maybe_async]
pub async fn list_buckets(
region: Region,
credentials: Credentials,
) -> Result<crate::bucket_ops::ListBucketsResponse, S3Error> {
let dummy_bucket = Bucket::new("", region, credentials)?.with_path_style();
let request = RequestImpl::new(&dummy_bucket, "", Command::ListBuckets)?;
let response = request.response_data(false).await?;

Ok(quick_xml::de::from_str::<
crate::bucket_ops::ListBucketsResponse,
>(response.as_str()?)?)
}

/// Determine whether the instantiated bucket exists.
/// ```no_run
/// use s3::{Bucket, BucketConfiguration};
/// use s3::creds::Credentials;
/// use s3::region::Region;
/// use anyhow::Result;
///
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let bucket_name = "some-bucket-that-is-known-to-exist";
/// let region = "us-east-1".parse()?;
/// let credentials = Credentials::default()?;
///
/// let bucket = Bucket::new(bucket_name, region, credentials)?;
///
/// // Async variant with `tokio` or `async-std` features
/// let exists = bucket.exists().await?;
///
/// // `sync` feature will produce an identical method
/// #[cfg(feature = "sync")]
/// let exists = bucket.exists()?;
///
/// // Blocking variant, generated with `blocking` feature in combination
/// // with `tokio` or `async-std` features.
/// #[cfg(feature = "blocking")]
/// let exists = bucket.exists_blocking()?;
///
/// assert_eq!(exists, true);
/// # Ok(())
/// # }
/// ```
#[maybe_async::maybe_async]
pub async fn exists(&self) -> Result<bool, S3Error> {
let credentials = self
.credentials
.read()
.expect("Read lock to be acquired on Credentials")
.clone();

let response = Self::list_buckets(self.region.clone(), credentials).await?;

Ok(response
.bucket_names()
.collect::<std::collections::HashSet<String>>()
.contains(&self.name))
}

/// Create a new `Bucket` with path style and instantiate it
///
/// ```no_run
Expand Down Expand Up @@ -2549,7 +2643,7 @@ mod test {
init();
let remote_path = "+stream_test_big";
let local_path = "+stream_test_big";
std::fs::remove_file(remote_path).unwrap_or_else(|_| {});
std::fs::remove_file(remote_path).unwrap_or(());
let content: Vec<u8> = object(20_000_000);

let mut file = File::create(local_path).await.unwrap();
Expand Down Expand Up @@ -2585,7 +2679,7 @@ mod test {

let response_data = bucket.delete_object(remote_path).await.unwrap();
assert_eq!(response_data.status_code(), 204);
std::fs::remove_file(local_path).unwrap_or_else(|_| {});
std::fs::remove_file(local_path).unwrap_or(());
}

#[ignore]
Expand Down
111 changes: 111 additions & 0 deletions s3/src/bucket_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,114 @@ impl CreateBucketResponse {
self.response_code == 200
}
}

pub use list_buckets::*;

mod list_buckets {

#[derive(Clone, Default, Deserialize, Debug)]
#[serde(rename_all = "PascalCase", rename = "ListAllMyBucketsResult")]
pub struct ListBucketsResponse {
pub owner: BucketOwner,
pub buckets: BucketContainer,
}

impl ListBucketsResponse {
pub fn bucket_names(&self) -> impl Iterator<Item = String> + '_ {
self.buckets.bucket.iter().map(|bucket| bucket.name.clone())
}
}

#[derive(Deserialize, Default, Clone, Debug, PartialEq, Eq)]
pub struct BucketOwner {
#[serde(rename = "ID")]
pub id: String,
#[serde(rename = "DisplayName")]
pub display_name: String,
}

#[derive(Deserialize, Default, Clone, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct BucketInfo {
pub name: String,
pub creation_date: String,
}

#[derive(Deserialize, Default, Clone, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct BucketContainer {
#[serde(default)]
pub bucket: Vec<BucketInfo>,
}

#[cfg(test)]
mod tests {
#[test]
pub fn parse_list_buckets_response() {
let response = r#"
<?xml version="1.0" encoding="UTF-8"?>
<ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Owner>
<ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID>
<DisplayName>minio</DisplayName>
</Owner>
<Buckets>
<Bucket>
<Name>test-rust-s3</Name>
<CreationDate>2023-06-04T20:13:37.837Z</CreationDate>
</Bucket>
<Bucket>
<Name>test-rust-s3-2</Name>
<CreationDate>2023-06-04T20:17:47.152Z</CreationDate>
</Bucket>
</Buckets>
</ListAllMyBucketsResult>
"#;

let parsed = quick_xml::de::from_str::<super::ListBucketsResponse>(response).unwrap();

assert_eq!(parsed.owner.display_name, "minio");
assert_eq!(
parsed.owner.id,
"02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
);
assert_eq!(parsed.buckets.bucket.len(), 2);

assert_eq!(parsed.buckets.bucket.first().unwrap().name, "test-rust-s3");
assert_eq!(
parsed.buckets.bucket.first().unwrap().creation_date,
"2023-06-04T20:13:37.837Z"
);

assert_eq!(parsed.buckets.bucket.last().unwrap().name, "test-rust-s3-2");
assert_eq!(
parsed.buckets.bucket.last().unwrap().creation_date,
"2023-06-04T20:17:47.152Z"
);
}

#[test]
pub fn parse_list_buckets_response_when_no_buckets_exist() {
let response = r#"
<?xml version="1.0" encoding="UTF-8"?>
<ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Owner>
<ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID>
<DisplayName>minio</DisplayName>
</Owner>
<Buckets>
</Buckets>
</ListAllMyBucketsResult>
"#;

let parsed = quick_xml::de::from_str::<super::ListBucketsResponse>(response).unwrap();

assert_eq!(parsed.owner.display_name, "minio");
assert_eq!(
parsed.owner.id,
"02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
);
assert_eq!(parsed.buckets.bucket.len(), 0);
}
}
}
2 changes: 2 additions & 0 deletions s3/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ pub enum Command<'a> {
config: BucketConfiguration,
},
DeleteBucket,
ListBuckets,
PutBucketCors {
configuration: CorsConfiguration,
},
Expand All @@ -135,6 +136,7 @@ impl<'a> Command<'a> {
Command::GetObject
| Command::GetObjectTorrent
| Command::GetObjectRange { .. }
| Command::ListBuckets
| Command::ListObjects { .. }
| Command::ListObjectsV2 { .. }
| Command::GetBucketLocation
Expand Down
4 changes: 4 additions & 0 deletions s3/src/request/request_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,10 @@ pub trait Request {
fn url(&self) -> Result<Url, S3Error> {
let mut url_str = self.bucket().url();

if let Command::ListBuckets { .. } = self.command() {
return Ok(Url::parse(&url_str)?);
}

if let Command::CreateBucket { .. } = self.command() {
return Ok(Url::parse(&url_str)?);
}
Expand Down
8 changes: 4 additions & 4 deletions s3/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,31 +175,31 @@ mod test {
#[test]
fn test_etag_large_file() {
let path = "test_etag";
std::fs::remove_file(path).unwrap_or_else(|_| {});
std::fs::remove_file(path).unwrap_or(());
let test: Vec<u8> = object(10_000_000);

let mut file = File::create(path).unwrap();
file.write_all(&test).unwrap();

let etag = etag_for_path(path).unwrap();

std::fs::remove_file(path).unwrap_or_else(|_| {});
std::fs::remove_file(path).unwrap_or(());

assert_eq!(etag, "e438487f09f09c042b2de097765e5ac2-2");
}

#[test]
fn test_etag_small_file() {
let path = "test_etag";
std::fs::remove_file(path).unwrap_or_else(|_| {});
std::fs::remove_file(path).unwrap_or(());
let test: Vec<u8> = object(1000);

let mut file = File::create(path).unwrap();
file.write_all(&test).unwrap();

let etag = etag_for_path(path).unwrap();

std::fs::remove_file(path).unwrap_or_else(|_| {});
std::fs::remove_file(path).unwrap_or(());

assert_eq!(etag, "8122ef1c2b2331f7986349560248cf56");
}
Expand Down

0 comments on commit c235062

Please sign in to comment.