Skip to main content

zerolaunch_plugin_api/services/storage/
webdav_storage.rs

1use 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
8/// WebDAV 连接配置。
9/// 用于创建 WebDAVStorageService 实例。
10pub struct WebDAVConfig {
11    /// WebDAV 服务器地址
12    pub host_url: String,
13    /// 认证账号
14    pub account: String,
15    /// 认证密码
16    pub password: String,
17    /// 远程目标目录
18    pub destination_dir: String,
19}
20
21/// WebDAV 远程存储服务。
22/// 通过 WebDAV 协议将文件存储到远程服务器,使用 reqwest_dav 实现(跨平台)。
23pub struct WebDAVStorageService {
24    /// 远程目标目录
25    destination_dir: PathBuf,
26    /// WebDAV 客户端
27    client: Option<Client>,
28}
29
30impl WebDAVStorageService {
31    /// 创建 WebDAVStorageService。
32    /// 参数:config - WebDAV 连接配置。
33    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    /// 将数据上传到 WebDAV 服务器。
53    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    /// 从 WebDAV 服务器下载数据。
78    /// 文件不存在(404)时返回 Ok(None)。
79    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                // 404 表示文件不存在,返回 None
105                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    /// 获取 WebDAV 存储的目标目录路径。
122    fn target_dir_path(&self) -> String {
123        self.destination_dir.to_str().unwrap_or("").to_string()
124    }
125
126    /// 从 WebDAV 服务器删除文件。
127    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    /// 列出 WebDAV 服务器上指定前缀下的所有文件。
152    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    /// 验证 WebDAV 存储配置是否有效。
188    /// 尝试写入并读取测试文件来验证。
189    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    /// WebDAV 集成测试服务器地址(bun fixture 固定监听端口)。
215    const SERVER_URL: &str = "http://127.0.0.1:18080";
216
217    /// 启动 bun WebDAV 测试服务器(tests/fixtures/webdav_server.ts)。
218    /// 轮询 OPTIONS 直至就绪,超时 panic。
219    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    /// 轮询等待服务器就绪(OPTIONS 返回 200),单次请求 2 秒超时。
233    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    /// 构造指向测试服务器的 WebDAVStorageService(目标目录为根)。
254    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    /// WebDAV 存储服务端到端契约:upload/download/delete/list/validate 全链路。
264    ///
265    /// 依赖 bun 与 tests/fixtures/webdav_server.ts;整体 30 秒超时保护,
266    /// 服务器进程通过 shutdown 端点优雅退出(兜底 kill 进程树)。
267    /// 默认忽略:需要本地起 WebDAV 测试服务器,且走代理时可能超时。
268    #[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            // 上传 → 下载往返,内容一致
280            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            // 不存在文件 → Ok(None) 而非错误(404 语义)
291            assert_eq!(
292                svc.download("not-exist.json").await.expect("下载失败"),
293                None,
294                "不存在的文件应返回 None"
295            );
296
297            // 删除 → 再下载为 None
298            svc.delete("remote/config.json").await.expect("删除失败");
299            assert_eq!(
300                svc.download("remote/config.json").await.expect("下载失败"),
301                None,
302                "删除后文件应不可下载"
303            );
304
305            // 列表:前缀目录下仅返回文件(过滤目录)
306            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            // validate:上传+下载测试文件往返成功
321            assert!(svc.validate().await, "validate 上传下载往返应成功");
322
323            // 清理服务器端残留
324            svc.delete("dir/a.txt").await.expect("清理失败");
325            svc.delete("dir/b.txt").await.expect("清理失败");
326
327            // 优雅关闭测试服务器(避免 bun 进程树残留)
328            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        // 等待服务器退出(轮询最多 5 秒),兜底杀进程树
341        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}