1use 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
16pub struct S3ServiceState {
24 buckets: DashMap<String, S3Bucket>,
26 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 #[must_use]
47 pub fn new() -> Self {
48 Self {
49 buckets: DashMap::new(),
50 global_bucket_owner: DashMap::new(),
51 }
52 }
53
54 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 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 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 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(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 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 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 #[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 #[must_use]
161 pub fn bucket_exists(&self, name: &str) -> bool {
162 self.buckets.contains_key(name)
163 }
164
165 pub fn reset(&self) {
167 debug!("resetting all S3 service state");
168 self.buckets.clear();
169 self.global_bucket_owner.clear();
170 }
171
172 #[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 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#[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 {
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 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}