Skip to main content

fakecloud_s3/
state.rs

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, HashMap};
7use std::io::{self, Read, Seek, SeekFrom};
8use std::sync::Arc;
9
10/// An ACL grant entry.
11#[derive(Debug, Clone)]
12pub struct AclGrant {
13    pub grantee_type: String, // "CanonicalUser" or "Group"
14    pub grantee_id: Option<String>,
15    pub grantee_display_name: Option<String>,
16    pub grantee_uri: Option<String>,
17    pub permission: String, // READ, WRITE, READ_ACP, WRITE_ACP, FULL_CONTROL
18}
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: HashMap<String, String>,
29    pub storage_class: String,
30    pub tags: HashMap<String, String>,
31    pub acl_grants: Vec<AclGrant>,
32    pub acl_owner_id: Option<String>,
33    /// If created from multipart upload, the number of parts.
34    pub parts_count: Option<u32>,
35    /// Per-part sizes for multipart objects (part_number, size).
36    pub part_sizes: Option<Vec<(u32, u64)>>,
37    /// Server-side encryption algorithm.
38    pub sse_algorithm: Option<String>,
39    /// KMS key ID for SSE-KMS.
40    pub sse_kms_key_id: Option<String>,
41    /// Whether bucket key is enabled.
42    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    /// Glacier restore: ongoing request status.
48    pub restore_ongoing: Option<bool>,
49    /// Glacier restore: expiry date string.
50    pub restore_expiry: Option<String>,
51    /// Checksum algorithm (CRC32, SHA1, SHA256).
52    pub checksum_algorithm: Option<String>,
53    /// Base64-encoded checksum value.
54    pub checksum_value: Option<String>,
55    /// Object lock mode (GOVERNANCE or COMPLIANCE).
56    pub lock_mode: Option<String>,
57    /// Object lock retain-until date (ISO 8601).
58    pub lock_retain_until: Option<DateTime<Utc>>,
59    /// Legal hold status (ON or OFF).
60    pub lock_legal_hold: Option<String>,
61}
62
63/// A part uploaded via the multipart upload API.
64#[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/// An in-progress multipart upload.
74#[derive(Debug, Clone)]
75pub struct MultipartUpload {
76    pub upload_id: String,
77    pub key: String,
78    pub initiated: DateTime<Utc>,
79    /// Parts keyed by part number.
80    pub parts: BTreeMap<u32, UploadPart>,
81    /// Metadata provided at CreateMultipartUpload time.
82    pub metadata: HashMap<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    /// Objects keyed by their full key path.
98    pub objects: BTreeMap<String, S3Object>,
99    pub tags: HashMap<String, String>,
100    pub acl_grants: Vec<AclGrant>,
101    pub acl_owner_id: String,
102    /// In-progress multipart uploads keyed by upload ID.
103    pub multipart_uploads: HashMap<String, MultipartUpload>,
104    /// Versioning status: None = never enabled, Some("Enabled"), Some("Suspended").
105    pub versioning: Option<String>,
106    /// Object versions keyed by key, each value is a list of versions.
107    pub object_versions: HashMap<String, Vec<S3Object>>,
108    /// Bucket ACL (canned or XML).
109    pub acl: Option<String>,
110    pub encryption_config: Option<String>,
111    pub lifecycle_config: Option<String>,
112    pub policy: Option<String>,
113    pub cors_config: Option<String>,
114    pub notification_config: Option<String>,
115    pub logging_config: Option<String>,
116    pub website_config: Option<String>,
117    pub accelerate_status: Option<String>,
118    pub public_access_block: Option<String>,
119    pub object_lock_config: Option<String>,
120    pub replication_config: Option<String>,
121    pub ownership_controls: Option<String>,
122    pub inventory_configs: HashMap<String, String>,
123    /// Whether EventBridge notifications are enabled for this bucket.
124    pub eventbridge_enabled: bool,
125}
126
127impl S3Bucket {
128    pub fn new(name: &str, region: &str, owner_id: &str) -> Self {
129        Self {
130            name: name.to_string(),
131            creation_date: Utc::now(),
132            region: region.to_string(),
133            objects: BTreeMap::new(),
134            tags: HashMap::new(),
135            acl_grants: vec![AclGrant {
136                grantee_type: "CanonicalUser".to_string(),
137                grantee_id: Some(owner_id.to_string()),
138                grantee_display_name: Some(owner_id.to_string()),
139                grantee_uri: None,
140                permission: "FULL_CONTROL".to_string(),
141            }],
142            acl_owner_id: owner_id.to_string(),
143            multipart_uploads: HashMap::new(),
144            versioning: None,
145            object_versions: HashMap::new(),
146            acl: None,
147            encryption_config: None,
148            lifecycle_config: None,
149            policy: None,
150            cors_config: None,
151            notification_config: None,
152            logging_config: None,
153            website_config: None,
154            accelerate_status: None,
155            public_access_block: None,
156            object_lock_config: None,
157            replication_config: None,
158            ownership_controls: None,
159            inventory_configs: HashMap::new(),
160            eventbridge_enabled: false,
161        }
162    }
163}
164
165/// A recorded S3 notification event for introspection.
166#[derive(Debug, Clone)]
167pub struct S3NotificationEvent {
168    pub bucket: String,
169    pub key: String,
170    pub event_type: String,
171    pub timestamp: DateTime<Utc>,
172}
173
174pub struct S3State {
175    pub account_id: String,
176    pub region: String,
177    pub buckets: HashMap<String, S3Bucket>,
178    pub notification_events: Vec<S3NotificationEvent>,
179    pub body_cache: Option<Arc<BodyCache>>,
180}
181
182impl S3State {
183    pub fn new(account_id: &str, region: &str) -> Self {
184        Self {
185            account_id: account_id.to_string(),
186            region: region.to_string(),
187            buckets: HashMap::new(),
188            notification_events: Vec::new(),
189            body_cache: None,
190        }
191    }
192
193    pub fn set_body_cache(&mut self, cache: Arc<BodyCache>) {
194        self.body_cache = Some(cache);
195    }
196
197    pub fn reset(&mut self) {
198        self.buckets.clear();
199        self.notification_events.clear();
200    }
201
202    /// Read the full body referenced by a [`BodyRef`], consulting the
203    /// persistent [`BodyCache`] when one is configured.
204    pub fn read_body(&self, body: &BodyRef) -> io::Result<Bytes> {
205        match body {
206            BodyRef::Memory(b) => Ok(b.clone()),
207            BodyRef::Disk {
208                bucket,
209                key,
210                version,
211                path,
212                ..
213            } => {
214                let cache_key = BodyKey::new(bucket.clone(), key.clone(), version.clone());
215                if let Some(cache) = &self.body_cache {
216                    if let Some(hit) = cache.get(&cache_key) {
217                        return Ok(hit);
218                    }
219                }
220                let data = std::fs::read(path)?;
221                let bytes = Bytes::from(data);
222                if let Some(cache) = &self.body_cache {
223                    cache.insert(cache_key, bytes.clone());
224                }
225                Ok(bytes)
226            }
227        }
228    }
229
230    /// Read a byte range from the body without loading the full object into
231    /// memory. Memory bodies are sliced directly; disk bodies are seek+read'd.
232    /// Ranges bypass the body cache (the cache stores whole objects only).
233    pub fn read_body_range(&self, body: &BodyRef, offset: u64, len: u64) -> io::Result<Bytes> {
234        match body {
235            BodyRef::Memory(b) => {
236                let start = offset as usize;
237                let end = start.saturating_add(len as usize).min(b.len());
238                if start > b.len() {
239                    return Ok(Bytes::new());
240                }
241                Ok(b.slice(start..end))
242            }
243            BodyRef::Disk { path, .. } => {
244                let mut f = std::fs::File::open(path)?;
245                f.seek(SeekFrom::Start(offset))?;
246                let mut buf = vec![0u8; len as usize];
247                f.read_exact(&mut buf)?;
248                Ok(Bytes::from(buf))
249            }
250        }
251    }
252}
253
254pub type SharedS3State = Arc<RwLock<S3State>>;
255
256/// Construct a memory-backed [`BodyRef`] from [`Bytes`].
257pub fn memory_body(bytes: Bytes) -> BodyRef {
258    BodyRef::Memory(bytes)
259}