s3/bucket/
list.rs

1use crate::bucket::Bucket;
2use crate::command::Command;
3use crate::error::S3Error;
4use crate::request::Request;
5use crate::request::RequestImpl;
6use crate::serde_types::{ListBucketResult, ListMultipartUploadsResult};
7use awscreds::Credentials;
8use awsregion::Region;
9use serde::Deserialize;
10
11impl Bucket {
12    /// Get a list of all existing buckets in the region
13    /// that are accessible by the given credentials.
14    /// ```no_run
15    /// use s3::{Bucket, BucketConfiguration};
16    /// use s3::creds::Credentials;
17    /// use s3::region::Region;
18    /// use anyhow::Result;
19    ///
20    /// # #[tokio::main]
21    /// # async fn main() -> Result<()> {
22    /// let region = Region::Custom {
23    ///   region: "eu-central-1".to_owned(),
24    ///   endpoint: "http://localhost:9000".to_owned()
25    /// };
26    /// let credentials = Credentials::default()?;
27    ///
28    /// let response = Bucket::list_buckets(region, credentials).await?;
29    ///
30    /// let found_buckets = response.bucket_names().collect::<Vec<String>>();
31    /// println!("found buckets: {:#?}", found_buckets);
32    /// # Ok(())
33    /// # }
34    /// ```
35    pub async fn list_buckets(
36        region: Region,
37        credentials: Credentials,
38    ) -> Result<crate::bucket::ListBucketsResponse, S3Error> {
39        let dummy_bucket = Bucket::new("", region, credentials)?.with_path_style();
40        let request = RequestImpl::new(&dummy_bucket, "", Command::ListBuckets)?;
41        let response = request.response_data(false).await?;
42
43        Ok(quick_xml::de::from_str::<crate::bucket::ListBucketsResponse>(response.as_str()?)?)
44    }
45
46    /// Determine whether the instantiated bucket exists.
47    /// ```no_run
48    /// use s3::{Bucket, BucketConfiguration};
49    /// use s3::creds::Credentials;
50    /// use s3::region::Region;
51    /// use anyhow::Result;
52    ///
53    /// # #[tokio::main]
54    /// # async fn main() -> Result<()> {
55    /// let bucket_name = "some-bucket-that-is-known-to-exist";
56    /// let region = "us-east-1".parse()?;
57    /// let credentials = Credentials::default()?;
58    ///
59    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
60    ///
61    /// let exists = bucket.exists().await?;
62    ///
63    /// assert_eq!(exists, true);
64    /// # Ok(())
65    /// # }
66    /// ```
67    pub async fn exists(&self) -> Result<bool, S3Error> {
68        let credentials = self
69            .credentials
70            .read()
71            .expect("Read lock to be acquired on Credentials")
72            .clone();
73
74        let response = Self::list_buckets(self.region.clone(), credentials).await?;
75
76        Ok(response
77            .bucket_names()
78            .collect::<std::collections::HashSet<String>>()
79            .contains(&self.name))
80    }
81
82    pub async fn list_page(
83        &self,
84        prefix: String,
85        delimiter: Option<String>,
86        continuation_token: Option<String>,
87        start_after: Option<String>,
88        max_keys: Option<usize>,
89    ) -> Result<(ListBucketResult, u16), S3Error> {
90        let command = if self.listobjects_v2 {
91            Command::ListObjectsV2 {
92                prefix,
93                delimiter,
94                continuation_token,
95                start_after,
96                max_keys,
97            }
98        } else {
99            // In the v1 ListObjects request, there is only one "marker"
100            // field that serves as both the initial starting position,
101            // and as the continuation token.
102            Command::ListObjects {
103                prefix,
104                delimiter,
105                marker: std::cmp::max(continuation_token, start_after),
106                max_keys,
107            }
108        };
109        let request = RequestImpl::new(self, "/", command)?;
110        let response_data = request.response_data(false).await?;
111        let list_bucket_result = quick_xml::de::from_reader(response_data.as_slice())?;
112
113        Ok((list_bucket_result, response_data.status_code()))
114    }
115
116    /// List the contents of an S3 bucket.
117    ///
118    /// # Example:
119    ///
120    /// ```no_run
121    /// use s3::bucket::Bucket;
122    /// use s3::creds::Credentials;
123    /// use anyhow::Result;
124    ///
125    /// # #[tokio::main]
126    /// # async fn main() -> Result<()> {
127    ///
128    /// let bucket_name = "rust-s3-test";
129    /// let region = "us-east-1".parse()?;
130    /// let credentials = Credentials::default()?;
131    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
132    ///
133    /// let results = bucket.list("/".to_string(), Some("/".to_string())).await?;
134    /// #
135    /// # Ok(())
136    /// # }
137    /// ```
138    pub async fn list(
139        &self,
140        prefix: String,
141        delimiter: Option<String>,
142    ) -> Result<Vec<ListBucketResult>, S3Error> {
143        let the_bucket = self.to_owned();
144        let mut results = Vec::new();
145        let mut continuation_token = None;
146
147        loop {
148            let (list_bucket_result, _) = the_bucket
149                .list_page(
150                    prefix.clone(),
151                    delimiter.clone(),
152                    continuation_token,
153                    None,
154                    None,
155                )
156                .await?;
157            continuation_token = list_bucket_result.next_continuation_token.clone();
158            results.push(list_bucket_result);
159            if continuation_token.is_none() {
160                break;
161            }
162        }
163
164        Ok(results)
165    }
166
167    pub async fn list_multiparts_uploads_page(
168        &self,
169        prefix: Option<&str>,
170        delimiter: Option<&str>,
171        key_marker: Option<String>,
172        max_uploads: Option<usize>,
173    ) -> Result<(ListMultipartUploadsResult, u16), S3Error> {
174        let command = Command::ListMultipartUploads {
175            prefix,
176            delimiter,
177            key_marker,
178            max_uploads,
179        };
180        let request = RequestImpl::new(self, "/", command)?;
181        let response_data = request.response_data(false).await?;
182        let list_bucket_result = quick_xml::de::from_reader(response_data.as_slice())?;
183
184        Ok((list_bucket_result, response_data.status_code()))
185    }
186
187    /// List the ongoing multipart uploads of an S3 bucket. This may be useful to cleanup failed
188    /// uploads, together with [`crate::bucket::Bucket::abort_upload`].
189    ///
190    /// # Example:
191    ///
192    /// ```no_run
193    /// use s3::bucket::Bucket;
194    /// use s3::creds::Credentials;
195    /// use anyhow::Result;
196    ///
197    /// # #[tokio::main]
198    /// # async fn main() -> Result<()> {
199    ///
200    /// let bucket_name = "rust-s3-test";
201    /// let region = "us-east-1".parse()?;
202    /// let credentials = Credentials::default()?;
203    /// let bucket = Bucket::new(bucket_name, region, credentials)?;
204    ///
205    /// let results = bucket.list_multiparts_uploads(Some("/"), Some("/")).await?;
206    /// #
207    /// # Ok(())
208    /// # }
209    /// ```
210    pub async fn list_multiparts_uploads(
211        &self,
212        prefix: Option<&str>,
213        delimiter: Option<&str>,
214    ) -> Result<Vec<ListMultipartUploadsResult>, S3Error> {
215        let the_bucket = self.to_owned();
216        let mut results = Vec::new();
217        let mut next_marker: Option<String> = None;
218
219        loop {
220            let (list_multiparts_uploads_result, _) = the_bucket
221                .list_multiparts_uploads_page(prefix, delimiter, next_marker, None)
222                .await?;
223
224            let is_truncated = list_multiparts_uploads_result.is_truncated;
225            next_marker = list_multiparts_uploads_result.next_marker.clone();
226            results.push(list_multiparts_uploads_result);
227
228            if !is_truncated {
229                break;
230            }
231        }
232
233        Ok(results)
234    }
235}
236
237#[derive(Clone, Default, Deserialize, Debug)]
238#[serde(rename_all = "PascalCase", rename = "ListAllMyBucketsResult")]
239pub struct ListBucketsResponse {
240    pub owner: BucketOwner,
241    pub buckets: BucketContainer,
242}
243
244impl ListBucketsResponse {
245    pub fn bucket_names(&self) -> impl Iterator<Item = String> + '_ {
246        self.buckets.bucket.iter().map(|bucket| bucket.name.clone())
247    }
248}
249
250#[derive(Deserialize, Default, Clone, Debug, PartialEq, Eq)]
251pub struct BucketOwner {
252    #[serde(rename = "ID")]
253    pub id: String,
254    #[serde(rename = "DisplayName")]
255    pub display_name: String,
256}
257
258#[derive(Deserialize, Default, Clone, Debug)]
259#[serde(rename_all = "PascalCase")]
260pub struct BucketInfo {
261    pub name: String,
262    pub creation_date: crate::serde_types::DateTime,
263}
264
265#[derive(Deserialize, Default, Clone, Debug)]
266#[serde(rename_all = "PascalCase")]
267pub struct BucketContainer {
268    #[serde(default)]
269    pub bucket: Vec<BucketInfo>,
270}
271
272#[cfg(test)]
273mod tests {
274    #[test]
275    pub fn parse_list_buckets_response() {
276        let response = r#"
277        <?xml version="1.0" encoding="UTF-8"?>
278            <ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
279                <Owner>
280                    <ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID>
281                    <DisplayName>minio</DisplayName>
282                </Owner>
283                <Buckets>
284                    <Bucket>
285                        <Name>test-rust-s3</Name>
286                        <CreationDate>2023-06-04T20:13:37.837Z</CreationDate>
287                    </Bucket>
288                    <Bucket>
289                        <Name>test-rust-s3-2</Name>
290                        <CreationDate>2023-06-04T20:17:47.152Z</CreationDate>
291                    </Bucket>
292                </Buckets>
293            </ListAllMyBucketsResult>
294        "#;
295
296        let parsed = quick_xml::de::from_str::<super::ListBucketsResponse>(response).unwrap();
297
298        assert_eq!(parsed.owner.display_name, "minio");
299        assert_eq!(
300            parsed.owner.id,
301            "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
302        );
303        assert_eq!(parsed.buckets.bucket.len(), 2);
304
305        assert_eq!(parsed.buckets.bucket.first().unwrap().name, "test-rust-s3");
306        assert_eq!(
307            parsed.buckets.bucket.first().unwrap().creation_date,
308            "2023-06-04T20:13:37.837Z"
309                .parse::<crate::serde_types::DateTime>()
310                .unwrap()
311        );
312
313        assert_eq!(parsed.buckets.bucket.last().unwrap().name, "test-rust-s3-2");
314        assert_eq!(
315            parsed.buckets.bucket.last().unwrap().creation_date,
316            "2023-06-04T20:17:47.152Z"
317                .parse::<crate::serde_types::DateTime>()
318                .unwrap()
319        );
320    }
321
322    #[test]
323    pub fn parse_list_buckets_response_when_no_buckets_exist() {
324        let response = r#"
325        <?xml version="1.0" encoding="UTF-8"?>
326            <ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
327                <Owner>
328                    <ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID>
329                    <DisplayName>minio</DisplayName>
330                </Owner>
331                <Buckets>
332                </Buckets>
333            </ListAllMyBucketsResult>
334        "#;
335
336        let parsed = quick_xml::de::from_str::<super::ListBucketsResponse>(response).unwrap();
337
338        assert_eq!(parsed.owner.display_name, "minio");
339        assert_eq!(
340            parsed.owner.id,
341            "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
342        );
343        assert_eq!(parsed.buckets.bucket.len(), 0);
344    }
345}