1pub 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
23pub 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 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 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 assert!(debug_output.contains("my-bucket"));
367 assert!(debug_output.contains("us-east-1"));
368 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}