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    /// Return all bucket names sorted for deterministic snapshot export.
173    #[must_use]
174    pub(crate) fn snapshot_bucket_names(&self) -> Vec<String> {
175        let mut names: Vec<String> = self
176            .buckets
177            .iter()
178            .map(|entry| entry.key().clone())
179            .collect();
180        names.sort();
181        names
182    }
183
184    /// Insert a bucket while rebuilding the global owner index.
185    pub(crate) fn insert_snapshot_bucket(&self, bucket: S3Bucket) {
186        let name = bucket.name.clone();
187        let owner_id = bucket.owner.id.clone();
188        self.buckets.insert(name.clone(), bucket);
189        self.global_bucket_owner.insert(name, owner_id);
190    }
191}
192
193// ---------------------------------------------------------------------------
194// Tests
195// ---------------------------------------------------------------------------
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    fn default_owner() -> Owner {
202        Owner::default()
203    }
204
205    fn other_owner() -> Owner {
206        Owner {
207            id: "other-account-id".to_owned(),
208            display_name: "other-user".to_owned(),
209        }
210    }
211
212    #[test]
213    fn test_should_create_empty_service_state() {
214        let state = S3ServiceState::new();
215        assert!(!state.bucket_exists("anything"));
216        assert!(state.list_buckets().is_empty());
217    }
218
219    #[test]
220    fn test_should_debug_format_service_state() {
221        let state = S3ServiceState::new();
222        let debug_str = format!("{state:?}");
223        assert!(debug_str.contains("S3ServiceState"));
224    }
225
226    #[test]
227    fn test_should_create_and_list_bucket() {
228        let state = S3ServiceState::new();
229        state
230            .create_bucket(
231                "my-bucket".to_owned(),
232                "us-east-1".to_owned(),
233                default_owner(),
234            )
235            .unwrap_or_else(|e| panic!("create_bucket failed: {e}"));
236
237        assert!(state.bucket_exists("my-bucket"));
238
239        let buckets = state.list_buckets();
240        assert_eq!(buckets.len(), 1);
241        assert_eq!(buckets[0].0, "my-bucket");
242    }
243
244    #[test]
245    fn test_should_reject_duplicate_bucket_same_owner() {
246        let state = S3ServiceState::new();
247        state
248            .create_bucket("dup".to_owned(), "us-east-1".to_owned(), default_owner())
249            .unwrap_or_else(|e| panic!("first create failed: {e}"));
250
251        let result = state.create_bucket("dup".to_owned(), "us-east-1".to_owned(), default_owner());
252        assert!(
253            matches!(result, Err(S3ServiceError::BucketAlreadyOwnedByYou { .. })),
254            "expected BucketAlreadyOwnedByYou, got {result:?}"
255        );
256    }
257
258    #[test]
259    fn test_should_reject_duplicate_bucket_different_owner() {
260        let state = S3ServiceState::new();
261        state
262            .create_bucket("shared".to_owned(), "us-east-1".to_owned(), default_owner())
263            .unwrap_or_else(|e| panic!("first create failed: {e}"));
264
265        let result =
266            state.create_bucket("shared".to_owned(), "eu-west-1".to_owned(), other_owner());
267        assert!(
268            matches!(result, Err(S3ServiceError::BucketAlreadyExists { .. })),
269            "expected BucketAlreadyExists, got {result:?}"
270        );
271    }
272
273    #[test]
274    fn test_should_delete_empty_bucket() {
275        let state = S3ServiceState::new();
276        state
277            .create_bucket(
278                "deleteme".to_owned(),
279                "us-east-1".to_owned(),
280                default_owner(),
281            )
282            .unwrap_or_else(|e| panic!("create failed: {e}"));
283
284        state
285            .delete_bucket("deleteme")
286            .unwrap_or_else(|e| panic!("delete failed: {e}"));
287
288        assert!(!state.bucket_exists("deleteme"));
289        assert!(state.list_buckets().is_empty());
290    }
291
292    #[test]
293    fn test_should_reject_delete_nonexistent_bucket() {
294        let state = S3ServiceState::new();
295        let result = state.delete_bucket("ghost");
296        assert!(matches!(result, Err(S3ServiceError::NoSuchBucket { .. })));
297    }
298
299    #[test]
300    fn test_should_reject_delete_non_empty_bucket() {
301        use crate::state::object::{ObjectMetadata, S3Object};
302
303        let state = S3ServiceState::new();
304        state
305            .create_bucket("full".to_owned(), "us-east-1".to_owned(), default_owner())
306            .unwrap_or_else(|e| panic!("create failed: {e}"));
307
308        // Insert an object via the bucket's object store.
309        {
310            let bucket = state
311                .get_bucket("full")
312                .unwrap_or_else(|e| panic!("get failed: {e}"));
313            let obj = S3Object {
314                key: "file.txt".to_owned(),
315                version_id: "null".to_owned(),
316                etag: "\"abc\"".to_owned(),
317                size: 42,
318                last_modified: chrono::Utc::now(),
319                storage_class: "STANDARD".to_owned(),
320                metadata: ObjectMetadata::default(),
321                owner: default_owner(),
322                checksum: None,
323                parts_count: None,
324                part_etags: Vec::new(),
325            };
326            bucket.objects.write().put(obj);
327        }
328
329        let result = state.delete_bucket("full");
330        assert!(
331            matches!(result, Err(S3ServiceError::BucketNotEmpty { .. })),
332            "expected BucketNotEmpty, got {result:?}"
333        );
334    }
335
336    #[test]
337    fn test_should_get_bucket_immutable_ref() {
338        let state = S3ServiceState::new();
339        state
340            .create_bucket(
341                "ref-test".to_owned(),
342                "us-east-1".to_owned(),
343                default_owner(),
344            )
345            .unwrap_or_else(|e| panic!("create failed: {e}"));
346
347        let bucket = state
348            .get_bucket("ref-test")
349            .unwrap_or_else(|e| panic!("get failed: {e}"));
350        assert_eq!(bucket.name, "ref-test");
351        assert_eq!(bucket.region, "us-east-1");
352    }
353
354    #[test]
355    fn test_should_get_bucket_mutable_ref() {
356        let state = S3ServiceState::new();
357        state
358            .create_bucket(
359                "mut-test".to_owned(),
360                "us-east-1".to_owned(),
361                default_owner(),
362            )
363            .unwrap_or_else(|e| panic!("create failed: {e}"));
364
365        let bucket = state
366            .get_bucket_mut("mut-test")
367            .unwrap_or_else(|e| panic!("get_mut failed: {e}"));
368        assert_eq!(bucket.name, "mut-test");
369    }
370
371    #[test]
372    fn test_should_return_error_for_nonexistent_bucket() {
373        let state = S3ServiceState::new();
374        assert!(matches!(
375            state.get_bucket("nope"),
376            Err(S3ServiceError::NoSuchBucket { .. })
377        ));
378        assert!(matches!(
379            state.get_bucket_mut("nope"),
380            Err(S3ServiceError::NoSuchBucket { .. })
381        ));
382    }
383
384    #[test]
385    fn test_should_list_buckets_sorted() {
386        let state = S3ServiceState::new();
387        for name in ["charlie", "alpha", "bravo"] {
388            state
389                .create_bucket(name.to_owned(), "us-east-1".to_owned(), default_owner())
390                .unwrap_or_else(|e| panic!("create {name} failed: {e}"));
391        }
392
393        let names: Vec<String> = state.list_buckets().into_iter().map(|(n, _)| n).collect();
394        assert_eq!(names, vec!["alpha", "bravo", "charlie"]);
395    }
396
397    #[test]
398    fn test_should_reset_all_state() {
399        let state = S3ServiceState::new();
400        state
401            .create_bucket("a".to_owned(), "us-east-1".to_owned(), default_owner())
402            .unwrap_or_else(|e| panic!("create failed: {e}"));
403        state
404            .create_bucket("b".to_owned(), "us-east-1".to_owned(), default_owner())
405            .unwrap_or_else(|e| panic!("create failed: {e}"));
406
407        assert_eq!(state.list_buckets().len(), 2);
408        state.reset();
409        assert!(state.list_buckets().is_empty());
410        assert!(!state.bucket_exists("a"));
411        assert!(!state.bucket_exists("b"));
412    }
413
414    #[test]
415    fn test_should_recreate_bucket_after_delete() {
416        let state = S3ServiceState::new();
417        state
418            .create_bucket("reuse".to_owned(), "us-east-1".to_owned(), default_owner())
419            .unwrap_or_else(|e| panic!("create failed: {e}"));
420        state
421            .delete_bucket("reuse")
422            .unwrap_or_else(|e| panic!("delete failed: {e}"));
423
424        // Should be able to recreate.
425        state
426            .create_bucket("reuse".to_owned(), "eu-west-1".to_owned(), default_owner())
427            .unwrap_or_else(|e| panic!("recreate failed: {e}"));
428
429        let bucket = state
430            .get_bucket("reuse")
431            .unwrap_or_else(|e| panic!("get failed: {e}"));
432        assert_eq!(bucket.region, "eu-west-1");
433    }
434
435    #[test]
436    fn test_should_use_default_trait() {
437        let state = S3ServiceState::default();
438        assert!(state.list_buckets().is_empty());
439    }
440}