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 website_redirect_location: Option<String>,
47 pub restore_ongoing: Option<bool>,
49 pub restore_expiry: Option<String>,
51 pub checksum_algorithm: Option<String>,
53 pub checksum_value: Option<String>,
55 pub lock_mode: Option<String>,
57 pub lock_retain_until: Option<DateTime<Utc>>,
59 pub lock_legal_hold: Option<String>,
61}
62
63#[derive(Debug, Clone)]
65pub struct UploadPart {
66 pub part_number: u32,
67 pub body: BodyRef,
68 pub etag: String,
69 pub size: u64,
70 pub last_modified: DateTime<Utc>,
71}
72
73#[derive(Debug, Clone)]
75pub struct MultipartUpload {
76 pub upload_id: String,
77 pub key: String,
78 pub initiated: DateTime<Utc>,
79 pub parts: BTreeMap<u32, UploadPart>,
81 pub metadata: BTreeMap<String, String>,
83 pub content_type: String,
84 pub storage_class: String,
85 pub sse_algorithm: Option<String>,
86 pub sse_kms_key_id: Option<String>,
87 pub tagging: Option<String>,
88 pub acl_grants: Vec<AclGrant>,
89 pub checksum_algorithm: Option<String>,
90}
91
92#[derive(Debug, Clone)]
93pub struct S3Bucket {
94 pub name: String,
95 pub creation_date: DateTime<Utc>,
96 pub region: String,
97 pub objects: BTreeMap<String, S3Object>,
99 pub tags: BTreeMap<String, String>,
100 pub acl_grants: Vec<AclGrant>,
101 pub acl_owner_id: String,
102 pub multipart_uploads: BTreeMap<String, MultipartUpload>,
104 pub versioning: Option<String>,
106 pub object_versions: BTreeMap<String, Vec<S3Object>>,
108 pub acl: Option<String>,
110 pub encryption_config: Option<String>,
111 pub lifecycle_config: Option<String>,
112 pub lifecycle_transition_default_min_size: Option<String>,
117 pub policy: Option<String>,
118 pub cors_config: Option<String>,
119 pub notification_config: Option<String>,
120 pub logging_config: Option<String>,
121 pub website_config: Option<String>,
122 pub accelerate_status: Option<String>,
123 pub public_access_block: Option<String>,
124 pub object_lock_config: Option<String>,
125 pub replication_config: Option<String>,
126 pub ownership_controls: Option<String>,
127 pub inventory_configs: BTreeMap<String, String>,
128 pub eventbridge_enabled: bool,
130 pub analytics_configs: BTreeMap<String, String>,
132 pub intelligent_tiering_configs: BTreeMap<String, String>,
134 pub metrics_configs: BTreeMap<String, String>,
136 pub request_payment: Option<String>,
138 pub abac_config: Option<String>,
140 pub metadata_configuration: Option<String>,
142 pub metadata_table_configuration: Option<String>,
144}
145
146impl S3Bucket {
147 pub fn new(name: &str, region: &str, owner_id: &str) -> Self {
148 Self {
149 name: name.to_string(),
150 creation_date: Utc::now(),
151 region: region.to_string(),
152 objects: BTreeMap::new(),
153 tags: BTreeMap::new(),
154 acl_grants: vec![AclGrant {
155 grantee_type: "CanonicalUser".to_string(),
156 grantee_id: Some(owner_id.to_string()),
157 grantee_display_name: Some(owner_id.to_string()),
158 grantee_uri: None,
159 permission: "FULL_CONTROL".to_string(),
160 }],
161 acl_owner_id: owner_id.to_string(),
162 multipart_uploads: BTreeMap::new(),
163 versioning: None,
164 object_versions: BTreeMap::new(),
165 acl: None,
166 encryption_config: None,
167 lifecycle_config: None,
168 lifecycle_transition_default_min_size: None,
169 policy: None,
170 cors_config: None,
171 notification_config: None,
172 logging_config: None,
173 website_config: None,
174 accelerate_status: None,
175 public_access_block: None,
176 object_lock_config: None,
177 replication_config: None,
178 ownership_controls: None,
179 inventory_configs: BTreeMap::new(),
180 eventbridge_enabled: false,
181 analytics_configs: BTreeMap::new(),
182 intelligent_tiering_configs: BTreeMap::new(),
183 metrics_configs: BTreeMap::new(),
184 request_payment: None,
185 abac_config: None,
186 metadata_configuration: None,
187 metadata_table_configuration: None,
188 }
189 }
190}
191
192#[derive(Debug, Clone)]
194pub struct S3NotificationEvent {
195 pub bucket: String,
196 pub key: String,
197 pub event_type: String,
198 pub timestamp: DateTime<Utc>,
199}
200
201#[derive(Debug, Clone)]
204pub struct ObjectLambdaResponse {
205 pub route: String,
206 pub token: String,
207 pub body: Vec<u8>,
208 pub content_type: Option<String>,
209 pub fwd_status: Option<u16>,
210 pub fwd_error_message: Option<String>,
211 pub metadata: BTreeMap<String, String>,
212 pub encryption: Option<String>,
213 pub kms_key_id: Option<String>,
214 pub stored_at: DateTime<Utc>,
215}
216
217#[derive(Debug, Clone)]
218pub struct S3AccessPoint {
219 pub name: String,
220 pub bucket: String,
221 pub account_id: String,
222 pub network_origin: String,
223 pub vpc_configuration: Option<String>,
224 pub creation_date: DateTime<Utc>,
225 pub public_access_block: Option<String>,
226 pub bucket_account_id: Option<String>,
227}
228
229pub struct S3State {
230 pub account_id: String,
231 pub region: String,
232 pub buckets: BTreeMap<String, S3Bucket>,
233 pub notification_events: Vec<S3NotificationEvent>,
234 pub body_cache: Option<Arc<BodyCache>>,
235 pub object_lambda_responses: BTreeMap<String, ObjectLambdaResponse>,
237 pub access_points: BTreeMap<String, S3AccessPoint>,
238}
239
240impl S3State {
241 pub fn new(account_id: &str, region: &str) -> Self {
242 Self {
243 account_id: account_id.to_string(),
244 region: region.to_string(),
245 buckets: BTreeMap::new(),
246 notification_events: Vec::new(),
247 body_cache: None,
248 object_lambda_responses: BTreeMap::new(),
249 access_points: BTreeMap::new(),
250 }
251 }
252
253 pub fn set_body_cache(&mut self, cache: Arc<BodyCache>) {
254 self.body_cache = Some(cache);
255 }
256
257 pub fn reset(&mut self) {
258 self.buckets.clear();
259 self.notification_events.clear();
260 self.object_lambda_responses.clear();
261 }
262
263 pub fn read_body(&self, body: &BodyRef) -> io::Result<Bytes> {
266 match body {
267 BodyRef::Memory(b) => Ok(b.clone()),
268 BodyRef::Disk {
269 bucket,
270 key,
271 version,
272 path,
273 ..
274 } => {
275 let cache_key = BodyKey::new(bucket.clone(), key.clone(), version.clone());
276 if let Some(cache) = &self.body_cache {
277 if let Some(hit) = cache.get(&cache_key) {
278 return Ok(hit);
279 }
280 }
281 let data = std::fs::read(path)?;
282 let bytes = Bytes::from(data);
283 if let Some(cache) = &self.body_cache {
284 cache.insert(cache_key, bytes.clone());
285 }
286 Ok(bytes)
287 }
288 }
289 }
290
291 pub fn read_body_range(&self, body: &BodyRef, offset: u64, len: u64) -> io::Result<Bytes> {
295 match body {
296 BodyRef::Memory(b) => {
297 let start = offset as usize;
298 let end = start.saturating_add(len as usize).min(b.len());
299 if start > b.len() {
300 return Ok(Bytes::new());
301 }
302 Ok(b.slice(start..end))
303 }
304 BodyRef::Disk { path, .. } => {
305 let mut f = std::fs::File::open(path)?;
306 f.seek(SeekFrom::Start(offset))?;
307 let mut buf = vec![0u8; len as usize];
308 f.read_exact(&mut buf)?;
309 Ok(Bytes::from(buf))
310 }
311 }
312 }
313}
314
315impl fakecloud_core::multi_account::AccountState for S3State {
316 fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
317 Self::new(account_id, region)
318 }
319
320 fn inherit_from(&mut self, sibling: &Self) {
321 if let Some(cache) = &sibling.body_cache {
322 self.body_cache = Some(cache.clone());
323 }
324 }
325}
326
327pub type SharedS3State = Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<S3State>>>;
328
329pub fn memory_body(bytes: Bytes) -> BodyRef {
331 BodyRef::Memory(bytes)
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use std::io::Write;
338
339 #[test]
340 fn new_bucket_seeds_full_control_acl() {
341 let b = S3Bucket::new("my-bucket", "us-east-1", "owner-id");
342 assert_eq!(b.name, "my-bucket");
343 assert_eq!(b.region, "us-east-1");
344 assert_eq!(b.acl_owner_id, "owner-id");
345 assert_eq!(b.acl_grants.len(), 1);
346 assert_eq!(b.acl_grants[0].permission, "FULL_CONTROL");
347 assert_eq!(b.acl_grants[0].grantee_type, "CanonicalUser");
348 assert!(!b.eventbridge_enabled);
349 assert!(b.versioning.is_none());
350 }
351
352 #[test]
353 fn s3state_new_and_reset_clears_buckets() {
354 let mut state = S3State::new("123456789012", "us-east-1");
355 assert!(state.buckets.is_empty());
356 state
357 .buckets
358 .insert("b".to_string(), S3Bucket::new("b", "us-east-1", "owner"));
359 state.notification_events.push(S3NotificationEvent {
360 bucket: "b".to_string(),
361 key: "k".to_string(),
362 event_type: "s3:ObjectCreated:Put".to_string(),
363 timestamp: Utc::now(),
364 });
365 state.reset();
366 assert!(state.buckets.is_empty());
367 assert!(state.notification_events.is_empty());
368 }
369
370 #[test]
371 fn read_body_from_memory_returns_bytes() {
372 let state = S3State::new("123", "us-east-1");
373 let body = memory_body(Bytes::from_static(b"hello"));
374 assert_eq!(state.read_body(&body).unwrap(), &b"hello"[..]);
375 }
376
377 #[test]
378 fn read_body_from_disk_reads_file() {
379 let tmp = tempfile::NamedTempFile::new().unwrap();
380 tmp.as_file().write_all(b"file-body").unwrap();
381 let body = BodyRef::Disk {
382 bucket: "b".to_string(),
383 key: "k".to_string(),
384 version: None,
385 path: tmp.path().to_path_buf(),
386 size: 9,
387 };
388 let state = S3State::new("123", "us-east-1");
389 assert_eq!(state.read_body(&body).unwrap(), &b"file-body"[..]);
390 }
391
392 #[test]
393 fn read_body_range_slices_memory() {
394 let state = S3State::new("123", "us-east-1");
395 let body = memory_body(Bytes::from_static(b"abcdefghij"));
396 assert_eq!(state.read_body_range(&body, 2, 4).unwrap(), &b"cdef"[..]);
397 }
398
399 #[test]
400 fn read_body_range_memory_beyond_length_returns_empty() {
401 let state = S3State::new("123", "us-east-1");
402 let body = memory_body(Bytes::from_static(b"abc"));
403 assert!(state.read_body_range(&body, 100, 4).unwrap().is_empty());
404 }
405
406 #[test]
407 fn read_body_range_memory_clamps_to_length() {
408 let state = S3State::new("123", "us-east-1");
409 let body = memory_body(Bytes::from_static(b"abcdef"));
410 assert_eq!(state.read_body_range(&body, 4, 100).unwrap(), &b"ef"[..]);
411 }
412
413 #[test]
414 fn read_body_range_from_disk() {
415 let tmp = tempfile::NamedTempFile::new().unwrap();
416 tmp.as_file().write_all(b"0123456789").unwrap();
417 let body = BodyRef::Disk {
418 bucket: "b".to_string(),
419 key: "k".to_string(),
420 version: None,
421 path: tmp.path().to_path_buf(),
422 size: 10,
423 };
424 let state = S3State::new("123", "us-east-1");
425 assert_eq!(state.read_body_range(&body, 3, 4).unwrap(), &b"3456"[..]);
426 }
427
428 #[test]
429 fn account_state_impl_new_for_account() {
430 use fakecloud_core::multi_account::AccountState;
431 let s = S3State::new_for_account("111122223333", "eu-west-1", "http://x");
432 assert_eq!(s.account_id, "111122223333");
433 assert_eq!(s.region, "eu-west-1");
434 }
435}