diff --git a/Cargo.lock b/Cargo.lock index 0dd95e8f0..a4019d621 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1119,6 +1119,7 @@ dependencies = [ "rustls", "scopeguard", "serde", + "serde-xml-rs", "serde_json", "serde_yaml_ng", "smartstring", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index bb9c92f0c..34eb8fda1 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -83,6 +83,7 @@ tauri-plugin-deep-link = "2.4.9" tauri-plugin-window-state = "2.4.1" zip = "8.6.0" reqwest_dav = "0.3.3" +serde-xml-rs = "0.6.0" aes-gcm = { version = "0.11.0" } base64 = "0.22.1" getrandom = "0.4.3" diff --git a/src-tauri/src/core/backup.rs b/src-tauri/src/core/backup.rs index 37d87b271..891c0ad5e 100644 --- a/src-tauri/src/core/backup.rs +++ b/src-tauri/src/core/backup.rs @@ -5,7 +5,7 @@ use arc_swap::{ArcSwap, ArcSwapOption}; use backon::{ConstantBuilder, Retryable as _}; use clash_verge_logging::{Type, logging}; use once_cell::sync::OnceCell; -use reqwest_dav::list_cmd::{ListEntity, ListFile}; +use reqwest_dav::list_cmd::{ListEntity, ListFile, ListMultiStatus}; use smartstring::alias::String; use std::{ collections::HashMap, @@ -227,17 +227,29 @@ impl WebDavClient { let path = format!("{}/", dirs::BACKUP_DIR); let fut = async { - let files = client.list(path.as_str(), reqwest_dav::Depth::Number(1)).await?; + let response = client.list_raw(path.as_str(), reqwest_dav::Depth::Number(1)).await?; + let status = response.status(); + if !status.is_success() { + return Err(reqwest_dav::Error::Decode(reqwest_dav::DecodeError::StatusMismatched( + reqwest_dav::StatusMismatchedError { + response_code: status.as_u16(), + expected_code: 207, + }, + ))); + } + + let xml = response.text().await?; + let files = parse_webdav_list(&xml)?; let mut final_files = Vec::new(); for file in files { if let ListEntity::File(file) = file { final_files.push(file); } } - Ok::, Error>(final_files) + Ok::, reqwest_dav::Error>(final_files) }; - timeout(Duration::from_secs(TIMEOUT_LIST), fut).await? + Ok(timeout(Duration::from_secs(TIMEOUT_LIST), fut).await??) } pub async fn delete(&self, file_name: String) -> Result<(), Error> { @@ -250,6 +262,44 @@ impl WebDavClient { } } +fn parse_webdav_list(xml: &str) -> Result, reqwest_dav::Error> { + // Some WebDAV servers emit RFC 2822's numeric UTC offset instead of HTTP-date's `GMT`. + let normalized_xml = xml.replace(" +0000 + + + /clash-verge-rev-backup/backup.zip + + HTTP/1.1 200 OK + + Sun, 12 Jul 2026 17:09:37 +0000 + + 42 + application/zip + + + + "#; + + let files = parse_webdav_list(xml).expect("numeric UTC offset should be accepted"); + let ListEntity::File(file) = &files[0] else { + panic!("expected a file"); + }; + assert_eq!(file.href, "/clash-verge-rev-backup/backup.zip"); + assert_eq!(file.last_modified.timestamp(), 1_783_876_177); + } +} + pub async fn create_backup() -> Result<(String, PathBuf), Error> { let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string(); let zip_file_name: String = format!("{OS}-backup-{now}.zip").into();