1use bytes::Bytes;
2use chrono::{DateTime, Utc};
3use fakecloud_persistence::cache::{BodyCache, BodyKey};
4use fakecloud_persistence::BodyRef;
5use parking_lot::RwLock;
6use std::collections::BTreeMap;
7use std::io::{self, Read, Seek, SeekFrom};
8use std::sync::Arc;
9
10#[derive(Debug, Clone)]
12pub struct AclGrant {
13 pub grantee_type: String, pub grantee_id: Option<String>,
15 pub grantee_display_name: Option<String>,
16 pub grantee_uri: Option<String>,
17 pub permission: String, }
19
20#[derive(Debug, Clone, Default)]
21pub struct S3Object {
22 pub key: String,
23 pub body: BodyRef,
24 pub content_type: String,
25 pub etag: String,
26 pub size: u64,
27 pub last_modified: DateTime<Utc>,
28 pub metadata: BTreeMap<String, String>,
29 pub storage_class: String,
30 pub tags: BTreeMap<String, String>,
31 pub acl_grants: Vec<AclGrant>,
32 pub acl_owner_id: Option<String>,
33 pub parts_count: Option<u32>,
35 pub part_sizes: Option<Vec<(u32, u64)>>,
37 pub sse_algorithm: Option<String>,
39 pub sse_kms_key_id: Option<String>,
41 pub bucket_key_enabled: Option<bool>,
43 pub version_id: Option<String>,
44 pub is_delete_marker: bool,
45 pub content_encoding: Option<String>,
46 pub cache_control: Option<String>,
48 pub content_disposition: Option<String>,
49 pub content_language: Option<String>,
50 pub expires: Option<String>,
51 pub website_redirect_location: Option<String>,
52 pub restore_ongoing: Option<bool>,
54 pub restore_expiry: Option<String>,
56 pub checksum_algorithm: Option<String>,
58 pub checksum_value: Option<String>,
60 pub lock_mode: Option<String>,
62 pub lock_retain_until: Option<DateTime<Utc>>,
64 pub lock_legal_hold: Option<String>,
66}
67
68#[derive(Debug, Clone)]
70pub struct UploadPart {
71 pub part_number: u32,
72 pub body: BodyRef,
73 pub etag: String,
74 pub size: u64,
75 pub last_modified: DateTime<Utc>,
76}
77
78#[derive(Debug, Clone)]
80pub struct MultipartUpload {
81 pub upload_id: String,
82 pub key: String,
83 pub initiated: DateTime<Utc>,
84 pub parts: BTreeMap<u32, UploadPart>,
86 pub metadata: BTreeMap<String, String>,
88 pub content_type: String,
89 pub storage_class: String,
90 pub sse_algorithm: Option<String>,
91 pub sse_kms_key_id: Option<String>,
92 pub tagging: Option<String>,
93 pub acl_grants: Vec<AclGrant>,
94 pub checksum_algorithm: Option<String>,
95 pub cache_control: Option<String>,
96 pub content_disposition: Option<String>,
97 pub content_language: Option<String>,
98 pub expires: Option<String>,
99}
100
101#[derive(Debug, Clone)]
102pub struct S3Bucket {
103 pub name: String,
104 pub creation_date: DateTime<Utc>,
105 pub region: String,
106 pub objects: BTreeMap<String, S3Object>,
108 pub tags: BTreeMap<String, String>,
109 pub acl_grants: Vec<AclGrant>,
110 pub acl_owner_id: String,
111 pub multipart_uploads: BTreeMap<String, MultipartUpload>,
113 pub versioning: Option<String>,
115 pub mfa_delete: Option<String>,
117 pub object_versions: BTreeMap<String, Vec<S3Object>>,
119 pub acl: Option<String>,
121 pub encryption_config: Option<String>,
122 pub lifecycle_config: Option<String>,
123 pub lifecycle_transition_default_min_size: Option<String>,
128 pub policy: Option<String>,
129 pub cors_config: Option<String>,
130 pub notification_config: Option<String>,
131 pub logging_config: Option<String>,
132 pub website_config: Option<String>,
133 pub accelerate_status: Option<String>,
134 pub public_access_block: Option<String>,
135 pub object_lock_config: Option<String>,
136 pub replication_config: Option<String>,
137 pub ownership_controls: Option<String>,
138 pub inventory_configs: BTreeMap<String, String>,
139 pub eventbridge_enabled: bool,
141 pub analytics_configs: BTreeMap<String, String>,
143 pub intelligent_tiering_configs: BTreeMap<String, String>,
145 pub metrics_configs: BTreeMap<String, String>,
147 pub request_payment: Option<String>,
149 pub abac_config: Option<String>,
151 pub metadata_configuration: Option<String>,
153 pub metadata_table_configuration: Option<String>,
155}
156
157impl S3Bucket {
158 pub fn new(name: &str, region: &str, owner_id: &str) -> Self {
159 Self {
160 name: name.to_string(),
161 creation_date: Utc::now(),
162 region: region.to_string(),
163 objects: BTreeMap::new(),
164 tags: BTreeMap::new(),
165 acl_grants: vec![AclGrant {
166 grantee_type: "CanonicalUser".to_string(),
167 grantee_id: Some(owner_id.to_string()),
168 grantee_display_name: Some(owner_id.to_string()),
169 grantee_uri: None,
170 permission: "FULL_CONTROL".to_string(),
171 }],
172 acl_owner_id: owner_id.to_string(),
173 multipart_uploads: BTreeMap::new(),
174 versioning: None,
175 mfa_delete: None,
176 object_versions: BTreeMap::new(),
177 acl: None,
178 encryption_config: None,
179 lifecycle_config: None,
180 lifecycle_transition_default_min_size: None,
181 policy: None,
182 cors_config: None,
183 notification_config: None,
184 logging_config: None,
185 website_config: None,
186 accelerate_status: None,
187 public_access_block: None,
188 object_lock_config: None,
189 replication_config: None,
190 ownership_controls: None,
191 inventory_configs: BTreeMap::new(),
192 eventbridge_enabled: false,
193 analytics_configs: BTreeMap::new(),
194 intelligent_tiering_configs: BTreeMap::new(),
195 metrics_configs: BTreeMap::new(),
196 request_payment: None,
197 abac_config: None,
198 metadata_configuration: None,
199 metadata_table_configuration: None,
200 }
201 }
202}
203
204#[derive(Debug, Clone)]
206pub struct S3NotificationEvent {
207 pub bucket: String,
208 pub key: String,
209 pub event_type: String,
210 pub timestamp: DateTime<Utc>,
211}
212
213#[derive(Debug, Clone)]
216pub struct ObjectLambdaResponse {
217 pub route: String,
218 pub token: String,
219 pub body: Vec<u8>,
220 pub content_type: Option<String>,
221 pub fwd_status: Option<u16>,
222 pub fwd_error_message: Option<String>,
223 pub metadata: BTreeMap<String, String>,
224 pub encryption: Option<String>,
225 pub kms_key_id: Option<String>,
226 pub stored_at: DateTime<Utc>,
227}
228
229#[derive(Debug, Clone)]
230pub struct S3AccessPoint {
231 pub name: String,
232 pub bucket: String,
233 pub account_id: String,
234 pub network_origin: String,
235 pub vpc_configuration: Option<String>,
236 pub creation_date: DateTime<Utc>,
237 pub public_access_block: Option<String>,
238 pub bucket_account_id: Option<String>,
239}
240
241pub struct S3State {
242 pub account_id: String,
243 pub region: String,
244 pub buckets: BTreeMap<String, S3Bucket>,
245 pub notification_events: Vec<S3NotificationEvent>,
246 pub body_cache: Option<Arc<BodyCache>>,
247 pub object_lambda_responses: BTreeMap<String, ObjectLambdaResponse>,
249 pub access_points: BTreeMap<String, S3AccessPoint>,
250}
251
252impl S3State {
253 pub fn new(account_id: &str, region: &str) -> Self {
254 Self {
255 account_id: account_id.to_string(),
256 region: region.to_string(),
257 buckets: BTreeMap::new(),
258 notification_events: Vec::new(),
259 body_cache: None,
260 object_lambda_responses: BTreeMap::new(),
261 access_points: BTreeMap::new(),
262 }
263 }
264
265 pub fn set_body_cache(&mut self, cache: Arc<BodyCache>) {
266 self.body_cache = Some(cache);
267 }
268
269 pub fn reset(&mut self) {
270 self.buckets.clear();
271 self.notification_events.clear();
272 self.object_lambda_responses.clear();
273 }
274
275 pub fn read_body_uncached(body: &BodyRef) -> io::Result<Bytes> {
282 match body {
283 BodyRef::Memory(b) => Ok(b.clone()),
284 BodyRef::Disk { path, .. } => Ok(Bytes::from(std::fs::read(path)?)),
285 }
286 }
287
288 pub fn read_body(&self, body: &BodyRef) -> io::Result<Bytes> {
291 match body {
292 BodyRef::Memory(b) => Ok(b.clone()),
293 BodyRef::Disk {
294 bucket,
295 key,
296 version,
297 path,
298 ..
299 } => {
300 let cache_key = BodyKey::new(bucket.clone(), key.clone(), version.clone());
301 if let Some(cache) = &self.body_cache {
302 if let Some(hit) = cache.get(&cache_key) {
303 return Ok(hit);
304 }
305 }
306 let data = std::fs::read(path)?;
307 let bytes = Bytes::from(data);
308 if let Some(cache) = &self.body_cache {
309 cache.insert(cache_key, bytes.clone());
310 }
311 Ok(bytes)
312 }
313 }
314 }
315
316 pub fn read_body_range(&self, body: &BodyRef, offset: u64, len: u64) -> io::Result<Bytes> {
320 match body {
321 BodyRef::Memory(b) => {
322 let start = offset as usize;
323 let end = start.saturating_add(len as usize).min(b.len());
324 if start > b.len() {
325 return Ok(Bytes::new());
326 }
327 Ok(b.slice(start..end))
328 }
329 BodyRef::Disk { path, .. } => {
330 let mut f = std::fs::File::open(path)?;
331 f.seek(SeekFrom::Start(offset))?;
332 let mut buf = vec![0u8; len as usize];
333 f.read_exact(&mut buf)?;
334 Ok(Bytes::from(buf))
335 }
336 }
337 }
338}
339
340impl fakecloud_core::multi_account::AccountState for S3State {
341 fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
342 Self::new(account_id, region)
343 }
344
345 fn inherit_from(&mut self, sibling: &Self) {
346 if let Some(cache) = &sibling.body_cache {
347 self.body_cache = Some(cache.clone());
348 }
349 }
350}
351
352pub type SharedS3State = Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<S3State>>>;
353
354pub fn memory_body(bytes: Bytes) -> BodyRef {
356 BodyRef::Memory(bytes)
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362 use std::io::Write;
363
364 #[test]
365 fn new_bucket_seeds_full_control_acl() {
366 let b = S3Bucket::new("my-bucket", "us-east-1", "owner-id");
367 assert_eq!(b.name, "my-bucket");
368 assert_eq!(b.region, "us-east-1");
369 assert_eq!(b.acl_owner_id, "owner-id");
370 assert_eq!(b.acl_grants.len(), 1);
371 assert_eq!(b.acl_grants[0].permission, "FULL_CONTROL");
372 assert_eq!(b.acl_grants[0].grantee_type, "CanonicalUser");
373 assert!(!b.eventbridge_enabled);
374 assert!(b.versioning.is_none());
375 }
376
377 #[test]
378 fn s3state_new_and_reset_clears_buckets() {
379 let mut state = S3State::new("123456789012", "us-east-1");
380 assert!(state.buckets.is_empty());
381 state
382 .buckets
383 .insert("b".to_string(), S3Bucket::new("b", "us-east-1", "owner"));
384 state.notification_events.push(S3NotificationEvent {
385 bucket: "b".to_string(),
386 key: "k".to_string(),
387 event_type: "s3:ObjectCreated:Put".to_string(),
388 timestamp: Utc::now(),
389 });
390 state.reset();
391 assert!(state.buckets.is_empty());
392 assert!(state.notification_events.is_empty());
393 }
394
395 #[test]
396 fn read_body_from_memory_returns_bytes() {
397 let state = S3State::new("123", "us-east-1");
398 let body = memory_body(Bytes::from_static(b"hello"));
399 assert_eq!(state.read_body(&body).unwrap(), &b"hello"[..]);
400 }
401
402 #[test]
403 fn read_body_from_disk_reads_file() {
404 let tmp = tempfile::NamedTempFile::new().unwrap();
405 tmp.as_file().write_all(b"file-body").unwrap();
406 let body = BodyRef::Disk {
407 bucket: "b".to_string(),
408 key: "k".to_string(),
409 version: None,
410 path: tmp.path().to_path_buf(),
411 size: 9,
412 };
413 let state = S3State::new("123", "us-east-1");
414 assert_eq!(state.read_body(&body).unwrap(), &b"file-body"[..]);
415 }
416
417 #[test]
418 fn read_body_uncached_reads_memory_and_disk() {
419 let mem = memory_body(Bytes::from_static(b"hello"));
422 assert_eq!(S3State::read_body_uncached(&mem).unwrap(), &b"hello"[..]);
423
424 let tmp = tempfile::NamedTempFile::new().unwrap();
425 tmp.as_file().write_all(b"file-body").unwrap();
426 let disk = BodyRef::Disk {
427 bucket: "b".to_string(),
428 key: "k".to_string(),
429 version: None,
430 path: tmp.path().to_path_buf(),
431 size: 9,
432 };
433 assert_eq!(
434 S3State::read_body_uncached(&disk).unwrap(),
435 &b"file-body"[..]
436 );
437 }
438
439 #[test]
440 fn read_body_range_slices_memory() {
441 let state = S3State::new("123", "us-east-1");
442 let body = memory_body(Bytes::from_static(b"abcdefghij"));
443 assert_eq!(state.read_body_range(&body, 2, 4).unwrap(), &b"cdef"[..]);
444 }
445
446 #[test]
447 fn read_body_range_memory_beyond_length_returns_empty() {
448 let state = S3State::new("123", "us-east-1");
449 let body = memory_body(Bytes::from_static(b"abc"));
450 assert!(state.read_body_range(&body, 100, 4).unwrap().is_empty());
451 }
452
453 #[test]
454 fn read_body_range_memory_clamps_to_length() {
455 let state = S3State::new("123", "us-east-1");
456 let body = memory_body(Bytes::from_static(b"abcdef"));
457 assert_eq!(state.read_body_range(&body, 4, 100).unwrap(), &b"ef"[..]);
458 }
459
460 #[test]
461 fn read_body_range_from_disk() {
462 let tmp = tempfile::NamedTempFile::new().unwrap();
463 tmp.as_file().write_all(b"0123456789").unwrap();
464 let body = BodyRef::Disk {
465 bucket: "b".to_string(),
466 key: "k".to_string(),
467 version: None,
468 path: tmp.path().to_path_buf(),
469 size: 10,
470 };
471 let state = S3State::new("123", "us-east-1");
472 assert_eq!(state.read_body_range(&body, 3, 4).unwrap(), &b"3456"[..]);
473 }
474
475 #[test]
476 fn account_state_impl_new_for_account() {
477 use fakecloud_core::multi_account::AccountState;
478 let s = S3State::new_for_account("111122223333", "eu-west-1", "http://x");
479 assert_eq!(s.account_id, "111122223333");
480 assert_eq!(s.region, "eu-west-1");
481 }
482}