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