s3/
bucket_ops.rs

1//! This module provides utilities for configuring and managing Amazon S3 buckets, focusing on access control, bucket configuration, and handling responses from the S3 service.
2//! It includes structures for defining bucket access policies (both canned and custom), configuring various bucket properties, and parsing responses from S3 API calls like `ListBuckets`.
3//!
4//! ## Key Components
5//!
6//! - **CannedBucketAcl Enum**
7//!   - Represents standard predefined Amazon S3 access control lists (ACLs) for buckets.
8//!   - Variants include:
9//!     - `Private`: Only the owner has full control.
10//!     - `PublicRead`: Anyone can read objects in the bucket.
11//!     - `PublicReadWrite`: Anyone can read and write objects in the bucket.
12//!     - `AuthenticatedRead`: Only authenticated AWS users can read objects.
13//!     - `Custom(String)`: A custom ACL specified as a string.
14//!   - The `fmt::Display` trait is implemented to easily convert ACL variants into their corresponding string representations for HTTP headers.
15//!
16//! - **BucketAcl Enum**
17//!   - Represents more granular access controls using different types of identifiers:
18//!     - `Id`: A unique user ID.
19//!     - `Uri`: A URI specifying the group granted access.
20//!     - `Email`: An email address specifying the user granted access.
21//!   - The `fmt::Display` implementation allows these ACLs to be formatted as strings suitable for use in HTTP headers.
22//!
23//! - **BucketConfiguration Struct**
24//!   - Encapsulates the configuration for an S3 bucket, including:
25//!     - Optional ACL (`CannedBucketAcl`)
26//!     - Object lock settings
27//!     - Permissions for full control, read, write, and their respective ACLs.
28//!     - The region in which the bucket is located.
29//!   - Provides methods for setting default configurations, setting the region, and adding relevant headers to HTTP requests.
30//!
31//! - **CreateBucketResponse Struct**
32//!   - Represents the response from a bucket creation request, including the bucket reference and HTTP response details.
33//!   - Includes a method `success` to check if the bucket creation was successful (i.e., HTTP status code 200).
34//!
35//! - **ListBucketsResponse Struct**
36//!   - This structure is used to parse and hold the response from the `ListBuckets` API call.
37//!   - Provides methods for retrieving the list of bucket names from the response.
38
39use crate::error::S3Error;
40use crate::{Bucket, Region};
41
42/// [AWS Documentation](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#CannedACL)
43#[allow(dead_code)]
44#[derive(Clone, Debug)]
45pub enum CannedBucketAcl {
46    Private,
47    PublicRead,
48    PublicReadWrite,
49    AuthenticatedRead,
50    Custom(String),
51}
52
53use http::HeaderMap;
54use http::header::HeaderName;
55use std::fmt;
56
57impl fmt::Display for CannedBucketAcl {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match self {
60            CannedBucketAcl::Private => write!(f, "private"),
61            CannedBucketAcl::PublicRead => write!(f, "public-read"),
62            CannedBucketAcl::PublicReadWrite => write!(f, "public-read-write"),
63            CannedBucketAcl::AuthenticatedRead => write!(f, "authenticated-read"),
64            CannedBucketAcl::Custom(policy) => write!(f, "{policy}"),
65        }
66    }
67}
68
69/// [AWS Documentation](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html)
70#[allow(dead_code)]
71#[derive(Clone, Debug)]
72pub enum BucketAcl {
73    Id { id: String },
74    Uri { uri: String },
75    Email { email: String },
76}
77
78impl fmt::Display for BucketAcl {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match self {
81            BucketAcl::Id { id } => write!(f, "id=\"{}\"", id),
82            BucketAcl::Uri { uri } => write!(f, "uri=\"{}\"", uri),
83            BucketAcl::Email { email } => write!(f, "email=\"{}\"", email),
84        }
85    }
86}
87
88#[derive(Clone, Debug)]
89pub struct BucketConfiguration {
90    acl: Option<CannedBucketAcl>,
91    object_lock_enabled: bool,
92    grant_full_control: Option<Vec<BucketAcl>>,
93    grant_read: Option<Vec<BucketAcl>>,
94    grant_read_acp: Option<Vec<BucketAcl>>,
95    grant_write: Option<Vec<BucketAcl>>,
96    grant_write_acp: Option<Vec<BucketAcl>>,
97    location_constraint: Option<Region>,
98}
99
100impl Default for BucketConfiguration {
101    fn default() -> Self {
102        BucketConfiguration::private()
103    }
104}
105
106fn acl_list(acl: &[BucketAcl]) -> String {
107    acl.iter()
108        .map(|x| x.to_string())
109        .collect::<Vec<String>>()
110        .join(",")
111}
112
113impl BucketConfiguration {
114    #[allow(clippy::too_many_arguments)]
115    pub fn new(
116        acl: Option<CannedBucketAcl>,
117        object_lock_enabled: bool,
118        grant_full_control: Option<Vec<BucketAcl>>,
119        grant_read: Option<Vec<BucketAcl>>,
120        grant_read_acp: Option<Vec<BucketAcl>>,
121        grant_write: Option<Vec<BucketAcl>>,
122        grant_write_acp: Option<Vec<BucketAcl>>,
123        location_constraint: Option<Region>,
124    ) -> Self {
125        Self {
126            acl,
127            object_lock_enabled,
128            grant_full_control,
129            grant_read,
130            grant_read_acp,
131            grant_write,
132            grant_write_acp,
133            location_constraint,
134        }
135    }
136
137    pub fn public() -> Self {
138        BucketConfiguration {
139            acl: None,
140            object_lock_enabled: false,
141            grant_full_control: None,
142            grant_read: None,
143            grant_read_acp: None,
144            grant_write: None,
145            grant_write_acp: None,
146            location_constraint: None,
147        }
148    }
149
150    pub fn private() -> Self {
151        BucketConfiguration {
152            acl: Some(CannedBucketAcl::Private),
153            object_lock_enabled: false,
154            grant_full_control: None,
155            grant_read: None,
156            grant_read_acp: None,
157            grant_write: None,
158            grant_write_acp: None,
159            location_constraint: None,
160        }
161    }
162
163    pub fn set_region(&mut self, region: Region) {
164        self.set_location_constraint(region)
165    }
166
167    pub fn set_location_constraint(&mut self, region: Region) {
168        self.location_constraint = Some(region)
169    }
170
171    pub fn location_constraint_payload(&self) -> Option<String> {
172        if let Some(ref location_constraint) = self.location_constraint {
173            if location_constraint == &Region::UsEast1 {
174                return None;
175            }
176            Some(format!(
177                "<CreateBucketConfiguration><LocationConstraint>{}</LocationConstraint></CreateBucketConfiguration>",
178                location_constraint
179            ))
180        } else {
181            None
182        }
183    }
184
185    pub fn add_headers(&self, headers: &mut HeaderMap) -> Result<(), S3Error> {
186        if let Some(ref acl) = self.acl {
187            headers.insert(
188                HeaderName::from_static("x-amz-acl"),
189                acl.to_string().parse()?,
190            );
191        }
192
193        if self.object_lock_enabled {
194            headers.insert(
195                HeaderName::from_static("x-amz-bucket-object-lock-enabled"),
196                "Enabled".to_string().parse()?,
197            );
198        }
199        if let Some(ref value) = self.grant_full_control {
200            headers.insert(
201                HeaderName::from_static("x-amz-grant-full-control"),
202                acl_list(value).parse()?,
203            );
204        }
205        if let Some(ref value) = self.grant_read {
206            headers.insert(
207                HeaderName::from_static("x-amz-grant-read"),
208                acl_list(value).parse()?,
209            );
210        }
211        if let Some(ref value) = self.grant_read_acp {
212            headers.insert(
213                HeaderName::from_static("x-amz-grant-read-acp"),
214                acl_list(value).parse()?,
215            );
216        }
217        if let Some(ref value) = self.grant_write {
218            headers.insert(
219                HeaderName::from_static("x-amz-grant-write"),
220                acl_list(value).parse()?,
221            );
222        }
223        if let Some(ref value) = self.grant_write_acp {
224            headers.insert(
225                HeaderName::from_static("x-amz-grant-write-acp"),
226                acl_list(value).parse()?,
227            );
228        }
229        Ok(())
230    }
231}
232
233#[allow(dead_code)]
234pub struct CreateBucketResponse {
235    pub bucket: Box<Bucket>,
236    pub response_text: String,
237    pub response_code: u16,
238}
239
240impl CreateBucketResponse {
241    pub fn success(&self) -> bool {
242        self.response_code == 200
243    }
244}
245
246pub use list_buckets::*;
247
248mod list_buckets {
249
250    #[derive(Clone, Default, Deserialize, Debug)]
251    #[serde(rename_all = "PascalCase", rename = "ListAllMyBucketsResult")]
252    pub struct ListBucketsResponse {
253        pub owner: BucketOwner,
254        pub buckets: BucketContainer,
255    }
256
257    impl ListBucketsResponse {
258        pub fn bucket_names(&self) -> impl Iterator<Item = String> + '_ {
259            self.buckets.bucket.iter().map(|bucket| bucket.name.clone())
260        }
261    }
262
263    #[derive(Deserialize, Default, Clone, Debug, PartialEq, Eq)]
264    pub struct BucketOwner {
265        #[serde(rename = "ID")]
266        pub id: String,
267        #[serde(rename = "DisplayName")]
268        pub display_name: Option<String>,
269    }
270
271    #[derive(Deserialize, Default, Clone, Debug)]
272    #[serde(rename_all = "PascalCase")]
273    pub struct BucketInfo {
274        pub name: String,
275        pub creation_date: String,
276    }
277
278    #[derive(Deserialize, Default, Clone, Debug)]
279    #[serde(rename_all = "PascalCase")]
280    pub struct BucketContainer {
281        #[serde(default)]
282        pub bucket: Vec<BucketInfo>,
283    }
284
285    #[cfg(test)]
286    mod tests {
287        #[test]
288        pub fn parse_list_buckets_response() {
289            let response = r#"
290            <?xml version="1.0" encoding="UTF-8"?>
291                <ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
292                    <Owner>
293                        <ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID>
294                        <DisplayName>minio</DisplayName>
295                    </Owner>
296                    <Buckets>
297                        <Bucket>
298                            <Name>test-rust-s3</Name>
299                            <CreationDate>2023-06-04T20:13:37.837Z</CreationDate>
300                        </Bucket>
301                        <Bucket>
302                            <Name>test-rust-s3-2</Name>
303                            <CreationDate>2023-06-04T20:17:47.152Z</CreationDate>
304                        </Bucket>
305                    </Buckets>
306                </ListAllMyBucketsResult>
307            "#;
308
309            let parsed = quick_xml::de::from_str::<super::ListBucketsResponse>(response).unwrap();
310
311            assert_eq!(parsed.owner.display_name, Some("minio".to_string()));
312            assert_eq!(
313                parsed.owner.id,
314                "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
315            );
316            assert_eq!(parsed.buckets.bucket.len(), 2);
317
318            assert_eq!(parsed.buckets.bucket.first().unwrap().name, "test-rust-s3");
319            assert_eq!(
320                parsed.buckets.bucket.first().unwrap().creation_date,
321                "2023-06-04T20:13:37.837Z"
322            );
323
324            assert_eq!(parsed.buckets.bucket.last().unwrap().name, "test-rust-s3-2");
325            assert_eq!(
326                parsed.buckets.bucket.last().unwrap().creation_date,
327                "2023-06-04T20:17:47.152Z"
328            );
329        }
330
331        #[test]
332        pub fn parse_list_buckets_response_when_no_buckets_exist() {
333            let response = r#"
334            <?xml version="1.0" encoding="UTF-8"?>
335                <ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
336                    <Owner>
337                        <ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID>
338                        <DisplayName>minio</DisplayName>
339                    </Owner>
340                    <Buckets>
341                    </Buckets>
342                </ListAllMyBucketsResult>
343            "#;
344
345            let parsed = quick_xml::de::from_str::<super::ListBucketsResponse>(response).unwrap();
346
347            assert_eq!(parsed.owner.display_name, Some("minio".to_string()));
348            assert_eq!(
349                parsed.owner.id,
350                "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
351            );
352            assert_eq!(parsed.buckets.bucket.len(), 0);
353        }
354    }
355}