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}