zerolaunch_plugin_api/services/storage/
webdav_storage.rs1use crate::services::storage::storage_error::StorageError;
2use crate::services::storage::storage_service::StorageService;
3use async_trait::async_trait;
4use reqwest_dav::{Client, ClientBuilder};
5use std::path::PathBuf;
6use tracing::{debug, warn};
7
8pub struct WebDAVConfig {
11 pub host_url: String,
13 pub account: String,
15 pub password: String,
17 pub destination_dir: String,
19}
20
21pub struct WebDAVStorageService {
24 destination_dir: PathBuf,
26 client: Option<Client>,
28}
29
30impl WebDAVStorageService {
31 pub fn new(config: &WebDAVConfig) -> Self {
34 let client = ClientBuilder::new()
35 .set_host(config.host_url.clone())
36 .set_auth(reqwest_dav::Auth::Basic(
37 config.account.clone(),
38 config.password.clone(),
39 ))
40 .build()
41 .ok();
42
43 Self {
44 destination_dir: PathBuf::from(&config.destination_dir),
45 client,
46 }
47 }
48}
49
50#[async_trait]
51impl StorageService for WebDAVStorageService {
52 async fn upload(&self, file_name: &str, data: &[u8]) -> Result<(), StorageError> {
54 let target_path = self.destination_dir.join(file_name);
55 let target_path_str = target_path
56 .to_str()
57 .ok_or_else(|| StorageError::InvalidPath(file_name.to_string()))?
58 .to_string();
59
60 let client = self
61 .client
62 .as_ref()
63 .ok_or(StorageError::ClientNotInitialized)?;
64
65 client
66 .put(&target_path_str, data.to_vec())
67 .await
68 .map_err(|e| StorageError::UploadFailed {
69 file: file_name.to_string(),
70 reason: e.to_string(),
71 })?;
72
73 debug!("WebDAV 上传完成: {}", file_name);
74 Ok(())
75 }
76
77 async fn download(&self, file_name: &str) -> Result<Option<Vec<u8>>, StorageError> {
80 let target_path = self.destination_dir.join(file_name);
81 let target_path_str = target_path
82 .to_str()
83 .ok_or_else(|| StorageError::InvalidPath(file_name.to_string()))?
84 .to_string();
85
86 let client = self
87 .client
88 .as_ref()
89 .ok_or(StorageError::ClientNotInitialized)?;
90
91 match client.get(&target_path_str).await {
92 Ok(response) => {
93 let bytes = response
94 .bytes()
95 .await
96 .map_err(|e| StorageError::DownloadFailed {
97 file: file_name.to_string(),
98 reason: format!("读取文件流失败: {}", e),
99 })?;
100 debug!("WebDAV 下载完成: {}, {} bytes", file_name, bytes.len());
101 Ok(Some(bytes.to_vec()))
102 }
103 Err(e) => {
104 if let reqwest_dav::Error::Decode(reqwest_dav::DecodeError::Server(server_error)) =
106 &e
107 {
108 if server_error.response_code == 404 {
109 debug!("WebDAV 文件不存在: {}", file_name);
110 return Ok(None);
111 }
112 }
113 Err(StorageError::DownloadFailed {
114 file: file_name.to_string(),
115 reason: format!("{:?}", e),
116 })
117 }
118 }
119 }
120
121 fn target_dir_path(&self) -> String {
123 self.destination_dir.to_str().unwrap_or("").to_string()
124 }
125
126 async fn delete(&self, file_name: &str) -> Result<(), StorageError> {
128 let target_path = self.destination_dir.join(file_name);
129 let target_path_str = target_path
130 .to_str()
131 .ok_or_else(|| StorageError::InvalidPath(file_name.to_string()))?
132 .to_string();
133
134 let client = self
135 .client
136 .as_ref()
137 .ok_or(StorageError::ClientNotInitialized)?;
138
139 client
140 .delete(&target_path_str)
141 .await
142 .map_err(|e| StorageError::DeleteFailed {
143 file: file_name.to_string(),
144 reason: format!("{:?}", e),
145 })?;
146
147 debug!("WebDAV 删除完成: {}", file_name);
148 Ok(())
149 }
150
151 async fn list(&self, prefix: &str) -> Result<Vec<String>, StorageError> {
153 let target_path = self.destination_dir.join(prefix);
154 let target_path_str = target_path
155 .to_str()
156 .ok_or_else(|| StorageError::InvalidPath(prefix.to_string()))?
157 .to_string();
158
159 let client = self
160 .client
161 .as_ref()
162 .ok_or(StorageError::ClientNotInitialized)?;
163
164 let entries = client
165 .list(&target_path_str, reqwest_dav::Depth::Number(1))
166 .await
167 .map_err(|e| StorageError::ListFailed {
168 prefix: prefix.to_string(),
169 reason: format!("{:?}", e),
170 })?;
171
172 let files: Vec<String> = entries
173 .into_iter()
174 .filter_map(|e| match e {
175 reqwest_dav::list_cmd::ListEntity::File(f) => {
176 let name = f.href.rsplit('/').next().map(|s| s.to_string());
177 name
178 }
179 reqwest_dav::list_cmd::ListEntity::Folder(_) => None,
180 })
181 .collect();
182
183 debug!("WebDAV 列表完成: {} ({})", prefix, files.len());
184 Ok(files)
185 }
186
187 async fn validate(&self) -> bool {
190 let test_file = "__zerolaunch_storage_test__.txt";
191 let test_data = b"ZeroLaunch storage validation test";
192
193 if self.upload(test_file, test_data).await.is_err() {
194 warn!("WebDAV 验证上传失败");
195 return false;
196 }
197
198 if self.download(test_file).await.is_err() {
199 warn!("WebDAV 验证下载失败");
200 return false;
201 }
202
203 true
204 }
205}
206
207#[cfg(all(test, feature = "webdav"))]
208mod tests {
209 use super::*;
210 use std::path::PathBuf;
211 use std::process::{Child, Stdio};
212 use std::time::Duration;
213
214 const SERVER_URL: &str = "http://127.0.0.1:18080";
216
217 fn start_server() -> Child {
220 let script =
221 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/webdav_server.ts");
222 let child = std::process::Command::new("bun")
223 .arg("run")
224 .arg(&script)
225 .stdout(Stdio::null())
226 .stderr(Stdio::null())
227 .spawn()
228 .expect("启动 WebDAV 测试服务器失败(需要 bun 可执行文件)");
229 child
230 }
231
232 async fn wait_ready() {
234 for _ in 0..25 {
235 let client = reqwest::Client::builder()
236 .timeout(Duration::from_secs(2))
237 .build()
238 .expect("创建 reqwest 客户端失败");
239 if client
240 .request(reqwest::Method::OPTIONS, SERVER_URL)
241 .send()
242 .await
243 .map(|r| r.status().is_success())
244 .unwrap_or(false)
245 {
246 return;
247 }
248 tokio::time::sleep(Duration::from_millis(200)).await;
249 }
250 panic!("WebDAV 测试服务器启动超时(5 秒)");
251 }
252
253 fn make_service() -> WebDAVStorageService {
255 WebDAVStorageService::new(&WebDAVConfig {
256 host_url: SERVER_URL.into(),
257 account: "test".into(),
258 password: "test".into(),
259 destination_dir: "/".into(),
260 })
261 }
262
263 #[ignore]
269 #[tokio::test]
270 async fn webdav_storage_full_roundtrip() {
271 let mut child = start_server();
272
273 let assertions = tokio::time::timeout(Duration::from_secs(30), async {
274 wait_ready().await;
275
276 let svc = make_service();
277 assert_eq!(svc.target_dir_path(), "/", "目标目录应返回 destination_dir");
278
279 svc.upload("remote/config.json", br#"{"a":1}"#)
281 .await
282 .expect("上传失败");
283 let data = svc
284 .download("remote/config.json")
285 .await
286 .expect("下载失败")
287 .expect("上传的文件应可下载");
288 assert_eq!(data, br#"{"a":1}"#);
289
290 assert_eq!(
292 svc.download("not-exist.json").await.expect("下载失败"),
293 None,
294 "不存在的文件应返回 None"
295 );
296
297 svc.delete("remote/config.json").await.expect("删除失败");
299 assert_eq!(
300 svc.download("remote/config.json").await.expect("下载失败"),
301 None,
302 "删除后文件应不可下载"
303 );
304
305 svc.upload("dir/a.txt", b"a").await.expect("上传失败");
307 svc.upload("dir/b.txt", b"b").await.expect("上传失败");
308 let files = svc.list("dir").await.expect("列表失败");
309 assert!(
310 files.contains(&"a.txt".to_string()),
311 "列表应含 a.txt: {:?}",
312 files
313 );
314 assert!(
315 files.contains(&"b.txt".to_string()),
316 "列表应含 b.txt: {:?}",
317 files
318 );
319
320 assert!(svc.validate().await, "validate 上传下载往返应成功");
322
323 svc.delete("dir/a.txt").await.expect("清理失败");
325 svc.delete("dir/b.txt").await.expect("清理失败");
326
327 let _ = reqwest::Client::builder()
329 .timeout(Duration::from_secs(2))
330 .build()
331 .expect("创建 reqwest 客户端失败")
332 .post(format!("{SERVER_URL}/__shutdown"))
333 .send()
334 .await;
335 Ok::<(), ()>(())
336 })
337 .await
338 .expect("WebDAV 端到端断言超时(30 秒)");
339
340 for _ in 0..50 {
342 if child.try_wait().ok().flatten().is_some() {
343 break;
344 }
345 tokio::time::sleep(Duration::from_millis(100)).await;
346 }
347 if child.try_wait().ok().flatten().is_none() {
348 #[cfg(windows)]
349 {
350 let _ = std::process::Command::new("taskkill")
351 .args(["/PID", &child.id().to_string(), "/T", "/F"])
352 .output();
353 }
354 #[cfg(not(windows))]
355 {
356 let _ = child.kill();
357 }
358 }
359
360 assertions.expect("WebDAV 端到端断言失败");
361 }
362}