Skip to main content

rustack_s3_core/ops/
bucket.rs

1//! Bucket CRUD operation handlers.
2//!
3//! Implements `create_bucket`, `delete_bucket`, `head_bucket`, `list_buckets`,
4//! and `get_bucket_location`.
5
6use rustack_s3_model::{
7    error::S3Error,
8    input::{
9        CreateBucketInput, DeleteBucketInput, GetBucketLocationInput, HeadBucketInput,
10        ListBucketsInput,
11    },
12    output::{CreateBucketOutput, GetBucketLocationOutput, HeadBucketOutput, ListBucketsOutput},
13    types::{Bucket, BucketLocationConstraint, LocationType, Owner},
14};
15use tracing::debug;
16
17use crate::{
18    error::S3ServiceError, provider::RustackS3, state::object::Owner as InternalOwner,
19    validation::validate_bucket_name,
20};
21
22/// Convert our internal [`InternalOwner`] to the model [`Owner`] type.
23pub(crate) fn to_model_owner(owner: &InternalOwner) -> Owner {
24    Owner {
25        display_name: Some(owner.display_name.clone()),
26        id: Some(owner.id.clone()),
27    }
28}
29
30// These handler methods must remain async because some operations involve
31// storage I/O. Methods that are fully synchronous are allowed to be async
32// for consistency.
33#[allow(clippy::unused_async)]
34impl RustackS3 {
35    /// Create a new S3 bucket.
36    pub async fn handle_create_bucket(
37        &self,
38        input: CreateBucketInput,
39    ) -> Result<CreateBucketOutput, S3Error> {
40        let bucket_name = input.bucket;
41
42        validate_bucket_name(&bucket_name).map_err(S3ServiceError::into_s3_error)?;
43
44        let region = input
45            .create_bucket_configuration
46            .and_then(|c| c.location_constraint)
47            .map_or_else(
48                || self.config.default_region.clone(),
49                |lc: BucketLocationConstraint| lc.as_str().to_owned(),
50            );
51
52        let owner = InternalOwner::default();
53
54        // Check if object lock is requested.
55        let object_lock_enabled = input.object_lock_enabled_for_bucket.unwrap_or(false);
56
57        self.state
58            .create_bucket(bucket_name.clone(), region, owner)
59            .map_err(S3ServiceError::into_s3_error)?;
60
61        // If object lock was requested, enable it on the bucket.
62        if object_lock_enabled {
63            if let Ok(bucket) = self.state.get_bucket(&bucket_name) {
64                *bucket.object_lock_enabled.write() = true;
65                // Object lock requires versioning.
66                bucket.enable_versioning();
67            }
68        }
69
70        debug!(bucket = %bucket_name, "create_bucket completed");
71
72        Ok(CreateBucketOutput {
73            bucket_arn: None,
74            location: Some(format!("/{bucket_name}")),
75        })
76    }
77
78    /// Delete an S3 bucket.
79    pub async fn handle_delete_bucket(&self, input: DeleteBucketInput) -> Result<(), S3Error> {
80        let bucket_name = input.bucket;
81
82        // Clean up CORS rules for this bucket.
83        self.cors_index.delete_rules(&bucket_name);
84
85        // Delete storage data for this bucket.
86        self.storage.delete_bucket_data(&bucket_name);
87
88        // Delete the bucket from state.
89        self.state
90            .delete_bucket(&bucket_name)
91            .map_err(S3ServiceError::into_s3_error)?;
92
93        debug!(bucket = %bucket_name, "delete_bucket completed");
94
95        Ok(())
96    }
97
98    /// Check if a bucket exists and is accessible (HEAD Bucket).
99    pub async fn handle_head_bucket(
100        &self,
101        input: HeadBucketInput,
102    ) -> Result<HeadBucketOutput, S3Error> {
103        let bucket_name = input.bucket;
104
105        let bucket = self
106            .state
107            .get_bucket(&bucket_name)
108            .map_err(S3ServiceError::into_s3_error)?;
109
110        Ok(HeadBucketOutput {
111            access_point_alias: None,
112            bucket_arn: None,
113            bucket_location_name: Some(bucket.region.clone()),
114            bucket_location_type: Some(LocationType::from("Region")),
115            bucket_region: Some(bucket.region.clone()),
116        })
117    }
118
119    /// List all buckets.
120    pub async fn handle_list_buckets(
121        &self,
122        _input: ListBucketsInput,
123    ) -> Result<ListBucketsOutput, S3Error> {
124        let bucket_list = self.state.list_buckets();
125
126        let buckets: Vec<Bucket> = bucket_list
127            .into_iter()
128            .map(|(name, creation_date)| Bucket {
129                bucket_arn: None,
130                name: Some(name),
131                creation_date: Some(creation_date),
132                bucket_region: None,
133            })
134            .collect();
135
136        let owner = to_model_owner(&InternalOwner::default());
137
138        Ok(ListBucketsOutput {
139            buckets,
140            continuation_token: None,
141            owner: Some(owner),
142            prefix: None,
143        })
144    }
145
146    /// Get the location (region) of a bucket.
147    pub async fn handle_get_bucket_location(
148        &self,
149        input: GetBucketLocationInput,
150    ) -> Result<GetBucketLocationOutput, S3Error> {
151        let bucket_name = input.bucket;
152
153        let bucket = self
154            .state
155            .get_bucket(&bucket_name)
156            .map_err(S3ServiceError::into_s3_error)?;
157
158        let location_constraint = if bucket.region == "us-east-1" {
159            // AWS returns null/empty for us-east-1.
160            None
161        } else {
162            Some(BucketLocationConstraint::from(bucket.region.as_str()))
163        };
164
165        Ok(GetBucketLocationOutput {
166            location_constraint,
167        })
168    }
169}