Readur/src/services/webdav/config.rs

209 lines
7.1 KiB
Rust

/// WebDAV server configuration
#[derive(Debug, Clone)]
pub struct WebDAVConfig {
pub server_url: String,
pub username: String,
pub password: String,
pub watch_folders: Vec<String>,
pub file_extensions: Vec<String>,
pub timeout_seconds: u64,
pub server_type: Option<String>, // "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<String>,
file_extensions: Vec<String>,
) -> 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<String> {
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)
}
}