Skip to main content

sz_orm_storage/
local.rs

1use crate::error::StorageError;
2use crate::storage::Storage;
3use async_trait::async_trait;
4use std::path::PathBuf;
5
6pub struct LocalStorage {
7    pub base_path: String,
8}
9
10impl LocalStorage {
11    pub fn new(base_path: impl Into<String>) -> Self {
12        Self {
13            base_path: base_path.into(),
14        }
15    }
16
17    pub fn full_path(&self, key: &str) -> PathBuf {
18        PathBuf::from(&self.base_path).join(key)
19    }
20}
21
22#[async_trait]
23impl Storage for LocalStorage {
24    async fn put(
25        &self,
26        key: &str,
27        data: &[u8],
28        _content_type: &str,
29    ) -> Result<String, StorageError> {
30        let path = self.full_path(key);
31        if let Some(parent) = path.parent() {
32            tokio::fs::create_dir_all(parent).await?;
33        }
34        tokio::fs::write(&path, data).await?;
35        Ok(format!("local://{}", key))
36    }
37
38    async fn get(&self, key: &str) -> Result<Vec<u8>, StorageError> {
39        let path = self.full_path(key);
40        if !path.exists() {
41            return Err(StorageError::NotFound(key.to_string()));
42        }
43        tokio::fs::read(&path).await.map_err(StorageError::from)
44    }
45
46    async fn delete(&self, key: &str) -> Result<(), StorageError> {
47        let path = self.full_path(key);
48        if path.exists() {
49            tokio::fs::remove_file(&path).await?;
50        }
51        Ok(())
52    }
53
54    async fn exists(&self, key: &str) -> Result<bool, StorageError> {
55        Ok(self.full_path(key).exists())
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    /// 测试数据目录:优先 F:\test\data(用户规范),回退到环境变量或系统 temp(CI/Linux)
64    ///
65    /// 注意:仅检查目录存在不足以保证可用——还需验证可写性,
66    /// 以避免在受限沙箱环境中因目录存在但不可写导致测试失败。
67    fn test_data_base() -> std::path::PathBuf {
68        let f_drive = std::path::Path::new("F:\\test\\data");
69        if is_dir_writable(f_drive) {
70            return f_drive.to_path_buf();
71        }
72        if let Ok(dir) = std::env::var("SZ_ORM_TEST_DATA_DIR") {
73            let p = std::path::PathBuf::from(&dir);
74            if is_dir_writable(&p) {
75                return p;
76            }
77        }
78        std::env::temp_dir()
79    }
80
81    /// 检查目录是否存在且可写:尝试在其中创建并删除一个探测文件
82    fn is_dir_writable(dir: &std::path::Path) -> bool {
83        if !dir.exists() {
84            return false;
85        }
86        let probe = dir.join(format!(".probe_{}", std::process::id()));
87        match std::fs::File::create(&probe) {
88            Ok(_) => {
89                let _ = std::fs::remove_file(&probe);
90                true
91            }
92            Err(_) => false,
93        }
94    }
95
96    fn temp_dir() -> std::path::PathBuf {
97        let nanos = std::time::SystemTime::now()
98            .duration_since(std::time::UNIX_EPOCH)
99            .unwrap_or_default()
100            .as_nanos();
101        test_data_base().join(format!("local_storage_test_{:x}", nanos))
102    }
103
104    #[tokio::test]
105    async fn test_local_put_and_get() {
106        let dir = temp_dir();
107        let storage = LocalStorage::new(dir.to_string_lossy());
108
109        storage
110            .put("file.txt", b"hello", "text/plain")
111            .await
112            .unwrap();
113        let data = storage.get("file.txt").await.unwrap();
114        assert_eq!(data, b"hello");
115
116        tokio::fs::remove_dir_all(&dir).await.ok();
117    }
118
119    #[tokio::test]
120    async fn test_local_get_not_found() {
121        let dir = temp_dir();
122        let storage = LocalStorage::new(dir.to_string_lossy());
123
124        let result = storage.get("missing.txt").await;
125        assert!(result.is_err());
126        assert!(matches!(result.unwrap_err(), StorageError::NotFound(_)));
127
128        tokio::fs::remove_dir_all(&dir).await.ok();
129    }
130
131    #[tokio::test]
132    async fn test_local_delete() {
133        let dir = temp_dir();
134        let storage = LocalStorage::new(dir.to_string_lossy());
135
136        storage
137            .put("delete.txt", b"data", "text/plain")
138            .await
139            .unwrap();
140        assert!(storage.exists("delete.txt").await.unwrap());
141
142        storage.delete("delete.txt").await.unwrap();
143        assert!(!storage.exists("delete.txt").await.unwrap());
144
145        tokio::fs::remove_dir_all(&dir).await.ok();
146    }
147
148    #[tokio::test]
149    async fn test_local_exists_false_for_missing() {
150        let dir = temp_dir();
151        let storage = LocalStorage::new(dir.to_string_lossy());
152        assert!(!storage.exists("nope.txt").await.unwrap());
153        tokio::fs::remove_dir_all(&dir).await.ok();
154    }
155
156    #[tokio::test]
157    async fn test_local_creates_subdirectories() {
158        let dir = temp_dir();
159        let storage = LocalStorage::new(dir.to_string_lossy());
160
161        storage
162            .put("nested/deep/file.txt", b"nested", "text/plain")
163            .await
164            .unwrap();
165        let data = storage.get("nested/deep/file.txt").await.unwrap();
166        assert_eq!(data, b"nested");
167
168        tokio::fs::remove_dir_all(&dir).await.ok();
169    }
170
171    #[tokio::test]
172    async fn test_local_put_returns_url() {
173        let dir = temp_dir();
174        let storage = LocalStorage::new(dir.to_string_lossy());
175
176        let url = storage.put("url.txt", b"data", "text/plain").await.unwrap();
177        assert!(url.starts_with("local://"));
178        assert!(url.contains("url.txt"));
179
180        tokio::fs::remove_dir_all(&dir).await.ok();
181    }
182
183    #[tokio::test]
184    async fn test_local_overwrite() {
185        let dir = temp_dir();
186        let storage = LocalStorage::new(dir.to_string_lossy());
187
188        storage
189            .put("overwrite.txt", b"v1", "text/plain")
190            .await
191            .unwrap();
192        storage
193            .put("overwrite.txt", b"v2", "text/plain")
194            .await
195            .unwrap();
196        let data = storage.get("overwrite.txt").await.unwrap();
197        assert_eq!(data, b"v2");
198
199        tokio::fs::remove_dir_all(&dir).await.ok();
200    }
201}