1pub mod error;
12pub mod storage;
13
14pub mod aliyun;
15pub mod huawei;
16pub mod local;
17pub mod qiniu;
18pub mod s3;
19pub mod tencent;
20pub mod upyun;
21
22pub use error::StorageError;
23pub use storage::*;
24
25pub use storage::StorageBuilder;
26pub use storage::StorageConfig;
27pub use storage::StorageProvider;
28pub use storage::StorageWrapper;
29
30pub use aliyun::AliyunOssStorage;
31pub use huawei::HuaweiObsStorage;
32pub use local::LocalStorage;
33pub use qiniu::QiniuKodoStorage;
34pub use s3::S3Storage;
35pub use tencent::TencentCosStorage;
36pub use upyun::UpYunStorage;
37
38#[cfg(feature = "s3-sdk")]
39pub mod s3_sdk;
40
41#[cfg(feature = "s3-sdk")]
42pub use s3_sdk::S3SdkStorage;
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47
48 #[tokio::test]
49 async fn test_local_storage_put_and_get() {
50 let temp_dir = std::env::temp_dir().join(format!("storage_test_{}", uuid_simple()));
51 let storage = LocalStorage::new(temp_dir.to_string_lossy());
52
53 let key = "test.txt";
54 let data = b"Hello, World!";
55 let content_type = "text/plain";
56
57 storage.put(key, data, content_type).await.unwrap();
58
59 let retrieved = storage.get(key).await.unwrap();
60 assert_eq!(retrieved, data);
61
62 tokio::fs::remove_dir_all(&temp_dir).await.ok();
63 }
64
65 #[tokio::test]
66 async fn test_local_storage_delete() {
67 let temp_dir = std::env::temp_dir().join(format!("storage_test_{}", uuid_simple()));
68 let storage = LocalStorage::new(temp_dir.to_string_lossy());
69
70 let key = "delete_me.txt";
71 storage.put(key, b"test", "text/plain").await.unwrap();
72
73 storage.delete(key).await.unwrap();
74
75 let exists = storage.exists(key).await.unwrap();
76 assert!(!exists);
77
78 tokio::fs::remove_dir_all(&temp_dir).await.ok();
79 }
80
81 #[tokio::test]
82 async fn test_local_storage_exists() {
83 let temp_dir = std::env::temp_dir().join(format!("storage_test_{}", uuid_simple()));
84 let storage = LocalStorage::new(temp_dir.to_string_lossy());
85
86 let key = "exists.txt";
87 assert!(!storage.exists(key).await.unwrap());
88
89 storage.put(key, b"test", "text/plain").await.unwrap();
90 assert!(storage.exists(key).await.unwrap());
91
92 tokio::fs::remove_dir_all(&temp_dir).await.ok();
93 }
94
95 #[tokio::test]
96 async fn test_local_storage_not_found() {
97 let temp_dir = std::env::temp_dir().join(format!("storage_test_{}", uuid_simple()));
98 let storage = LocalStorage::new(temp_dir.to_string_lossy());
99
100 let result = storage.get("nonexistent.txt").await;
101 assert!(result.is_err());
102 assert!(matches!(result.unwrap_err(), StorageError::NotFound(_)));
103 }
104
105 #[tokio::test]
106 async fn test_storage_builder_local() {
107 let builder = StorageBuilder::new(StorageProvider::Local)
108 .with_bucket("test-bucket")
109 .with_path_prefix("prefix/");
110
111 let wrapper = builder.build().unwrap();
112 assert!(matches!(wrapper, StorageWrapper::Local(_)));
113 }
114
115 #[tokio::test]
116 async fn test_storage_builder_s3() {
117 let builder = StorageBuilder::new(StorageProvider::S3(S3Config::default()))
118 .with_bucket("test-bucket")
119 .with_region("us-west-2")
120 .with_access_key("ak")
121 .with_secret_key("sk");
122
123 let wrapper = builder.build().unwrap();
124 assert!(matches!(wrapper, StorageWrapper::S3(_)));
125 }
126
127 #[tokio::test]
128 async fn test_storage_builder_aliyun() {
129 let builder = StorageBuilder::new(StorageProvider::AliyunOss(AliyunConfig::default()))
130 .with_bucket("test-bucket")
131 .with_endpoint("oss-cn-hangzhou.aliyuncs.com")
132 .with_access_key("ak")
133 .with_secret_key("sk");
134
135 let wrapper = builder.build().unwrap();
136 assert!(matches!(wrapper, StorageWrapper::Aliyun(_)));
137 }
138
139 #[tokio::test]
140 async fn test_storage_builder_tencent() {
141 let builder = StorageBuilder::new(StorageProvider::TencentCos(TencentConfig::default()))
142 .with_bucket("test-bucket")
143 .with_region("ap-guangzhou");
144
145 let wrapper = builder.build().unwrap();
146 assert!(matches!(wrapper, StorageWrapper::Tencent(_)));
147 }
148
149 #[tokio::test]
150 async fn test_storage_builder_qiniu() {
151 let builder = StorageBuilder::new(StorageProvider::QiniuKodo(QiniuConfig::default()))
152 .with_bucket("test-bucket");
153
154 let wrapper = builder.build().unwrap();
155 assert!(matches!(wrapper, StorageWrapper::Qiniu(_)));
156 }
157
158 #[tokio::test]
159 async fn test_storage_builder_huawei() {
160 let builder = StorageBuilder::new(StorageProvider::HuaweiObs(HuaweiConfig::default()))
161 .with_bucket("test-bucket")
162 .with_endpoint("obs.cn-north-4.myhuaweicloud.com");
163
164 let wrapper = builder.build().unwrap();
165 assert!(matches!(wrapper, StorageWrapper::Huawei(_)));
166 }
167
168 #[tokio::test]
169 async fn test_storage_builder_upyun() {
170 let builder = StorageBuilder::new(StorageProvider::UpYun(UpYunConfig::default()))
171 .with_bucket("test-bucket");
172
173 let wrapper = builder.build().unwrap();
174 assert!(matches!(wrapper, StorageWrapper::Upyun(_)));
175 }
176
177 #[tokio::test]
178 async fn test_builder_s3_actually_works() {
179 let wrapper = StorageBuilder::new(StorageProvider::S3(S3Config::default()))
180 .with_bucket("test-bucket")
181 .with_region("us-west-2")
182 .build()
183 .unwrap();
184
185 let url = wrapper
186 .put("k.txt", b"s3-via-builder", "text/plain")
187 .await
188 .unwrap();
189 assert!(url.starts_with("s3://test-bucket.us-west-2/"));
190
191 let data = wrapper.get("k.txt").await.unwrap();
192 assert_eq!(data, b"s3-via-builder");
193
194 assert!(wrapper.exists("k.txt").await.unwrap());
195 wrapper.delete("k.txt").await.unwrap();
196 assert!(!wrapper.exists("k.txt").await.unwrap());
197 }
198
199 #[tokio::test]
200 async fn test_builder_aliyun_actually_works() {
201 let wrapper = StorageBuilder::new(StorageProvider::AliyunOss(AliyunConfig::default()))
202 .with_bucket("bucket")
203 .with_endpoint("oss-cn-hangzhou.aliyuncs.com")
204 .build()
205 .unwrap();
206
207 wrapper
208 .put("k", b"aliyun-via-builder", "text/plain")
209 .await
210 .unwrap();
211 assert_eq!(wrapper.get("k").await.unwrap(), b"aliyun-via-builder");
212 }
213
214 #[tokio::test]
215 async fn test_builder_tencent_actually_works() {
216 let wrapper = StorageBuilder::new(StorageProvider::TencentCos(TencentConfig::default()))
217 .with_bucket("bucket")
218 .with_region("ap-guangzhou")
219 .build()
220 .unwrap();
221
222 wrapper
223 .put("k", b"tencent-via-builder", "text/plain")
224 .await
225 .unwrap();
226 assert_eq!(wrapper.get("k").await.unwrap(), b"tencent-via-builder");
227 assert!(wrapper.exists("k").await.unwrap());
228 }
229
230 #[tokio::test]
231 async fn test_builder_qiniu_actually_works() {
232 let wrapper = StorageBuilder::new(StorageProvider::QiniuKodo(QiniuConfig::default()))
233 .with_bucket("bucket")
234 .build()
235 .unwrap();
236
237 wrapper
238 .put("k", b"qiniu-via-builder", "text/plain")
239 .await
240 .unwrap();
241 assert_eq!(wrapper.get("k").await.unwrap(), b"qiniu-via-builder");
242 }
243
244 #[tokio::test]
245 async fn test_builder_huawei_actually_works() {
246 let wrapper = StorageBuilder::new(StorageProvider::HuaweiObs(HuaweiConfig::default()))
247 .with_bucket("bucket")
248 .with_endpoint("obs.cn-north-4.myhuaweicloud.com")
249 .build()
250 .unwrap();
251
252 wrapper
253 .put("k", b"huawei-via-builder", "text/plain")
254 .await
255 .unwrap();
256 assert_eq!(wrapper.get("k").await.unwrap(), b"huawei-via-builder");
257 }
258
259 #[tokio::test]
260 async fn test_builder_upyun_actually_works() {
261 let wrapper = StorageBuilder::new(StorageProvider::UpYun(UpYunConfig::default()))
262 .with_bucket("bucket")
263 .build()
264 .unwrap();
265
266 wrapper
267 .put("k", b"upyun-via-builder", "text/plain")
268 .await
269 .unwrap();
270 assert_eq!(wrapper.get("k").await.unwrap(), b"upyun-via-builder");
271 }
272
273 #[tokio::test]
274 async fn test_storage_wrapper_put() {
275 let temp_dir = std::env::temp_dir().join(format!("storage_test_{}", uuid_simple()));
276 let wrapper = StorageWrapper::Local(LocalStorage::new(temp_dir.to_string_lossy()));
277
278 let url = wrapper
279 .put("test.txt", b"data", "text/plain")
280 .await
281 .unwrap();
282 assert!(url.starts_with("local://"));
283
284 tokio::fs::remove_dir_all(&temp_dir).await.ok();
285 }
286
287 #[tokio::test]
288 async fn test_storage_wrapper_get() {
289 let temp_dir = std::env::temp_dir().join(format!("storage_test_{}", uuid_simple()));
290 let wrapper = StorageWrapper::Local(LocalStorage::new(temp_dir.to_string_lossy()));
291
292 wrapper
293 .put("test.txt", b"data", "text/plain")
294 .await
295 .unwrap();
296 let data = wrapper.get("test.txt").await.unwrap();
297 assert_eq!(data, b"data");
298
299 tokio::fs::remove_dir_all(&temp_dir).await.ok();
300 }
301
302 #[tokio::test]
303 async fn test_storage_config_default() {
304 let config = StorageConfig::default();
305 assert_eq!(config.bucket, "default-bucket");
306 assert_eq!(config.region, "us-east-1");
307 }
308
309 #[test]
310 fn test_storage_config_debug_masks_secrets() {
311 let config = StorageConfig {
312 bucket: "my-bucket".to_string(),
313 region: "us-east-1".to_string(),
314 endpoint: Some("https://example.com".to_string()),
315 access_key: Some("AKIAIOSFODNN7EXAMPLE".to_string()),
316 secret_key: Some("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string()),
317 path_prefix: Some("prefix/".to_string()),
318 base_path: Some("/tmp".to_string()),
319 };
320
321 let debug_output = format!("{:?}", config);
322
323 assert!(debug_output.contains("my-bucket"));
325 assert!(debug_output.contains("us-east-1"));
326 assert!(debug_output.contains("\"***\""));
328 assert!(!debug_output.contains("AKIAIOSFODNN7EXAMPLE"));
329 assert!(!debug_output.contains("wJalrXUtnFEMI"));
330 }
331
332 fn uuid_simple() -> String {
333 use std::time::{SystemTime, UNIX_EPOCH};
334 let now = SystemTime::now()
335 .duration_since(UNIX_EPOCH)
336 .unwrap_or_default()
337 .as_nanos();
338 format!("{:x}", now)
339 }
340}