llm_shield_cloud/storage.rs
1//! Cloud storage abstractions.
2//!
3//! Provides unified trait for object storage across cloud providers:
4//! - AWS S3
5//! - GCP Cloud Storage
6//! - Azure Blob Storage
7
8use crate::error::{CloudError, Result};
9use async_trait::async_trait;
10use std::time::SystemTime;
11
12/// Metadata about a storage object.
13#[derive(Debug, Clone)]
14pub struct ObjectMetadata {
15 /// Size of the object in bytes.
16 pub size: u64,
17
18 /// When the object was last modified.
19 pub last_modified: SystemTime,
20
21 /// Content type of the object (e.g., "application/json").
22 pub content_type: Option<String>,
23
24 /// ETag or version identifier.
25 pub etag: Option<String>,
26
27 /// Storage class/tier (e.g., "STANDARD", "GLACIER", "ARCHIVE").
28 pub storage_class: Option<String>,
29}
30
31/// Options for uploading objects.
32#[derive(Debug, Clone, Default)]
33pub struct PutObjectOptions {
34 /// Content type of the object.
35 pub content_type: Option<String>,
36
37 /// Storage class/tier.
38 pub storage_class: Option<String>,
39
40 /// Server-side encryption algorithm.
41 pub encryption: Option<String>,
42
43 /// Custom metadata key-value pairs.
44 pub metadata: Vec<(String, String)>,
45}
46
47/// Options for downloading objects.
48#[derive(Debug, Clone, Default)]
49pub struct GetObjectOptions {
50 /// Byte range to fetch (start, end).
51 pub range: Option<(u64, u64)>,
52
53 /// Expected ETag for conditional fetch.
54 pub if_match: Option<String>,
55}
56
57/// Unified trait for cloud object storage.
58///
59/// This trait provides a consistent interface for object storage operations
60/// across different cloud providers (AWS S3, GCP Cloud Storage, Azure Blob Storage).
61#[async_trait]
62pub trait CloudStorage: Send + Sync {
63 /// Gets an object by key.
64 ///
65 /// # Arguments
66 ///
67 /// * `key` - The object key/path
68 ///
69 /// # Returns
70 ///
71 /// Returns the object data as bytes.
72 ///
73 /// # Errors
74 ///
75 /// Returns `CloudError::StorageObjectNotFound` if the object doesn't exist.
76 /// Returns `CloudError::StorageFetch` if the fetch operation fails.
77 async fn get_object(&self, key: &str) -> Result<Vec<u8>>;
78
79 /// Gets an object with options.
80 ///
81 /// # Arguments
82 ///
83 /// * `key` - The object key/path
84 /// * `options` - Fetch options (range, conditional fetch, etc.)
85 ///
86 /// # Returns
87 ///
88 /// Returns the object data as bytes.
89 ///
90 /// # Errors
91 ///
92 /// Returns `CloudError::StorageFetch` if the fetch operation fails.
93 async fn get_object_with_options(
94 &self,
95 key: &str,
96 options: &GetObjectOptions,
97 ) -> Result<Vec<u8>> {
98 // Default implementation ignores options
99 self.get_object(key).await
100 }
101
102 /// Puts an object with key.
103 ///
104 /// # Arguments
105 ///
106 /// * `key` - The object key/path
107 /// * `data` - The object data
108 ///
109 /// # Errors
110 ///
111 /// Returns `CloudError::StoragePut` if the put operation fails.
112 async fn put_object(&self, key: &str, data: &[u8]) -> Result<()>;
113
114 /// Puts an object with options.
115 ///
116 /// # Arguments
117 ///
118 /// * `key` - The object key/path
119 /// * `data` - The object data
120 /// * `options` - Upload options (content type, encryption, etc.)
121 ///
122 /// # Errors
123 ///
124 /// Returns `CloudError::StoragePut` if the put operation fails.
125 async fn put_object_with_options(
126 &self,
127 key: &str,
128 data: &[u8],
129 options: &PutObjectOptions,
130 ) -> Result<()> {
131 // Default implementation ignores options
132 self.put_object(key, data).await
133 }
134
135 /// Deletes an object.
136 ///
137 /// # Arguments
138 ///
139 /// * `key` - The object key/path to delete
140 ///
141 /// # Errors
142 ///
143 /// Returns `CloudError::StorageDelete` if the delete operation fails.
144 async fn delete_object(&self, key: &str) -> Result<()>;
145
146 /// Lists objects with a given prefix.
147 ///
148 /// # Arguments
149 ///
150 /// * `prefix` - The prefix to filter objects
151 ///
152 /// # Returns
153 ///
154 /// Returns a vector of object keys/paths.
155 ///
156 /// # Errors
157 ///
158 /// Returns `CloudError::StorageList` if the list operation fails.
159 async fn list_objects(&self, prefix: &str) -> Result<Vec<String>>;
160
161 /// Lists objects with a given prefix and limit.
162 ///
163 /// # Arguments
164 ///
165 /// * `prefix` - The prefix to filter objects
166 /// * `max_results` - Maximum number of results to return
167 ///
168 /// # Returns
169 ///
170 /// Returns a vector of object keys/paths.
171 ///
172 /// # Errors
173 ///
174 /// Returns `CloudError::StorageList` if the list operation fails.
175 async fn list_objects_with_limit(
176 &self,
177 prefix: &str,
178 max_results: usize,
179 ) -> Result<Vec<String>> {
180 // Default implementation gets all and truncates
181 let mut objects = self.list_objects(prefix).await?;
182 objects.truncate(max_results);
183 Ok(objects)
184 }
185
186 /// Checks if an object exists.
187 ///
188 /// # Arguments
189 ///
190 /// * `key` - The object key/path to check
191 ///
192 /// # Returns
193 ///
194 /// Returns `true` if the object exists, `false` otherwise.
195 ///
196 /// # Errors
197 ///
198 /// Returns `CloudError::StorageFetch` if the check operation fails
199 /// (but not if the object simply doesn't exist).
200 async fn object_exists(&self, key: &str) -> Result<bool> {
201 match self.get_object_metadata(key).await {
202 Ok(_) => Ok(true),
203 Err(CloudError::StorageObjectNotFound(_)) => Ok(false),
204 Err(e) => Err(e),
205 }
206 }
207
208 /// Gets object metadata without fetching the full object.
209 ///
210 /// # Arguments
211 ///
212 /// * `key` - The object key/path
213 ///
214 /// # Returns
215 ///
216 /// Returns metadata about the object.
217 ///
218 /// # Errors
219 ///
220 /// Returns `CloudError::StorageObjectNotFound` if the object doesn't exist.
221 /// Returns `CloudError::StorageFetch` if the metadata fetch fails.
222 async fn get_object_metadata(&self, key: &str) -> Result<ObjectMetadata>;
223
224 /// Copies an object within the same storage.
225 ///
226 /// # Arguments
227 ///
228 /// * `from_key` - Source object key/path
229 /// * `to_key` - Destination object key/path
230 ///
231 /// # Errors
232 ///
233 /// Returns `CloudError::StorageFetch` if the source doesn't exist.
234 /// Returns `CloudError::StoragePut` if the copy operation fails.
235 async fn copy_object(&self, from_key: &str, to_key: &str) -> Result<()> {
236 // Default implementation: get then put
237 let data = self.get_object(from_key).await?;
238 self.put_object(to_key, &data).await
239 }
240
241 /// Moves an object (copy then delete).
242 ///
243 /// # Arguments
244 ///
245 /// * `from_key` - Source object key/path
246 /// * `to_key` - Destination object key/path
247 ///
248 /// # Errors
249 ///
250 /// Returns errors from copy or delete operations.
251 async fn move_object(&self, from_key: &str, to_key: &str) -> Result<()> {
252 self.copy_object(from_key, to_key).await?;
253 self.delete_object(from_key).await
254 }
255
256 /// Gets the storage provider name (e.g., "s3", "gcs", "azure").
257 fn provider_name(&self) -> &str {
258 "unknown"
259 }
260
261 /// Deletes multiple objects in batch.
262 ///
263 /// # Arguments
264 ///
265 /// * `keys` - Vector of object keys/paths to delete
266 ///
267 /// # Errors
268 ///
269 /// Returns `CloudError::StorageDelete` if the batch delete fails.
270 async fn delete_objects(&self, keys: &[String]) -> Result<()> {
271 // Default implementation: delete one by one
272 for key in keys {
273 self.delete_object(key).await?;
274 }
275 Ok(())
276 }
277
278 /// Lists objects with metadata.
279 ///
280 /// # Arguments
281 ///
282 /// * `prefix` - The prefix to filter objects
283 ///
284 /// # Returns
285 ///
286 /// Returns a vector of (key, metadata) tuples.
287 ///
288 /// # Errors
289 ///
290 /// Returns `CloudError::StorageList` if the list operation fails.
291 async fn list_objects_with_metadata(&self, prefix: &str) -> Result<Vec<ObjectMetadata>> {
292 // Default implementation not provided - must be overridden
293 Err(CloudError::OperationFailed(
294 "list_objects_with_metadata not implemented".to_string(),
295 ))
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn test_object_metadata() {
305 let metadata = ObjectMetadata {
306 size: 1024,
307 last_modified: SystemTime::now(),
308 content_type: Some("application/json".to_string()),
309 etag: Some("abc123".to_string()),
310 storage_class: Some("STANDARD".to_string()),
311 };
312
313 assert_eq!(metadata.size, 1024);
314 assert!(metadata.content_type.is_some());
315 assert_eq!(metadata.content_type.unwrap(), "application/json");
316 }
317
318 #[test]
319 fn test_put_object_options_default() {
320 let options = PutObjectOptions::default();
321 assert!(options.content_type.is_none());
322 assert!(options.storage_class.is_none());
323 assert!(options.encryption.is_none());
324 assert_eq!(options.metadata.len(), 0);
325 }
326
327 #[test]
328 fn test_put_object_options_builder() {
329 let options = PutObjectOptions {
330 content_type: Some("text/plain".to_string()),
331 storage_class: Some("GLACIER".to_string()),
332 encryption: Some("AES256".to_string()),
333 metadata: vec![
334 ("author".to_string(), "John Doe".to_string()),
335 ("version".to_string(), "1.0".to_string()),
336 ],
337 };
338
339 assert_eq!(options.content_type.unwrap(), "text/plain");
340 assert_eq!(options.storage_class.unwrap(), "GLACIER");
341 assert_eq!(options.metadata.len(), 2);
342 }
343
344 #[test]
345 fn test_get_object_options() {
346 let options = GetObjectOptions {
347 range: Some((0, 1023)),
348 if_match: Some("etag-123".to_string()),
349 };
350
351 assert!(options.range.is_some());
352 assert_eq!(options.range.unwrap(), (0, 1023));
353 assert_eq!(options.if_match.unwrap(), "etag-123");
354 }
355}