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
173#[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 {
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 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}