use crate::services::storage::storage_error::StorageError;
use crate::services::storage::storage_service::StorageService;
use async_trait::async_trait;
use reqwest_dav::{Client, ClientBuilder};
use std::path::PathBuf;
use tracing::{debug, warn};
pub struct WebDAVConfig {
pub host_url: String,
pub account: String,
pub password: String,
pub destination_dir: String,
}
pub struct WebDAVStorageService {
destination_dir: PathBuf,
client: Option<Client>,
}
impl WebDAVStorageService {
pub fn new(config: &WebDAVConfig) -> Self {
let client = ClientBuilder::new()
.set_host(config.host_url.clone())
.set_auth(reqwest_dav::Auth::Basic(
config.account.clone(),
config.password.clone(),
))
.build()
.ok();
Self {
destination_dir: PathBuf::from(&config.destination_dir),
client,
}
}
}
#[async_trait]
impl StorageService for WebDAVStorageService {
async fn upload(&self, file_name: &str, data: &[u8]) -> Result<(), StorageError> {
let target_path = self.destination_dir.join(file_name);
let target_path_str = target_path
.to_str()
.ok_or_else(|| StorageError::InvalidPath(file_name.to_string()))?
.to_string();
let client = self
.client
.as_ref()
.ok_or(StorageError::ClientNotInitialized)?;
client
.put(&target_path_str, data.to_vec())
.await
.map_err(|e| StorageError::UploadFailed {
file: file_name.to_string(),
reason: e.to_string(),
})?;
debug!("WebDAV 上传完成: {}", file_name);
Ok(())
}
async fn download(&self, file_name: &str) -> Result<Option<Vec<u8>>, StorageError> {
let target_path = self.destination_dir.join(file_name);
let target_path_str = target_path
.to_str()
.ok_or_else(|| StorageError::InvalidPath(file_name.to_string()))?
.to_string();
let client = self
.client
.as_ref()
.ok_or(StorageError::ClientNotInitialized)?;
match client.get(&target_path_str).await {
Ok(response) => {
let bytes = response
.bytes()
.await
.map_err(|e| StorageError::DownloadFailed {
file: file_name.to_string(),
reason: format!("读取文件流失败: {}", e),
})?;
debug!("WebDAV 下载完成: {}, {} bytes", file_name, bytes.len());
Ok(Some(bytes.to_vec()))
}
Err(e) => {
if let reqwest_dav::Error::Decode(reqwest_dav::DecodeError::Server(server_error)) =
&e
{
if server_error.response_code == 404 {
debug!("WebDAV 文件不存在: {}", file_name);
return Ok(None);
}
}
Err(StorageError::DownloadFailed {
file: file_name.to_string(),
reason: format!("{:?}", e),
})
}
}
}
fn target_dir_path(&self) -> String {
self.destination_dir.to_str().unwrap_or("").to_string()
}
async fn delete(&self, file_name: &str) -> Result<(), StorageError> {
let target_path = self.destination_dir.join(file_name);
let target_path_str = target_path
.to_str()
.ok_or_else(|| StorageError::InvalidPath(file_name.to_string()))?
.to_string();
let client = self
.client
.as_ref()
.ok_or(StorageError::ClientNotInitialized)?;
client
.delete(&target_path_str)
.await
.map_err(|e| StorageError::DeleteFailed {
file: file_name.to_string(),
reason: format!("{:?}", e),
})?;
debug!("WebDAV 删除完成: {}", file_name);
Ok(())
}
async fn list(&self, prefix: &str) -> Result<Vec<String>, StorageError> {
let target_path = self.destination_dir.join(prefix);
let target_path_str = target_path
.to_str()
.ok_or_else(|| StorageError::InvalidPath(prefix.to_string()))?
.to_string();
let client = self
.client
.as_ref()
.ok_or(StorageError::ClientNotInitialized)?;
let entries = client
.list(&target_path_str, reqwest_dav::Depth::Number(1))
.await
.map_err(|e| StorageError::ListFailed {
prefix: prefix.to_string(),
reason: format!("{:?}", e),
})?;
let files: Vec<String> = entries
.into_iter()
.filter_map(|e| match e {
reqwest_dav::list_cmd::ListEntity::File(f) => {
let name = f.href.rsplit('/').next().map(|s| s.to_string());
name
}
reqwest_dav::list_cmd::ListEntity::Folder(_) => None,
})
.collect();
debug!("WebDAV 列表完成: {} ({})", prefix, files.len());
Ok(files)
}
async fn validate(&self) -> bool {
let test_file = "__zerolaunch_storage_test__.txt";
let test_data = b"ZeroLaunch storage validation test";
if self.upload(test_file, test_data).await.is_err() {
warn!("WebDAV 验证上传失败");
return false;
}
if self.download(test_file).await.is_err() {
warn!("WebDAV 验证下载失败");
return false;
}
true
}
}
#[cfg(all(test, feature = "webdav"))]
mod tests {
use super::*;
use std::path::PathBuf;
use std::process::{Child, Stdio};
use std::time::Duration;
const SERVER_URL: &str = "http://127.0.0.1:18080";
fn start_server() -> Child {
let script =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/webdav_server.ts");
let child = std::process::Command::new("bun")
.arg("run")
.arg(&script)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("启动 WebDAV 测试服务器失败(需要 bun 可执行文件)");
child
}
async fn wait_ready() {
for _ in 0..25 {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(2))
.build()
.expect("创建 reqwest 客户端失败");
if client
.request(reqwest::Method::OPTIONS, SERVER_URL)
.send()
.await
.map(|r| r.status().is_success())
.unwrap_or(false)
{
return;
}
tokio::time::sleep(Duration::from_millis(200)).await;
}
panic!("WebDAV 测试服务器启动超时(5 秒)");
}
fn make_service() -> WebDAVStorageService {
WebDAVStorageService::new(&WebDAVConfig {
host_url: SERVER_URL.into(),
account: "test".into(),
password: "test".into(),
destination_dir: "/".into(),
})
}
#[ignore]
#[tokio::test]
async fn webdav_storage_full_roundtrip() {
let mut child = start_server();
let assertions = tokio::time::timeout(Duration::from_secs(30), async {
wait_ready().await;
let svc = make_service();
assert_eq!(svc.target_dir_path(), "/", "目标目录应返回 destination_dir");
svc.upload("remote/config.json", br#"{"a":1}"#)
.await
.expect("上传失败");
let data = svc
.download("remote/config.json")
.await
.expect("下载失败")
.expect("上传的文件应可下载");
assert_eq!(data, br#"{"a":1}"#);
assert_eq!(
svc.download("not-exist.json").await.expect("下载失败"),
None,
"不存在的文件应返回 None"
);
svc.delete("remote/config.json").await.expect("删除失败");
assert_eq!(
svc.download("remote/config.json").await.expect("下载失败"),
None,
"删除后文件应不可下载"
);
svc.upload("dir/a.txt", b"a").await.expect("上传失败");
svc.upload("dir/b.txt", b"b").await.expect("上传失败");
let files = svc.list("dir").await.expect("列表失败");
assert!(
files.contains(&"a.txt".to_string()),
"列表应含 a.txt: {:?}",
files
);
assert!(
files.contains(&"b.txt".to_string()),
"列表应含 b.txt: {:?}",
files
);
assert!(svc.validate().await, "validate 上传下载往返应成功");
svc.delete("dir/a.txt").await.expect("清理失败");
svc.delete("dir/b.txt").await.expect("清理失败");
let _ = reqwest::Client::builder()
.timeout(Duration::from_secs(2))
.build()
.expect("创建 reqwest 客户端失败")
.post(format!("{SERVER_URL}/__shutdown"))
.send()
.await;
Ok::<(), ()>(())
})
.await
.expect("WebDAV 端到端断言超时(30 秒)");
for _ in 0..50 {
if child.try_wait().ok().flatten().is_some() {
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
if child.try_wait().ok().flatten().is_none() {
#[cfg(windows)]
{
let _ = std::process::Command::new("taskkill")
.args(["/PID", &child.id().to_string(), "/T", "/F"])
.output();
}
#[cfg(not(windows))]
{
let _ = child.kill();
}
}
assertions.expect("WebDAV 端到端断言失败");
}
}