1use crate::error::StorageError;
14use crate::storage::Storage;
15use async_trait::async_trait;
16use std::collections::HashMap;
17use std::sync::Arc;
18use tokio::sync::RwLock;
19
20pub struct S3Storage {
25 pub bucket: String,
26 pub region: String,
27 store: Arc<RwLock<HashMap<String, Vec<u8>>>>,
28}
29
30impl S3Storage {
31 pub fn new(bucket: impl Into<String>, region: impl Into<String>) -> Self {
32 Self {
33 bucket: bucket.into(),
34 region: region.into(),
35 store: Arc::new(RwLock::new(HashMap::new())),
36 }
37 }
38
39 pub fn url_for(&self, key: &str) -> String {
40 format!("s3://{}.{}/{}", self.bucket, self.region, key)
41 }
42}
43
44#[async_trait]
45impl Storage for S3Storage {
46 async fn put(
47 &self,
48 key: &str,
49 data: &[u8],
50 _content_type: &str,
51 ) -> Result<String, StorageError> {
52 let mut store = self.store.write().await;
53 store.insert(key.to_string(), data.to_vec());
54 Ok(self.url_for(key))
55 }
56
57 async fn get(&self, key: &str) -> Result<Vec<u8>, StorageError> {
58 let store = self.store.read().await;
59 store
60 .get(key)
61 .cloned()
62 .ok_or_else(|| StorageError::NotFound(format!("s3://{}/{}", self.bucket, key)))
63 }
64
65 async fn delete(&self, key: &str) -> Result<(), StorageError> {
66 let mut store = self.store.write().await;
67 store.remove(key);
68 Ok(())
69 }
70
71 async fn exists(&self, key: &str) -> Result<bool, StorageError> {
72 let store = self.store.read().await;
73 Ok(store.contains_key(key))
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[tokio::test]
82 async fn test_s3_put_and_get() {
83 let storage = S3Storage::new("my-bucket", "us-east-1");
84 let url = storage
85 .put("file.txt", b"hello s3", "text/plain")
86 .await
87 .unwrap();
88 assert!(url.starts_with("s3://my-bucket.us-east-1/"));
89 assert!(url.contains("file.txt"));
90
91 let data = storage.get("file.txt").await.unwrap();
92 assert_eq!(data, b"hello s3");
93 }
94
95 #[tokio::test]
96 async fn test_s3_get_not_found() {
97 let storage = S3Storage::new("bucket", "us-west-2");
98 let result = storage.get("missing.txt").await;
99 assert!(result.is_err());
100 assert!(matches!(result.unwrap_err(), StorageError::NotFound(_)));
101 }
102
103 #[tokio::test]
104 async fn test_s3_delete() {
105 let storage = S3Storage::new("bucket", "us-east-1");
106 storage.put("del.txt", b"data", "text/plain").await.unwrap();
107 assert!(storage.exists("del.txt").await.unwrap());
108
109 storage.delete("del.txt").await.unwrap();
110 assert!(!storage.exists("del.txt").await.unwrap());
111 }
112
113 #[tokio::test]
114 async fn test_s3_exists_false_for_missing() {
115 let storage = S3Storage::new("bucket", "us-east-1");
116 assert!(!storage.exists("nope").await.unwrap());
117 }
118
119 #[tokio::test]
120 async fn test_s3_overwrite() {
121 let storage = S3Storage::new("bucket", "us-east-1");
122 storage.put("key", b"v1", "text/plain").await.unwrap();
123 storage.put("key", b"v2", "text/plain").await.unwrap();
124 let data = storage.get("key").await.unwrap();
125 assert_eq!(data, b"v2");
126 }
127
128 #[tokio::test]
129 async fn test_s3_multiple_keys() {
130 let storage = S3Storage::new("bucket", "us-east-1");
131 storage.put("a", b"a-data", "text/plain").await.unwrap();
132 storage.put("b", b"b-data", "text/plain").await.unwrap();
133 storage.put("c", b"c-data", "text/plain").await.unwrap();
134
135 assert_eq!(storage.get("a").await.unwrap(), b"a-data");
136 assert_eq!(storage.get("b").await.unwrap(), b"b-data");
137 assert_eq!(storage.get("c").await.unwrap(), b"c-data");
138
139 storage.delete("b").await.unwrap();
140 assert!(storage.exists("a").await.unwrap());
141 assert!(!storage.exists("b").await.unwrap());
142 assert!(storage.exists("c").await.unwrap());
143 }
144}