diff --git a/src/services/webdav/config.rs b/src/services/webdav/config.rs index 71ba15c..ab415fa 100644 --- a/src/services/webdav/config.rs +++ b/src/services/webdav/config.rs @@ -157,6 +157,41 @@ impl WebDAVConfig { } } + /// Returns alternative WebDAV URLs to try if the primary one fails + /// This is used for fallback mechanisms when encountering 405 errors + pub fn webdav_fallback_urls(&self) -> Vec { + let normalized_url = self.server_url.trim_end_matches('/').to_string(); + let mut fallback_urls = Vec::new(); + + match self.server_type.as_deref() { + Some("nextcloud") => { + // Primary: /remote.php/dav/files/{username} + // Fallback 1: /remote.php/webdav (legacy ownCloud style) + // Fallback 2: /webdav (generic) + fallback_urls.push(format!("{}/remote.php/webdav", normalized_url)); + fallback_urls.push(format!("{}/webdav", normalized_url)); + } + Some("owncloud") => { + // Primary: /remote.php/webdav + // Fallback 1: /remote.php/dav/files/{username} (newer Nextcloud style) + // Fallback 2: /webdav (generic) + fallback_urls.push(format!("{}/remote.php/dav/files/{}", normalized_url, self.username)); + fallback_urls.push(format!("{}/webdav", normalized_url)); + } + _ => { + // Generic WebDAV - try common patterns + // Fallback 1: /remote.php/webdav (ownCloud/Nextcloud) + // Fallback 2: /remote.php/dav/files/{username} (Nextcloud) + // Fallback 3: /dav (alternative) + fallback_urls.push(format!("{}/remote.php/webdav", normalized_url)); + fallback_urls.push(format!("{}/remote.php/dav/files/{}", normalized_url, self.username)); + fallback_urls.push(format!("{}/dav", normalized_url)); + } + } + + fallback_urls + } + /// Checks if a file extension is supported pub fn is_supported_extension(&self, filename: &str) -> bool { if self.file_extensions.is_empty() { diff --git a/src/services/webdav/service.rs b/src/services/webdav/service.rs index df64c54..4422edd 100644 --- a/src/services/webdav/service.rs +++ b/src/services/webdav/service.rs @@ -326,6 +326,21 @@ impl WebDAVService { pub async fn test_propfind(&self, path: &str) -> Result<()> { let url = self.get_url_for_path(path); + debug!("๐Ÿงช Testing PROPFIND for path '{}' at URL '{}'", path, url); + + // First, check server capabilities if this is the first PROPFIND + if path == "/" || path.is_empty() { + match self.validate_webdav_capabilities(&url).await { + Ok(capabilities) => { + info!("โœ… WebDAV capabilities validated: DAV={}, Methods={}", + capabilities.dav_compliance, capabilities.allowed_methods); + } + Err(e) => { + warn!("โš ๏ธ WebDAV capability validation failed (continuing anyway): {}", e); + } + } + } + let propfind_body = r#" @@ -348,7 +363,7 @@ impl WebDAVService { ).await?; if response.status().as_u16() == 207 { - debug!("PROPFIND successful for path: {}", path); + debug!("โœ… PROPFIND successful for path: {}", path); Ok(()) } else { Err(anyhow!( @@ -360,6 +375,66 @@ impl WebDAVService { } } + /// Validates WebDAV server capabilities to help diagnose configuration issues + async fn validate_webdav_capabilities(&self, url: &str) -> Result { + debug!("๐Ÿ” Validating WebDAV capabilities for URL: {}", url); + + let options_response = self.authenticated_request( + reqwest::Method::OPTIONS, + url, + None, + None, + ).await?; + + let dav_header = options_response + .headers() + .get("dav") + .and_then(|v| v.to_str().ok()) + .unwrap_or("") + .to_string(); + + let allow_header = options_response + .headers() + .get("allow") + .and_then(|v| v.to_str().ok()) + .unwrap_or("") + .to_string(); + + let server_header = options_response + .headers() + .get("server") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()); + + // Check if PROPFIND is in the allowed methods + if !allow_header.to_uppercase().contains("PROPFIND") { + warn!("โš ๏ธ PROPFIND method not listed in server's Allow header: {}", allow_header); + warn!("๐Ÿ’ก This suggests WebDAV may not be properly enabled on this endpoint"); + } + + // Check DAV compliance level + if dav_header.is_empty() { + warn!("โš ๏ธ No DAV header found - this endpoint may not support WebDAV"); + } else { + debug!("๐Ÿ“‹ Server DAV compliance: {}", dav_header); + } + + if let Some(ref server) = server_header { + debug!("๐Ÿ–ฅ๏ธ Server software: {}", server); + } + + Ok(ServerCapabilities { + dav_compliance: dav_header.clone(), + allowed_methods: allow_header, + server_software: server_header, + supports_etag: dav_header.contains("1") || dav_header.contains("2"), + supports_depth_infinity: dav_header.contains("1"), + infinity_depth_tested: false, + infinity_depth_works: false, + last_checked: std::time::Instant::now(), + }) + } + // ============================================================================ // HTTP Request Methods with Simple Retry Logic // ============================================================================ @@ -375,6 +450,24 @@ impl WebDAVService { let mut attempt = 0; let mut delay = self.retry_config.initial_delay_ms; + // Enhanced debug logging for HTTP requests + debug!("๐ŸŒ HTTP Request Details:"); + debug!(" Method: {}", method); + debug!(" URL: {}", url); + debug!(" Username: {}", self.config.username); + if let Some(ref headers_list) = headers { + debug!(" Headers: {:?}", headers_list); + } + if let Some(ref body_content) = body { + debug!(" Body length: {} bytes", body_content.len()); + debug!(" Body preview: {}", + if body_content.len() > 200 { + format!("{}...", &body_content[..200]) + } else { + body_content.clone() + }); + } + loop { let mut request = self.client .request(method.clone(), url) @@ -390,11 +483,23 @@ impl WebDAVService { } } + debug!("๐Ÿ“ค Sending HTTP {} request to: {}", method, url); match request.send().await { Ok(response) => { let status = response.status(); + debug!("๐Ÿ“ฅ HTTP Response: {} {}", status.as_u16(), status.canonical_reason().unwrap_or("")); + + // Log response headers for debugging + for (key, value) in response.headers() { + if key.as_str().to_lowercase().contains("allow") || + key.as_str().to_lowercase().contains("dav") || + key.as_str().to_lowercase().contains("server") { + debug!(" Response header: {}: {:?}", key, value); + } + } if status.is_success() || status.as_u16() == 207 { + debug!("โœ… HTTP request successful: {} {}", status.as_u16(), status.canonical_reason().unwrap_or("")); return Ok(response); } @@ -407,8 +512,40 @@ impl WebDAVService { // Handle client errors (don't retry) if status.is_client_error() && status.as_u16() != 429 { - return Err(anyhow!("Client error: {} - {}", status, - response.text().await.unwrap_or_default())); + let error_body = response.text().await.unwrap_or_default(); + + // Provide specific guidance for 405 Method Not Allowed errors + if status.as_u16() == 405 { + error!("๐Ÿšซ HTTP 405 Method Not Allowed for {} {}", method, url); + error!("๐Ÿ” Request Details:"); + error!(" Method: {}", method); + error!(" URL: {}", url); + error!(" Server type: {:?}", self.config.server_type); + error!(" Username: {}", self.config.username); + error!(" Server base URL: {}", self.config.server_url); + error!(" WebDAV base URL: {}", self.config.webdav_url()); + if let Some(ref headers_list) = headers { + error!(" Request headers: {:?}", headers_list); + } + error!("๐Ÿ“ This usually indicates:"); + error!(" 1. WebDAV is not enabled on the server"); + error!(" 2. The URL endpoint doesn't support {} method", method); + error!(" 3. Incorrect WebDAV endpoint URL"); + error!(" 4. Authentication issues or insufficient permissions"); + error!("๐Ÿ’ก Troubleshooting steps:"); + error!(" - Verify WebDAV is enabled in your server settings"); + error!(" - Check if the WebDAV endpoint URL is correct"); + error!(" - Try testing with a WebDAV client like Cyberduck"); + error!(" - Verify your user has WebDAV access permissions"); + + return Err(anyhow!( + "WebDAV {} method not allowed (405) at URL: {}. This typically means WebDAV is not properly enabled on the server or the URL is incorrect. \ + Server type: {:?}, Base URL: {}, WebDAV URL: {}. Error details: {}", + method, url, self.config.server_type, self.config.server_url, self.config.webdav_url(), error_body + )); + } + + return Err(anyhow!("Client error: {} - {}", status, error_body)); } // Handle server errors (retry) @@ -458,13 +595,23 @@ impl WebDAVService { let base_url = self.config.webdav_url(); let clean_path = path.trim_start_matches('/'); - if clean_path.is_empty() { - base_url + let final_url = if clean_path.is_empty() { + base_url.clone() } else { // Ensure no double slashes by normalizing the base URL let normalized_base = base_url.trim_end_matches('/'); format!("{}/{}", normalized_base, clean_path) - } + }; + + debug!("๐Ÿ”— URL Construction:"); + debug!(" Input path: '{}'", path); + debug!(" Clean path: '{}'", clean_path); + debug!(" Base WebDAV URL: '{}'", base_url); + debug!(" Final URL: '{}'", final_url); + debug!(" Server type: {:?}", self.config.server_type); + debug!(" Server base URL: '{}'", self.config.server_url); + + final_url } /// Convert full WebDAV href (from XML response) to relative path @@ -674,7 +821,60 @@ impl WebDAVService { /// Discovers both files and directories in a single directory async fn discover_files_and_directories_single(&self, directory_path: &str) -> Result { - let url = self.get_url_for_path(directory_path); + // Try the primary URL first, then fallback URLs if we get a 405 error + match self.discover_files_and_directories_single_with_url(directory_path, &self.get_url_for_path(directory_path)).await { + Ok(result) => Ok(result), + Err(e) => { + // Check if this is a 405 Method Not Allowed error + if e.to_string().contains("405") || e.to_string().contains("Method Not Allowed") { + warn!("๐Ÿ”„ Primary WebDAV URL failed with 405 error, trying fallback URLs..."); + self.try_fallback_discovery(directory_path).await + } else { + Err(e) + } + } + } + } + + /// Tries fallback URLs when the primary WebDAV URL fails with 405 + async fn try_fallback_discovery(&self, directory_path: &str) -> Result { + let fallback_urls = self.config.webdav_fallback_urls(); + + for (i, fallback_base_url) in fallback_urls.iter().enumerate() { + let fallback_url = if directory_path == "/" || directory_path.is_empty() { + fallback_base_url.clone() + } else { + format!("{}/{}", fallback_base_url.trim_end_matches('/'), directory_path.trim_start_matches('/')) + }; + + info!("๐Ÿ”„ Trying fallback URL #{}: {}", i + 1, fallback_url); + + match self.discover_files_and_directories_single_with_url(directory_path, &fallback_url).await { + Ok(result) => { + info!("โœ… Fallback URL #{} succeeded: {}", i + 1, fallback_url); + warn!("๐Ÿ’ก Consider updating your server type configuration to use this URL pattern"); + return Ok(result); + } + Err(e) => { + warn!("โŒ Fallback URL #{} failed: {} - {}", i + 1, fallback_url, e); + } + } + } + + Err(anyhow!( + "All WebDAV URLs failed for directory '{}'. Primary URL and {} fallback URLs were tried. \ + This suggests WebDAV is not properly configured on the server or the server type is incorrect.", + directory_path, fallback_urls.len() + )) + } + + /// Performs the actual discovery with a specific URL + async fn discover_files_and_directories_single_with_url(&self, directory_path: &str, url: &str) -> Result { + // Enhanced debug logging for WebDAV URL construction + debug!("๐Ÿ” WebDAV directory scan - Path: '{}', URL: '{}', Server type: {:?}", + directory_path, url, self.config.server_type); + debug!("๐Ÿ”ง WebDAV config - Server URL: '{}', Username: '{}', WebDAV base URL: '{}'", + self.config.server_url, self.config.username, self.config.webdav_url()); let propfind_body = r#" @@ -688,15 +888,22 @@ impl WebDAVService { "#; + debug!("๐Ÿ“ค Sending PROPFIND request to URL: {}", url); + debug!("๐Ÿ“‹ PROPFIND body length: {} bytes", propfind_body.len()); + let response = self.authenticated_request( Method::from_bytes(b"PROPFIND")?, - &url, + url, Some(propfind_body.to_string()), Some(vec![ ("Depth", "1"), ("Content-Type", "application/xml"), ]), - ).await?; + ).await.map_err(|e| { + error!("โŒ PROPFIND request failed for directory '{}' at URL '{}': {}", + directory_path, url, e); + e + })?; let body = response.text().await?; let all_items = parse_propfind_response_with_directories(&body)?;