1use chrono::{DateTime, Utc};
14use dashmap::DashMap;
15use parking_lot::RwLock;
16use serde::{Deserialize, Serialize};
17use tracing::debug;
18
19use super::{
20 keystore::ObjectStore,
21 multipart::MultipartUpload,
22 object::{CannedAcl, Owner},
23};
24
25#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
31pub enum VersioningStatus {
32 #[default]
34 Disabled,
35 Enabled,
37 Suspended,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct BucketEncryption {
45 pub sse_algorithm: String,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub kms_master_key_id: Option<String>,
50 #[serde(default)]
52 pub bucket_key_enabled: bool,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub struct CorsRuleConfig {
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub id: Option<String>,
65 pub allowed_origins: Vec<String>,
67 pub allowed_methods: Vec<String>,
69 #[serde(default)]
71 pub allowed_headers: Vec<String>,
72 #[serde(default)]
74 pub expose_headers: Vec<String>,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub max_age_seconds: Option<i32>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85#[allow(clippy::struct_excessive_bools)]
86pub struct PublicAccessBlockConfig {
87 #[serde(default)]
89 pub block_public_acls: bool,
90 #[serde(default)]
92 pub ignore_public_acls: bool,
93 #[serde(default)]
95 pub block_public_policy: bool,
96 #[serde(default)]
98 pub restrict_public_buckets: bool,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct OwnershipControlsConfig {
105 pub object_ownership: String,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112#[serde(rename_all = "camelCase")]
113pub struct ObjectLockConfiguration {
114 pub object_lock_enabled: String,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub rule: Option<ObjectLockRule>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct ObjectLockRule {
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub default_retention: Option<DefaultRetention>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct DefaultRetention {
134 pub mode: String,
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub days: Option<i32>,
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub years: Option<i32>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct WebsiteConfig {
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub index_document_suffix: Option<String>,
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub error_document_key: Option<String>,
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub redirect_all_requests_to_host: Option<String>,
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub redirect_all_requests_to_protocol: Option<String>,
160}
161
162pub struct S3Bucket {
171 pub name: String,
173 pub region: String,
175 pub creation_date: DateTime<Utc>,
177 pub owner: Owner,
179
180 pub objects: RwLock<ObjectStore>,
183 pub multipart_uploads: DashMap<String, MultipartUpload>,
185
186 pub versioning: RwLock<VersioningStatus>,
189
190 pub encryption: RwLock<Option<BucketEncryption>>,
193 pub cors_rules: RwLock<Option<Vec<CorsRuleConfig>>>,
195 pub lifecycle: RwLock<Option<rustack_s3_model::types::BucketLifecycleConfiguration>>,
197 pub policy: RwLock<Option<String>>,
199 pub tags: RwLock<Vec<(String, String)>>,
201 pub acl: RwLock<CannedAcl>,
203 pub notification_configuration:
205 RwLock<Option<rustack_s3_model::types::NotificationConfiguration>>,
206 pub logging: RwLock<Option<serde_json::Value>>,
208 pub public_access_block: RwLock<Option<PublicAccessBlockConfig>>,
210 pub ownership_controls: RwLock<Option<OwnershipControlsConfig>>,
212 pub object_lock_enabled: RwLock<bool>,
214 pub object_lock_configuration: RwLock<Option<ObjectLockConfiguration>>,
216 pub accelerate: RwLock<Option<String>>,
218 pub request_payment: RwLock<String>,
220 pub website: RwLock<Option<WebsiteConfig>>,
222 pub replication: RwLock<Option<serde_json::Value>>,
224 pub analytics: RwLock<Option<serde_json::Value>>,
226 pub metrics: RwLock<Option<serde_json::Value>>,
228 pub inventory: RwLock<Option<serde_json::Value>>,
230 pub intelligent_tiering: RwLock<Option<serde_json::Value>>,
232}
233
234impl std::fmt::Debug for S3Bucket {
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 f.debug_struct("S3Bucket")
237 .field("name", &self.name)
238 .field("region", &self.region)
239 .field("creation_date", &self.creation_date)
240 .field("owner", &self.owner)
241 .field("versioning", &*self.versioning.read())
242 .finish_non_exhaustive()
243 }
244}
245
246impl S3Bucket {
247 #[must_use]
251 pub fn new(name: String, region: String, owner: Owner) -> Self {
252 Self {
253 name,
254 region,
255 creation_date: Utc::now(),
256 owner,
257 objects: RwLock::new(ObjectStore::default()),
258 multipart_uploads: DashMap::new(),
259 versioning: RwLock::new(VersioningStatus::default()),
260 encryption: RwLock::new(None),
261 cors_rules: RwLock::new(None),
262 lifecycle: RwLock::new(None),
263 policy: RwLock::new(None),
264 tags: RwLock::new(Vec::new()),
265 acl: RwLock::new(CannedAcl::default()),
266 notification_configuration: RwLock::new(None),
267 logging: RwLock::new(None),
268 public_access_block: RwLock::new(None),
269 ownership_controls: RwLock::new(None),
270 object_lock_enabled: RwLock::new(false),
271 object_lock_configuration: RwLock::new(None),
272 accelerate: RwLock::new(None),
273 request_payment: RwLock::new("BucketOwner".to_owned()),
274 website: RwLock::new(None),
275 replication: RwLock::new(None),
276 analytics: RwLock::new(None),
277 metrics: RwLock::new(None),
278 inventory: RwLock::new(None),
279 intelligent_tiering: RwLock::new(None),
280 }
281 }
282
283 #[must_use]
285 pub fn is_empty(&self) -> bool {
286 self.objects.read().is_empty() && self.multipart_uploads.is_empty()
287 }
288
289 #[must_use]
291 pub fn is_versioning_enabled(&self) -> bool {
292 *self.versioning.read() == VersioningStatus::Enabled
293 }
294
295 pub fn enable_versioning(&self) {
301 let mut status = self.versioning.write();
302 if *status != VersioningStatus::Enabled {
303 debug!(bucket = %self.name, "enabling versioning");
304 let mut store = self.objects.write();
306 store.transition_to_versioned();
307 *status = VersioningStatus::Enabled;
308 }
309 }
310
311 pub fn suspend_versioning(&self) {
317 let mut status = self.versioning.write();
318 if *status == VersioningStatus::Enabled {
319 debug!(bucket = %self.name, "suspending versioning");
320 *status = VersioningStatus::Suspended;
321 }
322 }
323}
324
325#[cfg(test)]
330mod tests {
331 use super::*;
332
333 fn make_bucket(name: &str) -> S3Bucket {
334 S3Bucket::new(name.to_owned(), "us-east-1".to_owned(), Owner::default())
335 }
336
337 #[test]
338 fn test_should_create_bucket_with_defaults() {
339 let bucket = make_bucket("test-bucket");
340 assert_eq!(bucket.name, "test-bucket");
341 assert_eq!(bucket.region, "us-east-1");
342 assert!(bucket.is_empty());
343 assert!(!bucket.is_versioning_enabled());
344 assert_eq!(*bucket.versioning.read(), VersioningStatus::Disabled);
345 assert_eq!(*bucket.acl.read(), CannedAcl::Private);
346 assert_eq!(*bucket.request_payment.read(), "BucketOwner");
347 }
348
349 #[test]
350 fn test_should_debug_format_bucket() {
351 let bucket = make_bucket("debug-bucket");
352 let debug_str = format!("{bucket:?}");
353 assert!(debug_str.contains("debug-bucket"));
354 assert!(debug_str.contains("S3Bucket"));
355 }
356
357 #[test]
358 fn test_should_enable_versioning() {
359 let bucket = make_bucket("versioned-bucket");
360 assert!(!bucket.is_versioning_enabled());
361 assert!(!bucket.objects.read().is_versioned());
362
363 bucket.enable_versioning();
364 assert!(bucket.is_versioning_enabled());
365 assert!(bucket.objects.read().is_versioned());
366 }
367
368 #[test]
369 fn test_should_suspend_versioning() {
370 let bucket = make_bucket("suspend-bucket");
371 bucket.enable_versioning();
372 assert!(bucket.is_versioning_enabled());
373
374 bucket.suspend_versioning();
375 assert!(!bucket.is_versioning_enabled());
376 assert_eq!(*bucket.versioning.read(), VersioningStatus::Suspended);
377 assert!(bucket.objects.read().is_versioned());
379 }
380
381 #[test]
382 fn test_should_not_suspend_if_never_enabled() {
383 let bucket = make_bucket("never-versioned");
384 bucket.suspend_versioning();
385 assert_eq!(*bucket.versioning.read(), VersioningStatus::Disabled);
387 }
388
389 #[test]
390 fn test_should_enable_versioning_idempotent() {
391 let bucket = make_bucket("idem-bucket");
392 bucket.enable_versioning();
393 bucket.enable_versioning();
394 assert!(bucket.is_versioning_enabled());
395 }
396
397 #[test]
398 fn test_should_report_empty_with_no_objects_or_uploads() {
399 let bucket = make_bucket("empty-bucket");
400 assert!(bucket.is_empty());
401 }
402
403 #[test]
404 fn test_should_report_not_empty_with_multipart() {
405 let bucket = make_bucket("mp-bucket");
406 let upload = super::super::multipart::MultipartUpload::new(
407 "upload-1".to_owned(),
408 "key".to_owned(),
409 Owner::default(),
410 super::super::object::ObjectMetadata::default(),
411 );
412 bucket
413 .multipart_uploads
414 .insert("upload-1".to_owned(), upload);
415 assert!(!bucket.is_empty());
416 }
417
418 #[test]
419 fn test_should_default_versioning_status_to_disabled() {
420 assert_eq!(VersioningStatus::default(), VersioningStatus::Disabled);
421 }
422
423 #[test]
424 fn test_should_create_cors_rule_config() {
425 let rule = CorsRuleConfig {
426 id: Some("rule-1".to_owned()),
427 allowed_origins: vec!["*".to_owned()],
428 allowed_methods: vec!["GET".to_owned(), "PUT".to_owned()],
429 allowed_headers: vec!["*".to_owned()],
430 expose_headers: Vec::new(),
431 max_age_seconds: Some(3600),
432 };
433 assert_eq!(rule.id, Some("rule-1".to_owned()));
434 assert_eq!(rule.allowed_methods.len(), 2);
435 }
436
437 #[test]
438 fn test_should_create_public_access_block_config() {
439 let config = PublicAccessBlockConfig {
440 block_public_acls: true,
441 ignore_public_acls: true,
442 block_public_policy: true,
443 restrict_public_buckets: true,
444 };
445 assert!(config.block_public_acls);
446 assert!(config.restrict_public_buckets);
447 }
448
449 #[test]
450 fn test_should_create_object_lock_configuration() {
451 let config = ObjectLockConfiguration {
452 object_lock_enabled: "Enabled".to_owned(),
453 rule: Some(ObjectLockRule {
454 default_retention: Some(DefaultRetention {
455 mode: "GOVERNANCE".to_owned(),
456 days: Some(30),
457 years: None,
458 }),
459 }),
460 };
461 let retention = config
462 .rule
463 .as_ref()
464 .and_then(|r| r.default_retention.as_ref());
465 assert!(retention.is_some());
466 assert_eq!(retention.map(|r| r.days), Some(Some(30)));
467 }
468
469 #[test]
470 fn test_should_create_bucket_encryption() {
471 let enc = BucketEncryption {
472 sse_algorithm: "aws:kms".to_owned(),
473 kms_master_key_id: Some("arn:aws:kms:us-east-1:123456789012:key/abc".to_owned()),
474 bucket_key_enabled: true,
475 };
476 assert_eq!(enc.sse_algorithm, "aws:kms");
477 assert!(enc.bucket_key_enabled);
478 }
479}