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
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn test_object_metadata() {
268        let metadata = ObjectMetadata {
269            size: 1024,
270            last_modified: SystemTime::now(),
271            content_type: Some("application/json".to_string()),
272            etag: Some("abc123".to_string()),
273            storage_class: Some("STANDARD".to_string()),
274        };
275
276        assert_eq!(metadata.size, 1024);
277        assert!(metadata.content_type.is_some());
278        assert_eq!(metadata.content_type.unwrap(), "application/json");
279    }
280
281    #[test]
282    fn test_put_object_options_default() {
283        let options = PutObjectOptions::default();
284        assert!(options.content_type.is_none());
285        assert!(options.storage_class.is_none());
286        assert!(options.encryption.is_none());
287        assert_eq!(options.metadata.len(), 0);
288    }
289
290    #[test]
291    fn test_put_object_options_builder() {
292        let options = PutObjectOptions {
293            content_type: Some("text/plain".to_string()),
294            storage_class: Some("GLACIER".to_string()),
295            encryption: Some("AES256".to_string()),
296            metadata: vec![
297                ("author".to_string(), "John Doe".to_string()),
298                ("version".to_string(), "1.0".to_string()),
299            ],
300        };
301
302        assert_eq!(options.content_type.unwrap(), "text/plain");
303        assert_eq!(options.storage_class.unwrap(), "GLACIER");
304        assert_eq!(options.metadata.len(), 2);
305    }
306
307    #[test]
308    fn test_get_object_options() {
309        let options = GetObjectOptions {
310            range: Some((0, 1023)),
311            if_match: Some("etag-123".to_string()),
312        };
313
314        assert!(options.range.is_some());
315        assert_eq!(options.range.unwrap(), (0, 1023));
316        assert_eq!(options.if_match.unwrap(), "etag-123");
317    }
318}