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 advanced;
12pub mod error;
13pub mod storage;
14
15pub mod aliyun;
16pub mod huawei;
17pub mod local;
18pub mod qiniu;
19pub mod s3;
20pub mod tencent;
21pub mod upyun;
22
23// 高级存储功能(分片上传、断点续传、生命周期管理、CDN 刷新)
24pub use advanced::{
25    estimate_remaining_seconds, format_size, object_age_days, BucketLifecycle, CdnRefresher,
26    LifecycleAction, LifecycleEvaluationResult, LifecycleRule, MultipartUpload,
27    MultipartUploadSnapshot, Part, RefreshRequest, RefreshStatus, RefreshType,
28    ResumableUploadManager, UploadStatus,
29};
30
31pub use error::StorageError;
32pub use storage::*;
33
34pub use storage::StorageBuilder;
35pub use storage::StorageConfig;
36pub use storage::StorageProvider;
37pub use storage::StorageWrapper;
38
39pub use aliyun::AliyunOssStorage;
40pub use huawei::HuaweiObsStorage;
41pub use local::LocalStorage;
42pub use qiniu::QiniuKodoStorage;
43pub use s3::S3Storage;
44pub use tencent::TencentCosStorage;
45pub use upyun::UpYunStorage;
46
47#[cfg(feature = "s3-sdk")]
48pub mod s3_sdk;
49
50#[cfg(feature = "s3-sdk")]
51pub use s3_sdk::S3SdkStorage;
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    /// 测试数据目录:优先 F:\test\data(用户规范),回退到环境变量或系统 temp(CI/Linux)
58    ///
59    /// 注意:仅检查目录存在不足以保证可用——还需验证可写性,
60    /// 以避免在受限沙箱环境中因目录存在但不可写导致测试失败。
61    fn test_data_base() -> std::path::PathBuf {
62        let f_drive = std::path::Path::new("F:\\test\\data");
63        if is_dir_writable(f_drive) {
64            return f_drive.to_path_buf();
65        }
66        if let Ok(dir) = std::env::var("SZ_ORM_TEST_DATA_DIR") {
67            let p = std::path::PathBuf::from(&dir);
68            if is_dir_writable(&p) {
69                return p;
70            }
71        }
72        std::env::temp_dir()
73    }
74
75    /// 检查目录是否存在且可写:尝试在其中创建并删除一个探测文件
76    fn is_dir_writable(dir: &std::path::Path) -> bool {
77        if !dir.exists() {
78            return false;
79        }
80        let probe = dir.join(format!(".probe_{}", std::process::id()));
81        match std::fs::File::create(&probe) {
82            Ok(_) => {
83                let _ = std::fs::remove_file(&probe);
84                true
85            }
86            Err(_) => false,
87        }
88    }
89
90    #[tokio::test]
91    async fn test_local_storage_put_and_get() {
92        let temp_dir = test_data_base().join(format!("storage_test_{}", uuid_simple()));
93        let storage = LocalStorage::new(temp_dir.to_string_lossy());
94
95        let key = "test.txt";
96        let data = b"Hello, World!";
97        let content_type = "text/plain";
98
99        storage.put(key, data, content_type).await.unwrap();
100
101        let retrieved = storage.get(key).await.unwrap();
102        assert_eq!(retrieved, data);
103
104        tokio::fs::remove_dir_all(&temp_dir).await.ok();
105    }
106
107    #[tokio::test]
108    async fn test_local_storage_delete() {
109        let temp_dir = test_data_base().join(format!("storage_test_{}", uuid_simple()));
110        let storage = LocalStorage::new(temp_dir.to_string_lossy());
111
112        let key = "delete_me.txt";
113        storage.put(key, b"test", "text/plain").await.unwrap();
114
115        storage.delete(key).await.unwrap();
116
117        let exists = storage.exists(key).await.unwrap();
118        assert!(!exists);
119
120        tokio::fs::remove_dir_all(&temp_dir).await.ok();
121    }
122
123    #[tokio::test]
124    async fn test_local_storage_exists() {
125        let temp_dir = test_data_base().join(format!("storage_test_{}", uuid_simple()));
126        let storage = LocalStorage::new(temp_dir.to_string_lossy());
127
128        let key = "exists.txt";
129        assert!(!storage.exists(key).await.unwrap());
130
131        storage.put(key, b"test", "text/plain").await.unwrap();
132        assert!(storage.exists(key).await.unwrap());
133
134        tokio::fs::remove_dir_all(&temp_dir).await.ok();
135    }
136
137    #[tokio::test]
138    async fn test_local_storage_not_found() {
139        let temp_dir = test_data_base().join(format!("storage_test_{}", uuid_simple()));
140        let storage = LocalStorage::new(temp_dir.to_string_lossy());
141
142        let result = storage.get("nonexistent.txt").await;
143        assert!(result.is_err());
144        assert!(matches!(result.unwrap_err(), StorageError::NotFound(_)));
145    }
146
147    #[tokio::test]
148    async fn test_storage_builder_local() {
149        let builder = StorageBuilder::new(StorageProvider::Local)
150            .with_bucket("test-bucket")
151            .with_path_prefix("prefix/");
152
153        let wrapper = builder.build().unwrap();
154        assert!(matches!(wrapper, StorageWrapper::Local(_)));
155    }
156
157    #[tokio::test]
158    async fn test_storage_builder_s3() {
159        let builder = StorageBuilder::new(StorageProvider::S3(S3Config::default()))
160            .with_bucket("test-bucket")
161            .with_region("us-west-2")
162            .with_access_key("ak")
163            .with_secret_key("sk");
164
165        let wrapper = builder.build().unwrap();
166        assert!(matches!(wrapper, StorageWrapper::S3(_)));
167    }
168
169    #[tokio::test]
170    async fn test_storage_builder_aliyun() {
171        let builder = StorageBuilder::new(StorageProvider::AliyunOss(AliyunConfig::default()))
172            .with_bucket("test-bucket")
173            .with_endpoint("oss-cn-hangzhou.aliyuncs.com")
174            .with_access_key("ak")
175            .with_secret_key("sk");
176
177        let wrapper = builder.build().unwrap();
178        assert!(matches!(wrapper, StorageWrapper::Aliyun(_)));
179    }
180
181    #[tokio::test]
182    async fn test_storage_builder_tencent() {
183        let builder = StorageBuilder::new(StorageProvider::TencentCos(TencentConfig::default()))
184            .with_bucket("test-bucket")
185            .with_region("ap-guangzhou");
186
187        let wrapper = builder.build().unwrap();
188        assert!(matches!(wrapper, StorageWrapper::Tencent(_)));
189    }
190
191    #[tokio::test]
192    async fn test_storage_builder_qiniu() {
193        let builder = StorageBuilder::new(StorageProvider::QiniuKodo(QiniuConfig::default()))
194            .with_bucket("test-bucket");
195
196        let wrapper = builder.build().unwrap();
197        assert!(matches!(wrapper, StorageWrapper::Qiniu(_)));
198    }
199
200    #[tokio::test]
201    async fn test_storage_builder_huawei() {
202        let builder = StorageBuilder::new(StorageProvider::HuaweiObs(HuaweiConfig::default()))
203            .with_bucket("test-bucket")
204            .with_endpoint("obs.cn-north-4.myhuaweicloud.com");
205
206        let wrapper = builder.build().unwrap();
207        assert!(matches!(wrapper, StorageWrapper::Huawei(_)));
208    }
209
210    #[tokio::test]
211    async fn test_storage_builder_upyun() {
212        let builder = StorageBuilder::new(StorageProvider::UpYun(UpYunConfig::default()))
213            .with_bucket("test-bucket");
214
215        let wrapper = builder.build().unwrap();
216        assert!(matches!(wrapper, StorageWrapper::Upyun(_)));
217    }
218
219    #[tokio::test]
220    async fn test_builder_s3_actually_works() {
221        let wrapper = StorageBuilder::new(StorageProvider::S3(S3Config::default()))
222            .with_bucket("test-bucket")
223            .with_region("us-west-2")
224            .build()
225            .unwrap();
226
227        let url = wrapper
228            .put("k.txt", b"s3-via-builder", "text/plain")
229            .await
230            .unwrap();
231        assert!(url.starts_with("s3://test-bucket.us-west-2/"));
232
233        let data = wrapper.get("k.txt").await.unwrap();
234        assert_eq!(data, b"s3-via-builder");
235
236        assert!(wrapper.exists("k.txt").await.unwrap());
237        wrapper.delete("k.txt").await.unwrap();
238        assert!(!wrapper.exists("k.txt").await.unwrap());
239    }
240
241    #[tokio::test]
242    async fn test_builder_aliyun_actually_works() {
243        let wrapper = StorageBuilder::new(StorageProvider::AliyunOss(AliyunConfig::default()))
244            .with_bucket("bucket")
245            .with_endpoint("oss-cn-hangzhou.aliyuncs.com")
246            .build()
247            .unwrap();
248
249        wrapper
250            .put("k", b"aliyun-via-builder", "text/plain")
251            .await
252            .unwrap();
253        assert_eq!(wrapper.get("k").await.unwrap(), b"aliyun-via-builder");
254    }
255
256    #[tokio::test]
257    async fn test_builder_tencent_actually_works() {
258        let wrapper = StorageBuilder::new(StorageProvider::TencentCos(TencentConfig::default()))
259            .with_bucket("bucket")
260            .with_region("ap-guangzhou")
261            .build()
262            .unwrap();
263
264        wrapper
265            .put("k", b"tencent-via-builder", "text/plain")
266            .await
267            .unwrap();
268        assert_eq!(wrapper.get("k").await.unwrap(), b"tencent-via-builder");
269        assert!(wrapper.exists("k").await.unwrap());
270    }
271
272    #[tokio::test]
273    async fn test_builder_qiniu_actually_works() {
274        let wrapper = StorageBuilder::new(StorageProvider::QiniuKodo(QiniuConfig::default()))
275            .with_bucket("bucket")
276            .build()
277            .unwrap();
278
279        wrapper
280            .put("k", b"qiniu-via-builder", "text/plain")
281            .await
282            .unwrap();
283        assert_eq!(wrapper.get("k").await.unwrap(), b"qiniu-via-builder");
284    }
285
286    #[tokio::test]
287    async fn test_builder_huawei_actually_works() {
288        let wrapper = StorageBuilder::new(StorageProvider::HuaweiObs(HuaweiConfig::default()))
289            .with_bucket("bucket")
290            .with_endpoint("obs.cn-north-4.myhuaweicloud.com")
291            .build()
292            .unwrap();
293
294        wrapper
295            .put("k", b"huawei-via-builder", "text/plain")
296            .await
297            .unwrap();
298        assert_eq!(wrapper.get("k").await.unwrap(), b"huawei-via-builder");
299    }
300
301    #[tokio::test]
302    async fn test_builder_upyun_actually_works() {
303        let wrapper = StorageBuilder::new(StorageProvider::UpYun(UpYunConfig::default()))
304            .with_bucket("bucket")
305            .build()
306            .unwrap();
307
308        wrapper
309            .put("k", b"upyun-via-builder", "text/plain")
310            .await
311            .unwrap();
312        assert_eq!(wrapper.get("k").await.unwrap(), b"upyun-via-builder");
313    }
314
315    #[tokio::test]
316    async fn test_storage_wrapper_put() {
317        let temp_dir = test_data_base().join(format!("storage_test_{}", uuid_simple()));
318        let wrapper = StorageWrapper::Local(LocalStorage::new(temp_dir.to_string_lossy()));
319
320        let url = wrapper
321            .put("test.txt", b"data", "text/plain")
322            .await
323            .unwrap();
324        assert!(url.starts_with("local://"));
325
326        tokio::fs::remove_dir_all(&temp_dir).await.ok();
327    }
328
329    #[tokio::test]
330    async fn test_storage_wrapper_get() {
331        let temp_dir = test_data_base().join(format!("storage_test_{}", uuid_simple()));
332        let wrapper = StorageWrapper::Local(LocalStorage::new(temp_dir.to_string_lossy()));
333
334        wrapper
335            .put("test.txt", b"data", "text/plain")
336            .await
337            .unwrap();
338        let data = wrapper.get("test.txt").await.unwrap();
339        assert_eq!(data, b"data");
340
341        tokio::fs::remove_dir_all(&temp_dir).await.ok();
342    }
343
344    #[tokio::test]
345    async fn test_storage_config_default() {
346        let config = StorageConfig::default();
347        assert_eq!(config.bucket, "default-bucket");
348        assert_eq!(config.region, "us-east-1");
349    }
350
351    #[test]
352    fn test_storage_config_debug_masks_secrets() {
353        let config = StorageConfig {
354            bucket: "my-bucket".to_string(),
355            region: "us-east-1".to_string(),
356            endpoint: Some("https://example.com".to_string()),
357            access_key: Some("AKIAIOSFODNN7EXAMPLE".to_string()),
358            secret_key: Some("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string()),
359            path_prefix: Some("prefix/".to_string()),
360            base_path: Some("/tmp".to_string()),
361        };
362
363        let debug_output = format!("{:?}", config);
364
365        // 非敏感字段应正常输出
366        assert!(debug_output.contains("my-bucket"));
367        assert!(debug_output.contains("us-east-1"));
368        // 敏感字段必须被遮掩
369        assert!(debug_output.contains("\"***\""));
370        assert!(!debug_output.contains("AKIAIOSFODNN7EXAMPLE"));
371        assert!(!debug_output.contains("wJalrXUtnFEMI"));
372    }
373
374    fn uuid_simple() -> String {
375        use std::time::{SystemTime, UNIX_EPOCH};
376        let now = SystemTime::now()
377            .duration_since(UNIX_EPOCH)
378            .unwrap_or_default()
379            .as_nanos();
380        format!("{:x}", now)
381    }
382}