Skip to main content

zerolaunch_plugin_api/services/storage/
local_storage.rs

1use crate::services::storage::storage_error::StorageError;
2use crate::services::storage::storage_service::StorageService;
3use async_trait::async_trait;
4use std::path::PathBuf;
5use tracing::debug;
6
7/// 本地文件系统存储服务。
8/// 将文件存储到本地目录,使用 tokio::fs 实现(跨平台)。
9pub struct LocalStorageService {
10    /// 存储目标目录
11    target_dir: PathBuf,
12}
13
14impl LocalStorageService {
15    /// 创建 LocalStorageService。
16    /// 参数:target_dir - 存储目标目录路径。
17    pub fn new(target_dir: impl Into<PathBuf>) -> Self {
18        Self {
19            target_dir: target_dir.into(),
20        }
21    }
22}
23
24#[async_trait]
25impl StorageService for LocalStorageService {
26    /// 将数据上传到本地文件系统。
27    /// 在目标目录下创建文件并写入数据,自动创建父目录。
28    async fn upload(&self, file_name: &str, data: &[u8]) -> Result<(), StorageError> {
29        let target_path = self.target_dir.join(file_name);
30
31        // 确保父目录存在
32        if let Some(parent) = target_path.parent() {
33            tokio::fs::create_dir_all(parent).await?;
34        }
35
36        tokio::fs::write(&target_path, data)
37            .await
38            .map_err(|e| StorageError::UploadFailed {
39                file: file_name.to_string(),
40                reason: format!("写入文件失败: {}", e),
41            })?;
42
43        debug!("本地上传完成: {}", file_name);
44        Ok(())
45    }
46
47    /// 从本地文件系统下载数据。
48    /// 文件不存在时返回 Ok(None)。
49    async fn download(&self, file_name: &str) -> Result<Option<Vec<u8>>, StorageError> {
50        let target_path = self.target_dir.join(file_name);
51
52        if !target_path.exists() {
53            debug!("本地文件不存在: {}", file_name);
54            return Ok(None);
55        }
56
57        let data =
58            tokio::fs::read(&target_path)
59                .await
60                .map_err(|e| StorageError::DownloadFailed {
61                    file: file_name.to_string(),
62                    reason: format!("读取文件失败: {}", e),
63                })?;
64
65        debug!("本地下载完成: {}, {} bytes", file_name, data.len());
66        Ok(Some(data))
67    }
68
69    /// 获取本地存储的目标目录路径。
70    fn target_dir_path(&self) -> String {
71        self.target_dir.to_str().unwrap_or(".").to_string()
72    }
73
74    /// 从本地文件系统删除文件。
75    async fn delete(&self, file_name: &str) -> Result<(), StorageError> {
76        let target_path = self.target_dir.join(file_name);
77        if !target_path.exists() {
78            debug!("本地文件不存在,跳过删除: {}", file_name);
79            return Ok(());
80        }
81        tokio::fs::remove_file(&target_path)
82            .await
83            .map_err(|e| StorageError::DeleteFailed {
84                file: file_name.to_string(),
85                reason: format!("删除文件失败: {}", e),
86            })?;
87        debug!("本地删除完成: {}", file_name);
88        Ok(())
89    }
90
91    /// 列出本地目录中指定前缀下的所有文件。
92    async fn list(&self, prefix: &str) -> Result<Vec<String>, StorageError> {
93        let target_dir = self.target_dir.join(prefix);
94        if !target_dir.exists() {
95            return Ok(Vec::new());
96        }
97        let mut entries =
98            tokio::fs::read_dir(&target_dir)
99                .await
100                .map_err(|e| StorageError::ListFailed {
101                    prefix: prefix.to_string(),
102                    reason: format!("读取目录失败: {}", e),
103                })?;
104        let mut files = Vec::new();
105        while let Some(entry) =
106            entries
107                .next_entry()
108                .await
109                .map_err(|e| StorageError::ListFailed {
110                    prefix: prefix.to_string(),
111                    reason: format!("遍历目录失败: {}", e),
112                })?
113        {
114            if entry
115                .file_type()
116                .await
117                .map(|t| t.is_file())
118                .unwrap_or(false)
119            {
120                if let Some(name) = entry.file_name().to_str() {
121                    files.push(name.to_string());
122                }
123            }
124        }
125        debug!("本地列表完成: {} ({})", prefix, files.len());
126        Ok(files)
127    }
128
129    /// 验证本地存储配置是否有效。
130    /// 尝试写入并读取测试文件来验证。
131    async fn validate(&self) -> bool {
132        let test_file = "__zerolaunch_storage_test__.txt";
133        let test_data = b"ZeroLaunch storage validation test";
134
135        if self.upload(test_file, test_data).await.is_err() {
136            return false;
137        }
138
139        if self.download(test_file).await.is_err() {
140            return false;
141        }
142
143        // 清理测试文件
144        let test_path = self.target_dir.join(test_file);
145        let _ = tokio::fs::remove_file(&test_path).await;
146
147        true
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    /// 创建指向临时目录的 LocalStorageService(测试目录自动清理)。
156    fn temp_service() -> (LocalStorageService, tempfile::TempDir) {
157        let dir = tempfile::tempdir().expect("创建临时目录失败");
158        let service = LocalStorageService::new(dir.path());
159        (service, dir)
160    }
161
162    #[tokio::test]
163    async fn upload_creates_file_with_content() {
164        let (service, dir) = temp_service();
165        service
166            .upload("config.json", br#"{"a":1}"#)
167            .await
168            .expect("上传失败");
169        let path = dir.path().join("config.json");
170        assert!(path.exists(), "上传后文件应存在于目标目录");
171        assert_eq!(std::fs::read_to_string(path).unwrap(), r#"{"a":1}"#);
172    }
173
174    #[tokio::test]
175    async fn upload_creates_nested_parent_dirs() {
176        let (service, dir) = temp_service();
177        service
178            .upload("sub/dir/config.json", b"data")
179            .await
180            .expect("上传失败");
181        assert!(dir.path().join("sub/dir/config.json").exists());
182    }
183
184    #[tokio::test]
185    async fn download_returns_content_when_exists() {
186        let (service, dir) = temp_service();
187        std::fs::create_dir_all(dir.path()).unwrap();
188        std::fs::write(dir.path().join("a.txt"), "hello").unwrap();
189        let data = service.download("a.txt").await.expect("下载失败");
190        assert_eq!(data, Some(b"hello".to_vec()));
191    }
192
193    #[tokio::test]
194    async fn download_returns_none_when_missing() {
195        let (service, _dir) = temp_service();
196        let data = service.download("missing.txt").await.expect("下载失败");
197        assert_eq!(data, None, "不存在的文件应返回 None 而非错误");
198    }
199
200    #[tokio::test]
201    async fn delete_removes_file_and_is_idempotent() {
202        let (service, dir) = temp_service();
203        std::fs::write(dir.path().join("d.txt"), "x").unwrap();
204        service.delete("d.txt").await.expect("删除失败");
205        assert!(!dir.path().join("d.txt").exists());
206        // 再次删除不存在文件应幂等成功
207        service.delete("d.txt").await.expect("重复删除应幂等");
208    }
209
210    #[tokio::test]
211    async fn list_filters_by_prefix_and_files_only() {
212        let (service, dir) = temp_service();
213        std::fs::create_dir_all(dir.path().join("a")).unwrap();
214        std::fs::write(dir.path().join("a/1.txt"), "").unwrap();
215        std::fs::write(dir.path().join("a/2.txt"), "").unwrap();
216        std::fs::write(dir.path().join("a/sub"), "").unwrap();
217        let files = service.list("a").await.expect("列表失败");
218        assert_eq!(files.len(), 3);
219        assert!(files.contains(&"1.txt".to_string()));
220        assert!(files.contains(&"2.txt".to_string()));
221        // 不存在的前缀返回空列表
222        let empty = service.list("nonexistent").await.expect("列表失败");
223        assert!(empty.is_empty());
224    }
225
226    #[tokio::test]
227    async fn validate_roundtrip_and_cleanup() {
228        let (service, dir) = temp_service();
229        assert!(service.validate().await, "本地上传下载往返应成功");
230        assert!(
231            !dir.path().join("__zerolaunch_storage_test__.txt").exists(),
232            "validate 应清理测试文件"
233        );
234    }
235
236    #[tokio::test]
237    async fn target_dir_path_matches() {
238        let dir = tempfile::tempdir().expect("创建临时目录失败");
239        let service = LocalStorageService::new(dir.path());
240        assert_eq!(
241            service.target_dir_path(),
242            dir.path().to_str().unwrap(),
243            "target_dir_path 应返回存储目标目录"
244        );
245    }
246}