Skip to main content

sz_orm_storage/
upyun.rs

1//! # UpYun Storage(**MOCK-ONLY,非生产可用**)
2//!
3//! ⚠️ **重要警告:本模块为内存 Mock 实现,未集成真实又拍云 SDK。**
4//!
5//! - 所有数据存储在进程内 `HashMap`,重启即丢失
6//! - 不执行任何 HTTP 请求,不与真实又拍云服务交互
7//! - 不支持认证、签名、TLS、分片上传等任何又拍云生产特性
8//! - **请勿用于生产环境**——仅适用于单元测试与本地开发
9//!
10//! 如需真实又拍云集成,请基于 [`crate::storage::Storage`] trait
11//! 接入又拍云官方 SDK 实现。
12
13use crate::error::StorageError;
14use crate::storage::Storage;
15use async_trait::async_trait;
16use std::collections::HashMap;
17use std::sync::Arc;
18use tokio::sync::RwLock;
19
20/// 又拍云存储后端(**Mock 实现**)
21///
22/// ⚠️ 仅用于测试。所有数据存储在内存 `HashMap`,不与真实又拍云服务交互。
23/// 如需生产使用,请实现 `Storage` trait 接入官方 SDK。
24pub struct UpYunStorage {
25    pub bucket: String,
26    store: Arc<RwLock<HashMap<String, Vec<u8>>>>,
27}
28
29impl UpYunStorage {
30    pub fn new(bucket: impl Into<String>) -> Self {
31        Self {
32            bucket: bucket.into(),
33            store: Arc::new(RwLock::new(HashMap::new())),
34        }
35    }
36
37    pub fn url_for(&self, key: &str) -> String {
38        format!("upyun://{}/{}", self.bucket, key)
39    }
40}
41
42#[async_trait]
43impl Storage for UpYunStorage {
44    async fn put(
45        &self,
46        key: &str,
47        data: &[u8],
48        _content_type: &str,
49    ) -> Result<String, StorageError> {
50        let mut store = self.store.write().await;
51        store.insert(key.to_string(), data.to_vec());
52        Ok(self.url_for(key))
53    }
54
55    async fn get(&self, key: &str) -> Result<Vec<u8>, StorageError> {
56        let store = self.store.read().await;
57        store
58            .get(key)
59            .cloned()
60            .ok_or_else(|| StorageError::NotFound(format!("upyun://{}/{}", self.bucket, key)))
61    }
62
63    async fn delete(&self, key: &str) -> Result<(), StorageError> {
64        let mut store = self.store.write().await;
65        store.remove(key);
66        Ok(())
67    }
68
69    async fn exists(&self, key: &str) -> Result<bool, StorageError> {
70        let store = self.store.read().await;
71        Ok(store.contains_key(key))
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[tokio::test]
80    async fn test_upyun_put_and_get() {
81        let storage = UpYunStorage::new("my-bucket");
82        let url = storage
83            .put("file.txt", b"upyun-data", "text/plain")
84            .await
85            .unwrap();
86        assert!(url.starts_with("upyun://my-bucket/"));
87        assert!(url.contains("file.txt"));
88
89        let data = storage.get("file.txt").await.unwrap();
90        assert_eq!(data, b"upyun-data");
91    }
92
93    #[tokio::test]
94    async fn test_upyun_get_not_found() {
95        let storage = UpYunStorage::new("bucket");
96        let result = storage.get("missing").await;
97        assert!(result.is_err());
98        assert!(matches!(result.unwrap_err(), StorageError::NotFound(_)));
99    }
100
101    #[tokio::test]
102    async fn test_upyun_delete_and_exists() {
103        let storage = UpYunStorage::new("bucket");
104        storage.put("key", b"data", "text/plain").await.unwrap();
105        assert!(storage.exists("key").await.unwrap());
106
107        storage.delete("key").await.unwrap();
108        assert!(!storage.exists("key").await.unwrap());
109    }
110
111    #[tokio::test]
112    async fn test_upyun_overwrite() {
113        let storage = UpYunStorage::new("bucket");
114        storage.put("key", b"v1", "text/plain").await.unwrap();
115        storage.put("key", b"v2", "text/plain").await.unwrap();
116        assert_eq!(storage.get("key").await.unwrap(), b"v2");
117    }
118
119    #[tokio::test]
120    async fn test_upyun_url_format() {
121        let storage = UpYunStorage::new("my-bucket");
122        assert_eq!(storage.url_for("file.txt"), "upyun://my-bucket/file.txt");
123        assert_eq!(
124            storage.url_for("dir/file.txt"),
125            "upyun://my-bucket/dir/file.txt"
126        );
127    }
128}