Skip to main content

rustack_s3_core/state/
service.rs

1//! Top-level S3 service state.
2//!
3//! [`S3ServiceState`] manages the collection of buckets and enforces global
4//! bucket-name uniqueness. All operations are thread-safe via `DashMap`.
5
6use chrono::{DateTime, Utc};
7use dashmap::{
8    DashMap,
9    mapref::one::{Ref, RefMut},
10};
11use tracing::{debug, info};
12
13use super::{bucket::S3Bucket, object::Owner};
14use crate::error::S3ServiceError;
15
16/// Top-level S3 service state holding all buckets.
17///
18/// Bucket names are globally unique across accounts, enforced by
19/// `global_bucket_owner`. Per-account bucket data is stored in `buckets`.
20///
21/// All fields are accessed concurrently via `DashMap`; no external locking is
22/// required.
23pub struct S3ServiceState {
24    /// Bucket name to `S3Bucket` mapping.
25    buckets: DashMap<String, S3Bucket>,
26    /// Bucket name to account-ID mapping (enforces global uniqueness).
27    global_bucket_owner: DashMap<String, String>,
28}
29
30impl std::fmt::Debug for S3ServiceState {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        f.debug_struct("S3ServiceState")
33            .field("bucket_count", &self.buckets.len())
34            .finish_non_exhaustive()
35    }
36}
37
38impl Default for S3ServiceState {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl S3ServiceState {
45    /// Create a new, empty service state.
46    #[must_use]
47    pub fn new() -> Self {
48        Self {
49            buckets: DashMap::new(),
50            global_bucket_owner: DashMap::new(),
51        }
52    }
53
54    /// Create a new bucket.
55    ///
56    /// # Errors
57    ///
58    /// - [`S3ServiceError::BucketAlreadyOwnedByYou`] if the caller already owns a bucket with the
59    ///   same name.
60    /// - [`S3ServiceError::BucketAlreadyExists`] if the bucket name is taken by a different
61    ///   account.
62    pub fn create_bucket(
63        &self,
64        name: String,
65        region: String,
66        owner: Owner,
67    ) -> Result<(), S3ServiceError> {
68        let account_id = owner.id.clone();
69
70        // Check global uniqueness.
71        if let Some(existing_owner) = self.global_bucket_owner.get(&name) {
72            if *existing_owner == account_id {
73                return Err(S3ServiceError::BucketAlreadyOwnedByYou { bucket: name });
74            }
75            return Err(S3ServiceError::BucketAlreadyExists { bucket: name });
76        }
77
78        // Insert into both maps.
79        let bucket = S3Bucket::new(name.clone(), region, owner);
80        self.buckets.insert(name.clone(), bucket);
81        self.global_bucket_owner.insert(name.clone(), account_id);
82
83        info!(bucket = %name, "bucket created");
84        Ok(())
85    }
86
87    /// Delete a bucket.
88    ///
89    /// # Errors
90    ///
91    /// - [`S3ServiceError::NoSuchBucket`] if the bucket does not exist.
92    /// - [`S3ServiceError::BucketNotEmpty`] if the bucket still contains objects or in-progress
93    ///   multipart uploads.
94    pub fn delete_bucket(&self, name: &str) -> Result<(), S3ServiceError> {
95        let bucket_ref = self
96            .buckets
97            .get(name)
98            .ok_or_else(|| S3ServiceError::NoSuchBucket {
99                bucket: name.to_owned(),
100            })?;
101
102        if !bucket_ref.is_empty() {
103            return Err(S3ServiceError::BucketNotEmpty {
104                bucket: name.to_owned(),
105            });
106        }
107
108        // Drop the read reference before removing.
109        drop(bucket_ref);
110
111        self.buckets.remove(name);
112        self.global_bucket_owner.remove(name);
113
114        info!(bucket = %name, "bucket deleted");
115        Ok(())
116    }
117
118    /// Get an immutable reference to a bucket.
119    ///
120    /// # Errors
121    ///
122    /// Returns [`S3ServiceError::NoSuchBucket`] if the bucket does not exist.
123    pub fn get_bucket(&self, name: &str) -> Result<Ref<'_, String, S3Bucket>, S3ServiceError> {
124        self.buckets
125            .get(name)
126            .ok_or_else(|| S3ServiceError::NoSuchBucket {
127                bucket: name.to_owned(),
128            })
129    }
130
131    /// Get a mutable reference to a bucket.
132    ///
133    /// # Errors
134    ///
135    /// Returns [`S3ServiceError::NoSuchBucket`] if the bucket does not exist.
136    pub fn get_bucket_mut(
137        &self,
138        name: &str,
139    ) -> Result<RefMut<'_, String, S3Bucket>, S3ServiceError> {
140        self.buckets
141            .get_mut(name)
142            .ok_or_else(|| S3ServiceError::NoSuchBucket {
143                bucket: name.to_owned(),
144            })
145    }
146
147    /// List all buckets, returning `(name, creation_date)` pairs sorted by name.
148    #[must_use]
149    pub fn list_buckets(&self) -> Vec<(String, DateTime<Utc>)> {
150        let mut buckets: Vec<(String, DateTime<Utc>)> = self
151            .buckets
152            .iter()
153            .map(|entry| (entry.key().clone(), entry.value().creation_date))
154            .collect();
155        buckets.sort_by(|a, b| a.0.cmp(&b.0));
156        buckets
157    }
158
159    /// Check whether a bucket exists.
160    #[must_use]
161    pub fn bucket_exists(&self, name: &str) -> bool {
162        self.buckets.contains_key(name)
163    }
164
165    /// Reset all state, removing all buckets.
166    pub fn reset(&self) {
167        debug!("resetting all S3 service state");
168        self.buckets.clear();
169        self.global_bucket_owner.clear();
170    }
171}
172
173// ---------------------------------------------------------------------------
174// Tests
175// ---------------------------------------------------------------------------
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    fn default_owner() -> Owner {
182        Owner::default()
183    }
184
185    fn other_owner() -> Owner {
186        Owner {
187            id: "other-account-id".to_owned(),
188            display_name: "other-user".to_owned(),
189        }
190    }
191
192    #[test]
193    fn test_should_create_empty_service_state() {
194        let state = S3ServiceState::new();
195        assert!(!state.bucket_exists("anything"));
196        assert!(state.list_buckets().is_empty());
197    }
198
199    #[test]
200    fn test_should_debug_format_service_state() {
201        let state = S3ServiceState::new();
202        let debug_str = format!("{state:?}");
203        assert!(debug_str.contains("S3ServiceState"));
204    }
205
206    #[test]
207    fn test_should_create_and_list_bucket() {
208        let state = S3ServiceState::new();
209        state
210            .create_bucket(
211                "my-bucket".to_owned(),
212                "us-east-1".to_owned(),
213                default_owner(),
214            )
215            .unwrap_or_else(|e| panic!("create_bucket failed: {e}"));
216
217        assert!(state.bucket_exists("my-bucket"));
218
219        let buckets = state.list_buckets();
220        assert_eq!(buckets.len(), 1);
221        assert_eq!(buckets[0].0, "my-bucket");
222    }
223
224    #[test]
225    fn test_should_reject_duplicate_bucket_same_owner() {
226        let state = S3ServiceState::new();
227        state
228            .create_bucket("dup".to_owned(), "us-east-1".to_owned(), default_owner())
229            .unwrap_or_else(|e| panic!("first create failed: {e}"));
230
231        let result = state.create_bucket("dup".to_owned(), "us-east-1".to_owned(), default_owner());
232        assert!(
233            matches!(result, Err(S3ServiceError::BucketAlreadyOwnedByYou { .. })),
234            "expected BucketAlreadyOwnedByYou, got {result:?}"
235        );
236    }
237
238    #[test]
239    fn test_should_reject_duplicate_bucket_different_owner() {
240        let state = S3ServiceState::new();
241        state
242            .create_bucket("shared".to_owned(), "us-east-1".to_owned(), default_owner())
243            .unwrap_or_else(|e| panic!("first create failed: {e}"));
244
245        let result =
246            state.create_bucket("shared".to_owned(), "eu-west-1".to_owned(), other_owner());
247        assert!(
248            matches!(result, Err(S3ServiceError::BucketAlreadyExists { .. })),
249            "expected BucketAlreadyExists, got {result:?}"
250        );
251    }
252
253    #[test]
254    fn test_should_delete_empty_bucket() {
255        let state = S3ServiceState::new();
256        state
257            .create_bucket(
258                "deleteme".to_owned(),
259                "us-east-1".to_owned(),
260                default_owner(),
261            )
262            .unwrap_or_else(|e| panic!("create failed: {e}"));
263
264        state
265            .delete_bucket("deleteme")
266            .unwrap_or_else(|e| panic!("delete failed: {e}"));
267
268        assert!(!state.bucket_exists("deleteme"));
269        assert!(state.list_buckets().is_empty());
270    }
271
272    #[test]
273    fn test_should_reject_delete_nonexistent_bucket() {
274        let state = S3ServiceState::new();
275        let result = state.delete_bucket("ghost");
276        assert!(matches!(result, Err(S3ServiceError::NoSuchBucket { .. })));
277    }
278
279    #[test]
280    fn test_should_reject_delete_non_empty_bucket() {
281        use crate::state::object::{ObjectMetadata, S3Object};
282
283        let state = S3ServiceState::new();
284        state
285            .create_bucket("full".to_owned(), "us-east-1".to_owned(), default_owner())
286            .unwrap_or_else(|e| panic!("create failed: {e}"));
287
288        // Insert an object via the bucket's object store.
289        {
290            let bucket = state
291                .get_bucket("full")
292                .unwrap_or_else(|e| panic!("get failed: {e}"));
293            let obj = S3Object {
294                key: "file.txt".to_owned(),
295                version_id: "null".to_owned(),
296                etag: "\"abc\"".to_owned(),
297                size: 42,
298                last_modified: chrono::Utc::now(),
299                storage_class: "STANDARD".to_owned(),
300                metadata: ObjectMetadata::default(),
301                owner: default_owner(),
302                checksum: None,
303                parts_count: None,
304                part_etags: Vec::new(),
305            };
306            bucket.objects.write().put(obj);
307        }
308
309        let result = state.delete_bucket("full");
310        assert!(
311            matches!(result, Err(S3ServiceError::BucketNotEmpty { .. })),
312            "expected BucketNotEmpty, got {result:?}"
313        );
314    }
315
316    #[test]
317    fn test_should_get_bucket_immutable_ref() {
318        let state = S3ServiceState::new();
319        state
320            .create_bucket(
321                "ref-test".to_owned(),
322                "us-east-1".to_owned(),
323                default_owner(),
324            )
325            .unwrap_or_else(|e| panic!("create failed: {e}"));
326
327        let bucket = state
328            .get_bucket("ref-test")
329            .unwrap_or_else(|e| panic!("get failed: {e}"));
330        assert_eq!(bucket.name, "ref-test");
331        assert_eq!(bucket.region, "us-east-1");
332    }
333
334    #[test]
335    fn test_should_get_bucket_mutable_ref() {
336        let state = S3ServiceState::new();
337        state
338            .create_bucket(
339                "mut-test".to_owned(),
340                "us-east-1".to_owned(),
341                default_owner(),
342            )
343            .unwrap_or_else(|e| panic!("create failed: {e}"));
344
345        let bucket = state
346            .get_bucket_mut("mut-test")
347            .unwrap_or_else(|e| panic!("get_mut failed: {e}"));
348        assert_eq!(bucket.name, "mut-test");
349    }
350
351    #[test]
352    fn test_should_return_error_for_nonexistent_bucket() {
353        let state = S3ServiceState::new();
354        assert!(matches!(
355            state.get_bucket("nope"),
356            Err(S3ServiceError::NoSuchBucket { .. })
357        ));
358        assert!(matches!(
359            state.get_bucket_mut("nope"),
360            Err(S3ServiceError::NoSuchBucket { .. })
361        ));
362    }
363
364    #[test]
365    fn test_should_list_buckets_sorted() {
366        let state = S3ServiceState::new();
367        for name in ["charlie", "alpha", "bravo"] {
368            state
369                .create_bucket(name.to_owned(), "us-east-1".to_owned(), default_owner())
370                .unwrap_or_else(|e| panic!("create {name} failed: {e}"));
371        }
372
373        let names: Vec<String> = state.list_buckets().into_iter().map(|(n, _)| n).collect();
374        assert_eq!(names, vec!["alpha", "bravo", "charlie"]);
375    }
376
377    #[test]
378    fn test_should_reset_all_state() {
379        let state = S3ServiceState::new();
380        state
381            .create_bucket("a".to_owned(), "us-east-1".to_owned(), default_owner())
382            .unwrap_or_else(|e| panic!("create failed: {e}"));
383        state
384            .create_bucket("b".to_owned(), "us-east-1".to_owned(), default_owner())
385            .unwrap_or_else(|e| panic!("create failed: {e}"));
386
387        assert_eq!(state.list_buckets().len(), 2);
388        state.reset();
389        assert!(state.list_buckets().is_empty());
390        assert!(!state.bucket_exists("a"));
391        assert!(!state.bucket_exists("b"));
392    }
393
394    #[test]
395    fn test_should_recreate_bucket_after_delete() {
396        let state = S3ServiceState::new();
397        state
398            .create_bucket("reuse".to_owned(), "us-east-1".to_owned(), default_owner())
399            .unwrap_or_else(|e| panic!("create failed: {e}"));
400        state
401            .delete_bucket("reuse")
402            .unwrap_or_else(|e| panic!("delete failed: {e}"));
403
404        // Should be able to recreate.
405        state
406            .create_bucket("reuse".to_owned(), "eu-west-1".to_owned(), default_owner())
407            .unwrap_or_else(|e| panic!("recreate failed: {e}"));
408
409        let bucket = state
410            .get_bucket("reuse")
411            .unwrap_or_else(|e| panic!("get failed: {e}"));
412        assert_eq!(bucket.region, "eu-west-1");
413    }
414
415    #[test]
416    fn test_should_use_default_trait() {
417        let state = S3ServiceState::default();
418        assert!(state.list_buckets().is_empty());
419    }
420}