Skip to main content

sz_orm_storage/
lib.rs

1//! # SZ-ORM Storage — 对象存储
2//!
3//! 提供统一的对象存储抽象,支持 S3、阿里云 OSS、七牛 Kodo、华为 OBS、
4//! 腾讯 COS、又拍云以及本地文件系统,可通过 `StorageBuilder` 配置多 provider。
5//!
6//! ## 主要模块
7//!
8//! - [`storage`] — 统一 trait 与构建器
9//! - [`s3`] / [`aliyun`] / [`huawei`] / [`tencent`] / [`qiniu`] / [`upyun`] / [`local`] — 各 provider 实现
10
11pub 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        // 非敏感字段应正常输出
324        assert!(debug_output.contains("my-bucket"));
325        assert!(debug_output.contains("us-east-1"));
326        // 敏感字段必须被遮掩
327        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}