/// WebDAV server configuration #[derive(Debug, Clone)] pub struct WebDAVConfig { pub server_url: String, pub username: String, pub password: String, pub watch_folders: Vec, pub file_extensions: Vec, pub timeout_seconds: u64, pub server_type: Option, // "nextcloud", "owncloud", "generic" } /// Retry configuration for WebDAV operations #[derive(Debug, Clone)] pub struct RetryConfig { pub max_retries: u32, pub initial_delay_ms: u64, pub max_delay_ms: u64, pub backoff_multiplier: f64, pub timeout_seconds: u64, pub rate_limit_backoff_ms: u64, // Additional backoff for 429 responses } /// Concurrency configuration for WebDAV operations #[derive(Debug, Clone)] pub struct ConcurrencyConfig { pub max_concurrent_scans: usize, pub max_concurrent_downloads: usize, pub adaptive_rate_limiting: bool, } /// Configuration for Depth infinity PROPFIND optimizations #[derive(Debug, Clone)] pub struct DepthInfinityConfig { /// Whether to attempt Depth infinity PROPFIND requests pub enabled: bool, /// Maximum response size in bytes before falling back to recursive approach pub max_response_size_bytes: usize, /// Timeout for infinity depth requests in seconds pub timeout_seconds: u64, /// Cache server capability detection results for this duration (seconds) pub capability_cache_duration_seconds: u64, /// Whether to automatically fallback to recursive approach on failure pub auto_fallback: bool, /// Maximum directory depth to attempt infinity for (0 = no limit) pub max_depth_for_infinity: u32, } impl Default for RetryConfig { fn default() -> Self { Self { max_retries: 3, initial_delay_ms: 1000, // 1 second max_delay_ms: 30000, // 30 seconds backoff_multiplier: 2.0, timeout_seconds: 30, rate_limit_backoff_ms: 5000, // 5 seconds } } } impl Default for ConcurrencyConfig { fn default() -> Self { Self { max_concurrent_scans: 4, max_concurrent_downloads: 8, adaptive_rate_limiting: true, } } } impl Default for DepthInfinityConfig { fn default() -> Self { Self { enabled: true, max_response_size_bytes: 50 * 1024 * 1024, // 50MB timeout_seconds: 120, // 2 minutes for large directories capability_cache_duration_seconds: 3600, // 1 hour auto_fallback: true, max_depth_for_infinity: 0, // No limit by default } } } impl WebDAVConfig { /// Creates a new WebDAV configuration pub fn new( server_url: String, username: String, password: String, watch_folders: Vec, file_extensions: Vec, ) -> Self { Self { server_url, username, password, watch_folders, file_extensions, timeout_seconds: 30, server_type: None, } } /// Validates the configuration pub fn validate(&self) -> anyhow::Result<()> { if self.server_url.is_empty() { return Err(anyhow::anyhow!("Server URL cannot be empty")); } if self.username.is_empty() { return Err(anyhow::anyhow!("Username cannot be empty")); } if self.password.is_empty() { return Err(anyhow::anyhow!("Password cannot be empty")); } if self.watch_folders.is_empty() { return Err(anyhow::anyhow!("At least one watch folder must be specified")); } // Validate URL format if !self.server_url.starts_with("http://") && !self.server_url.starts_with("https://") { return Err(anyhow::anyhow!("Server URL must start with http:// or https://")); } Ok(()) } /// Returns the base URL for WebDAV operations pub fn webdav_url(&self) -> String { // Normalize the server URL by removing trailing slashes let normalized_url = self.server_url.trim_end_matches('/').to_string(); // Add WebDAV path based on server type match self.server_type.as_deref() { Some("nextcloud") => { if !normalized_url.contains("/remote.php/dav/files/") { format!("{}/remote.php/dav/files/{}", normalized_url, self.username) } else { normalized_url } } Some("owncloud") => { if !normalized_url.contains("/remote.php/webdav") { format!("{}/remote.php/webdav", normalized_url) } else { normalized_url } } _ => { // Generic WebDAV - use the normalized URL as provided normalized_url } } } /// 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() { return true; // If no extensions specified, support all } let extension = filename.split('.').last().unwrap_or(""); self.file_extensions.iter().any(|ext| ext.eq_ignore_ascii_case(extension)) } /// Gets the timeout duration pub fn timeout(&self) -> std::time::Duration { std::time::Duration::from_secs(self.timeout_seconds) } }