zerolaunch_plugin_api/services/storage/
local_storage.rs1use 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
7pub struct LocalStorageService {
10 target_dir: PathBuf,
12}
13
14impl LocalStorageService {
15 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 async fn upload(&self, file_name: &str, data: &[u8]) -> Result<(), StorageError> {
29 let target_path = self.target_dir.join(file_name);
30
31 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 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 fn target_dir_path(&self) -> String {
71 self.target_dir.to_str().unwrap_or(".").to_string()
72 }
73
74 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 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 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 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 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 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 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}