1pub mod advanced;
53pub mod error;
54pub mod storage;
55
56pub mod aliyun;
57pub mod huawei;
58pub mod local;
59pub mod qiniu;
60pub mod s3;
61pub mod tencent;
62pub mod upyun;
63
64#[cfg(feature = "real-cloud")]
65pub mod real;
66
67pub use advanced::{
69 estimate_remaining_seconds, format_size, object_age_days, BucketLifecycle, CdnRefresher,
70 LifecycleAction, LifecycleEvaluationResult, LifecycleRule, MultipartUpload,
71 MultipartUploadSnapshot, Part, RefreshRequest, RefreshStatus, RefreshType,
72 ResumableUploadManager, UploadStatus,
73};
74
75pub use error::StorageError;
76pub use storage::*;
77
78pub use storage::StorageBuilder;
79pub use storage::StorageConfig;
80pub use storage::StorageProvider;
81pub use storage::StorageWrapper;
82
83pub use aliyun::AliyunOssStorage;
84pub use huawei::HuaweiObsStorage;
85pub use local::LocalStorage;
86pub use qiniu::QiniuKodoStorage;
87pub use s3::S3Storage;
88pub use tencent::TencentCosStorage;
89pub use upyun::UpYunStorage;
90
91#[cfg(feature = "real-cloud")]
92pub use real::{OpendalStorage, RealCloudStorage, RealQiniuKodoStorage};
93
94#[cfg(feature = "s3-sdk")]
95pub mod s3_sdk;
96
97#[cfg(feature = "s3-sdk")]
98pub use s3_sdk::S3SdkStorage;
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 fn test_data_base() -> std::path::PathBuf {
109 let f_drive = std::path::Path::new("F:\\test\\data");
110 if is_dir_writable(f_drive) {
111 return f_drive.to_path_buf();
112 }
113 if let Ok(dir) = std::env::var("SZ_ORM_TEST_DATA_DIR") {
114 let p = std::path::PathBuf::from(&dir);
115 if is_dir_writable(&p) {
116 return p;
117 }
118 }
119 std::env::temp_dir()
120 }
121
122 fn is_dir_writable(dir: &std::path::Path) -> bool {
124 if !dir.exists() {
125 return false;
126 }
127 let probe = dir.join(format!(".probe_{}", std::process::id()));
128 match std::fs::File::create(&probe) {
129 Ok(_) => {
130 let _ = std::fs::remove_file(&probe);
131 true
132 }
133 Err(_) => false,
134 }
135 }
136
137 #[tokio::test]
138 async fn test_local_storage_put_and_get() {
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 key = "test.txt";
143 let data = b"Hello, World!";
144 let content_type = "text/plain";
145
146 storage.put(key, data, content_type).await.unwrap();
147
148 let retrieved = storage.get(key).await.unwrap();
149 assert_eq!(retrieved, data);
150
151 tokio::fs::remove_dir_all(&temp_dir).await.ok();
152 }
153
154 #[tokio::test]
155 async fn test_local_storage_delete() {
156 let temp_dir = test_data_base().join(format!("storage_test_{}", uuid_simple()));
157 let storage = LocalStorage::new(temp_dir.to_string_lossy());
158
159 let key = "delete_me.txt";
160 storage.put(key, b"test", "text/plain").await.unwrap();
161
162 storage.delete(key).await.unwrap();
163
164 let exists = storage.exists(key).await.unwrap();
165 assert!(!exists);
166
167 tokio::fs::remove_dir_all(&temp_dir).await.ok();
168 }
169
170 #[tokio::test]
171 async fn test_local_storage_exists() {
172 let temp_dir = test_data_base().join(format!("storage_test_{}", uuid_simple()));
173 let storage = LocalStorage::new(temp_dir.to_string_lossy());
174
175 let key = "exists.txt";
176 assert!(!storage.exists(key).await.unwrap());
177
178 storage.put(key, b"test", "text/plain").await.unwrap();
179 assert!(storage.exists(key).await.unwrap());
180
181 tokio::fs::remove_dir_all(&temp_dir).await.ok();
182 }
183
184 #[tokio::test]
185 async fn test_local_storage_not_found() {
186 let temp_dir = test_data_base().join(format!("storage_test_{}", uuid_simple()));
187 let storage = LocalStorage::new(temp_dir.to_string_lossy());
188
189 let result = storage.get("nonexistent.txt").await;
190 assert!(result.is_err());
191 assert!(matches!(result.unwrap_err(), StorageError::NotFound(_)));
192 }
193
194 #[tokio::test]
195 async fn test_storage_builder_local() {
196 let builder = StorageBuilder::new(StorageProvider::Local)
197 .with_bucket("test-bucket")
198 .with_path_prefix("prefix/");
199
200 let wrapper = builder.build().unwrap();
201 assert!(matches!(wrapper, StorageWrapper::Local(_)));
202 }
203
204 #[tokio::test]
205 async fn test_storage_builder_s3() {
206 let builder = StorageBuilder::new(StorageProvider::S3(S3Config::default()))
207 .with_bucket("test-bucket")
208 .with_region("us-west-2")
209 .with_access_key("ak")
210 .with_secret_key("sk");
211
212 let wrapper = builder.build().unwrap();
213 assert!(matches!(wrapper, StorageWrapper::S3(_)));
214 }
215
216 #[cfg(not(feature = "real-cloud"))]
217 #[tokio::test]
218 async fn test_storage_builder_aliyun() {
219 let builder = StorageBuilder::new(StorageProvider::AliyunOss(AliyunConfig::default()))
220 .with_bucket("test-bucket")
221 .with_endpoint("oss-cn-hangzhou.aliyuncs.com")
222 .with_access_key("ak")
223 .with_secret_key("sk");
224
225 let wrapper = builder.build().unwrap();
226 assert!(matches!(wrapper, StorageWrapper::Aliyun(_)));
227 }
228
229 #[cfg(not(feature = "real-cloud"))]
230 #[tokio::test]
231 async fn test_storage_builder_tencent() {
232 let builder = StorageBuilder::new(StorageProvider::TencentCos(TencentConfig::default()))
233 .with_bucket("test-bucket")
234 .with_region("ap-guangzhou");
235
236 let wrapper = builder.build().unwrap();
237 assert!(matches!(wrapper, StorageWrapper::Tencent(_)));
238 }
239
240 #[cfg(not(feature = "real-cloud"))]
241 #[tokio::test]
242 async fn test_storage_builder_qiniu() {
243 let builder = StorageBuilder::new(StorageProvider::QiniuKodo(QiniuConfig::default()))
244 .with_bucket("test-bucket");
245
246 let wrapper = builder.build().unwrap();
247 assert!(matches!(wrapper, StorageWrapper::Qiniu(_)));
248 }
249
250 #[cfg(not(feature = "real-cloud"))]
251 #[tokio::test]
252 async fn test_storage_builder_huawei() {
253 let builder = StorageBuilder::new(StorageProvider::HuaweiObs(HuaweiConfig::default()))
254 .with_bucket("test-bucket")
255 .with_endpoint("obs.cn-north-4.myhuaweicloud.com");
256
257 let wrapper = builder.build().unwrap();
258 assert!(matches!(wrapper, StorageWrapper::Huawei(_)));
259 }
260
261 #[cfg(not(feature = "real-cloud"))]
262 #[tokio::test]
263 async fn test_storage_builder_upyun() {
264 let builder = StorageBuilder::new(StorageProvider::UpYun(UpYunConfig::default()))
265 .with_bucket("test-bucket");
266
267 let wrapper = builder.build().unwrap();
268 assert!(matches!(wrapper, StorageWrapper::Upyun(_)));
269 }
270
271 #[tokio::test]
272 async fn test_builder_s3_actually_works() {
273 let wrapper = StorageBuilder::new(StorageProvider::S3(S3Config::default()))
274 .with_bucket("test-bucket")
275 .with_region("us-west-2")
276 .build()
277 .unwrap();
278
279 let url = wrapper
280 .put("k.txt", b"s3-via-builder", "text/plain")
281 .await
282 .unwrap();
283 assert!(url.starts_with("s3://test-bucket.us-west-2/"));
284
285 let data = wrapper.get("k.txt").await.unwrap();
286 assert_eq!(data, b"s3-via-builder");
287
288 assert!(wrapper.exists("k.txt").await.unwrap());
289 wrapper.delete("k.txt").await.unwrap();
290 assert!(!wrapper.exists("k.txt").await.unwrap());
291 }
292
293 #[cfg(not(feature = "real-cloud"))]
294 #[tokio::test]
295 async fn test_builder_aliyun_actually_works() {
296 let wrapper = StorageBuilder::new(StorageProvider::AliyunOss(AliyunConfig::default()))
297 .with_bucket("bucket")
298 .with_endpoint("oss-cn-hangzhou.aliyuncs.com")
299 .build()
300 .unwrap();
301
302 wrapper
303 .put("k", b"aliyun-via-builder", "text/plain")
304 .await
305 .unwrap();
306 assert_eq!(wrapper.get("k").await.unwrap(), b"aliyun-via-builder");
307 }
308
309 #[cfg(not(feature = "real-cloud"))]
310 #[tokio::test]
311 async fn test_builder_tencent_actually_works() {
312 let wrapper = StorageBuilder::new(StorageProvider::TencentCos(TencentConfig::default()))
313 .with_bucket("bucket")
314 .with_region("ap-guangzhou")
315 .build()
316 .unwrap();
317
318 wrapper
319 .put("k", b"tencent-via-builder", "text/plain")
320 .await
321 .unwrap();
322 assert_eq!(wrapper.get("k").await.unwrap(), b"tencent-via-builder");
323 assert!(wrapper.exists("k").await.unwrap());
324 }
325
326 #[cfg(not(feature = "real-cloud"))]
327 #[tokio::test]
328 async fn test_builder_qiniu_actually_works() {
329 let wrapper = StorageBuilder::new(StorageProvider::QiniuKodo(QiniuConfig::default()))
330 .with_bucket("bucket")
331 .build()
332 .unwrap();
333
334 wrapper
335 .put("k", b"qiniu-via-builder", "text/plain")
336 .await
337 .unwrap();
338 assert_eq!(wrapper.get("k").await.unwrap(), b"qiniu-via-builder");
339 }
340
341 #[cfg(not(feature = "real-cloud"))]
342 #[tokio::test]
343 async fn test_builder_huawei_actually_works() {
344 let wrapper = StorageBuilder::new(StorageProvider::HuaweiObs(HuaweiConfig::default()))
345 .with_bucket("bucket")
346 .with_endpoint("obs.cn-north-4.myhuaweicloud.com")
347 .build()
348 .unwrap();
349
350 wrapper
351 .put("k", b"huawei-via-builder", "text/plain")
352 .await
353 .unwrap();
354 assert_eq!(wrapper.get("k").await.unwrap(), b"huawei-via-builder");
355 }
356
357 #[cfg(not(feature = "real-cloud"))]
358 #[tokio::test]
359 async fn test_builder_upyun_actually_works() {
360 let wrapper = StorageBuilder::new(StorageProvider::UpYun(UpYunConfig::default()))
361 .with_bucket("bucket")
362 .build()
363 .unwrap();
364
365 wrapper
366 .put("k", b"upyun-via-builder", "text/plain")
367 .await
368 .unwrap();
369 assert_eq!(wrapper.get("k").await.unwrap(), b"upyun-via-builder");
370 }
371
372 #[tokio::test]
373 async fn test_storage_wrapper_put() {
374 let temp_dir = test_data_base().join(format!("storage_test_{}", uuid_simple()));
375 let wrapper = StorageWrapper::Local(LocalStorage::new(temp_dir.to_string_lossy()));
376
377 let url = wrapper
378 .put("test.txt", b"data", "text/plain")
379 .await
380 .unwrap();
381 assert!(url.starts_with("local://"));
382
383 tokio::fs::remove_dir_all(&temp_dir).await.ok();
384 }
385
386 #[tokio::test]
387 async fn test_storage_wrapper_get() {
388 let temp_dir = test_data_base().join(format!("storage_test_{}", uuid_simple()));
389 let wrapper = StorageWrapper::Local(LocalStorage::new(temp_dir.to_string_lossy()));
390
391 wrapper
392 .put("test.txt", b"data", "text/plain")
393 .await
394 .unwrap();
395 let data = wrapper.get("test.txt").await.unwrap();
396 assert_eq!(data, b"data");
397
398 tokio::fs::remove_dir_all(&temp_dir).await.ok();
399 }
400
401 #[tokio::test]
402 async fn test_storage_config_default() {
403 let config = StorageConfig::default();
404 assert_eq!(config.bucket, "default-bucket");
405 assert_eq!(config.region, "us-east-1");
406 }
407
408 #[test]
409 fn test_storage_config_debug_masks_secrets() {
410 let config = StorageConfig {
411 bucket: "my-bucket".to_string(),
412 region: "us-east-1".to_string(),
413 endpoint: Some("https://example.com".to_string()),
414 access_key: Some("AKIAIOSFODNN7EXAMPLE".to_string()),
415 secret_key: Some("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string()),
416 path_prefix: Some("prefix/".to_string()),
417 base_path: Some("/tmp".to_string()),
418 };
419
420 let debug_output = format!("{:?}", config);
421
422 assert!(debug_output.contains("my-bucket"));
424 assert!(debug_output.contains("us-east-1"));
425 assert!(debug_output.contains("\"***\""));
427 assert!(!debug_output.contains("AKIAIOSFODNN7EXAMPLE"));
428 assert!(!debug_output.contains("wJalrXUtnFEMI"));
429 }
430
431 #[cfg(feature = "real-cloud")]
432 #[tokio::test]
433 async fn test_real_cloud_requires_credentials() {
434 for provider in [
436 StorageProvider::AliyunOss(AliyunConfig::default()),
437 StorageProvider::TencentCos(TencentConfig::default()),
438 StorageProvider::QiniuKodo(QiniuConfig::default()),
439 StorageProvider::HuaweiObs(HuaweiConfig::default()),
440 StorageProvider::UpYun(UpYunConfig::default()),
441 ] {
442 let result = StorageBuilder::new(provider)
443 .with_bucket("bucket")
444 .with_region("ap-guangzhou")
445 .with_endpoint("oss-cn-hangzhou.aliyuncs.com")
446 .build();
447 assert!(
448 matches!(result, Err(StorageError::InvalidConfig(_))),
449 "缺少凭据时应返回 InvalidConfig"
450 );
451 }
452 }
453
454 fn uuid_simple() -> String {
455 use std::time::{SystemTime, UNIX_EPOCH};
456 let now = SystemTime::now()
457 .duration_since(UNIX_EPOCH)
458 .unwrap_or_default()
459 .as_nanos();
460 format!("{:x}", now)
461 }
462}