352 lines
13 KiB
Rust
352 lines
13 KiB
Rust
use anyhow::Result;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use tracing::{debug, info, warn};
|
|
|
|
use super::config::WebDAVConfig;
|
|
use super::connection::WebDAVConnection;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ValidationReport {
|
|
pub overall_health_score: i32, // 0-100
|
|
pub issues: Vec<ValidationIssue>,
|
|
pub recommendations: Vec<ValidationRecommendation>,
|
|
pub summary: ValidationSummary,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ValidationIssue {
|
|
pub issue_type: ValidationIssueType,
|
|
pub severity: ValidationSeverity,
|
|
pub directory_path: String,
|
|
pub description: String,
|
|
pub details: Option<serde_json::Value>,
|
|
pub detected_at: chrono::DateTime<chrono::Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum ValidationIssueType {
|
|
/// Directory exists on server but not in our tracking
|
|
Untracked,
|
|
/// Directory in our tracking but missing on server
|
|
Missing,
|
|
/// ETag mismatch between server and our cache
|
|
ETagMismatch,
|
|
/// Directory hasn't been scanned in a very long time
|
|
Stale,
|
|
/// Server errors when accessing directory
|
|
Inaccessible,
|
|
/// ETag support seems unreliable for this directory
|
|
ETagUnreliable,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum ValidationSeverity {
|
|
Info, // No action needed, just FYI
|
|
Warning, // Should investigate but not urgent
|
|
Error, // Needs immediate attention
|
|
Critical, // System integrity at risk
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ValidationRecommendation {
|
|
pub action: ValidationAction,
|
|
pub reason: String,
|
|
pub affected_directories: Vec<String>,
|
|
pub priority: ValidationSeverity,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum ValidationAction {
|
|
/// Run a deep scan of specific directories
|
|
DeepScanRequired,
|
|
/// Clear and rebuild directory tracking
|
|
RebuildTracking,
|
|
/// ETag support is unreliable, switch to periodic scans
|
|
DisableETagOptimization,
|
|
/// Clean up orphaned database entries
|
|
CleanupDatabase,
|
|
/// Server configuration issue needs attention
|
|
CheckServerConfiguration,
|
|
/// No action needed, system is healthy
|
|
NoActionRequired,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ValidationSummary {
|
|
pub total_directories_checked: usize,
|
|
pub healthy_directories: usize,
|
|
pub directories_with_issues: usize,
|
|
pub critical_issues: usize,
|
|
pub warning_issues: usize,
|
|
pub info_issues: usize,
|
|
pub validation_duration_ms: u64,
|
|
}
|
|
|
|
pub struct WebDAVValidator {
|
|
connection: WebDAVConnection,
|
|
config: WebDAVConfig,
|
|
}
|
|
|
|
impl WebDAVValidator {
|
|
pub fn new(connection: WebDAVConnection, config: WebDAVConfig) -> Self {
|
|
Self { connection, config }
|
|
}
|
|
|
|
/// Performs comprehensive validation of WebDAV setup and directory tracking
|
|
pub async fn validate_system(&self) -> Result<ValidationReport> {
|
|
let start_time = std::time::Instant::now();
|
|
info!("🔍 Starting WebDAV system validation");
|
|
|
|
let mut issues = Vec::new();
|
|
let mut total_checked = 0;
|
|
|
|
// Test basic connectivity
|
|
match self.connection.test_connection().await {
|
|
Ok(result) if !result.success => {
|
|
issues.push(ValidationIssue {
|
|
issue_type: ValidationIssueType::Inaccessible,
|
|
severity: ValidationSeverity::Critical,
|
|
directory_path: "/".to_string(),
|
|
description: format!("WebDAV server connection failed: {}", result.message),
|
|
details: None,
|
|
detected_at: chrono::Utc::now(),
|
|
});
|
|
}
|
|
Err(e) => {
|
|
issues.push(ValidationIssue {
|
|
issue_type: ValidationIssueType::Inaccessible,
|
|
severity: ValidationSeverity::Critical,
|
|
directory_path: "/".to_string(),
|
|
description: format!("WebDAV server connectivity test failed: {}", e),
|
|
details: None,
|
|
detected_at: chrono::Utc::now(),
|
|
});
|
|
}
|
|
_ => {
|
|
debug!("✅ Basic connectivity test passed");
|
|
}
|
|
}
|
|
|
|
// Validate each watch folder
|
|
for folder in &self.config.watch_folders {
|
|
total_checked += 1;
|
|
if let Err(e) = self.validate_watch_folder(folder, &mut issues).await {
|
|
warn!("Failed to validate watch folder '{}': {}", folder, e);
|
|
}
|
|
}
|
|
|
|
// Test ETag reliability
|
|
self.validate_etag_support(&mut issues).await?;
|
|
|
|
// Generate recommendations based on issues
|
|
let recommendations = self.generate_recommendations(&issues);
|
|
|
|
let validation_duration = start_time.elapsed().as_millis() as u64;
|
|
let health_score = self.calculate_health_score(&issues);
|
|
|
|
let summary = ValidationSummary {
|
|
total_directories_checked: total_checked,
|
|
healthy_directories: total_checked - issues.len(),
|
|
directories_with_issues: issues.len(),
|
|
critical_issues: issues.iter().filter(|i| matches!(i.severity, ValidationSeverity::Critical)).count(),
|
|
warning_issues: issues.iter().filter(|i| matches!(i.severity, ValidationSeverity::Warning)).count(),
|
|
info_issues: issues.iter().filter(|i| matches!(i.severity, ValidationSeverity::Info)).count(),
|
|
validation_duration_ms: validation_duration,
|
|
};
|
|
|
|
info!("✅ WebDAV validation completed in {}ms. Health score: {}/100",
|
|
validation_duration, health_score);
|
|
|
|
Ok(ValidationReport {
|
|
overall_health_score: health_score,
|
|
issues,
|
|
recommendations,
|
|
summary,
|
|
})
|
|
}
|
|
|
|
/// Validates a specific watch folder
|
|
async fn validate_watch_folder(&self, folder: &str, issues: &mut Vec<ValidationIssue>) -> Result<()> {
|
|
debug!("Validating watch folder: {}", folder);
|
|
|
|
// Test PROPFIND access
|
|
match self.connection.test_propfind(folder).await {
|
|
Ok(_) => {
|
|
debug!("✅ Watch folder '{}' is accessible", folder);
|
|
}
|
|
Err(e) => {
|
|
issues.push(ValidationIssue {
|
|
issue_type: ValidationIssueType::Inaccessible,
|
|
severity: ValidationSeverity::Error,
|
|
directory_path: folder.to_string(),
|
|
description: format!("Cannot access watch folder: {}", e),
|
|
details: Some(serde_json::json!({
|
|
"error": e.to_string(),
|
|
"folder": folder
|
|
})),
|
|
detected_at: chrono::Utc::now(),
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests ETag support reliability
|
|
async fn validate_etag_support(&self, issues: &mut Vec<ValidationIssue>) -> Result<()> {
|
|
debug!("Testing ETag support reliability");
|
|
|
|
// Test ETag consistency across multiple requests
|
|
for folder in &self.config.watch_folders {
|
|
if let Err(e) = self.test_etag_consistency(folder, issues).await {
|
|
warn!("ETag consistency test failed for '{}': {}", folder, e);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests ETag consistency for a specific folder
|
|
async fn test_etag_consistency(&self, folder: &str, issues: &mut Vec<ValidationIssue>) -> Result<()> {
|
|
// Make two consecutive PROPFIND requests and compare ETags
|
|
let etag1 = self.get_folder_etag(folder).await?;
|
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
|
let etag2 = self.get_folder_etag(folder).await?;
|
|
|
|
if etag1 != etag2 && etag1.is_some() && etag2.is_some() {
|
|
issues.push(ValidationIssue {
|
|
issue_type: ValidationIssueType::ETagUnreliable,
|
|
severity: ValidationSeverity::Warning,
|
|
directory_path: folder.to_string(),
|
|
description: "ETag values are inconsistent across requests".to_string(),
|
|
details: Some(serde_json::json!({
|
|
"etag1": etag1,
|
|
"etag2": etag2,
|
|
"folder": folder
|
|
})),
|
|
detected_at: chrono::Utc::now(),
|
|
});
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Gets the ETag for a folder
|
|
async fn get_folder_etag(&self, folder: &str) -> Result<Option<String>> {
|
|
let url = self.connection.get_url_for_path(folder);
|
|
|
|
let propfind_body = r#"<?xml version="1.0" encoding="utf-8"?>
|
|
<D:propfind xmlns:D="DAV:">
|
|
<D:prop>
|
|
<D:getetag/>
|
|
</D:prop>
|
|
</D:propfind>"#;
|
|
|
|
let response = self.connection
|
|
.authenticated_request(
|
|
reqwest::Method::from_bytes(b"PROPFIND")?,
|
|
&url,
|
|
Some(propfind_body.to_string()),
|
|
Some(vec![
|
|
("Depth", "0"),
|
|
("Content-Type", "application/xml"),
|
|
]),
|
|
)
|
|
.await?;
|
|
|
|
let body = response.text().await?;
|
|
|
|
// Parse ETag from XML response (simplified)
|
|
if let Some(start) = body.find("<D:getetag>") {
|
|
if let Some(end) = body[start..].find("</D:getetag>") {
|
|
let etag = &body[start + 11..start + end];
|
|
return Ok(Some(etag.trim_matches('"').to_string()));
|
|
}
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
/// Generates recommendations based on detected issues
|
|
fn generate_recommendations(&self, issues: &Vec<ValidationIssue>) -> Vec<ValidationRecommendation> {
|
|
let mut recommendations = Vec::new();
|
|
let mut directories_by_issue: HashMap<ValidationIssueType, Vec<String>> = HashMap::new();
|
|
|
|
// Group directories by issue type
|
|
for issue in issues {
|
|
directories_by_issue
|
|
.entry(issue.issue_type.clone())
|
|
.or_insert_with(Vec::new)
|
|
.push(issue.directory_path.clone());
|
|
}
|
|
|
|
// Generate recommendations for each issue type
|
|
for (issue_type, directories) in directories_by_issue {
|
|
let recommendation = match issue_type {
|
|
ValidationIssueType::Inaccessible => ValidationRecommendation {
|
|
action: ValidationAction::CheckServerConfiguration,
|
|
reason: "Some directories are inaccessible. Check server configuration and permissions.".to_string(),
|
|
affected_directories: directories,
|
|
priority: ValidationSeverity::Critical,
|
|
},
|
|
ValidationIssueType::ETagUnreliable => ValidationRecommendation {
|
|
action: ValidationAction::DisableETagOptimization,
|
|
reason: "ETag support appears unreliable. Consider disabling ETag optimization.".to_string(),
|
|
affected_directories: directories,
|
|
priority: ValidationSeverity::Warning,
|
|
},
|
|
ValidationIssueType::Missing => ValidationRecommendation {
|
|
action: ValidationAction::CleanupDatabase,
|
|
reason: "Some tracked directories no longer exist on the server.".to_string(),
|
|
affected_directories: directories,
|
|
priority: ValidationSeverity::Warning,
|
|
},
|
|
ValidationIssueType::Stale => ValidationRecommendation {
|
|
action: ValidationAction::DeepScanRequired,
|
|
reason: "Some directories haven't been scanned recently.".to_string(),
|
|
affected_directories: directories,
|
|
priority: ValidationSeverity::Info,
|
|
},
|
|
_ => ValidationRecommendation {
|
|
action: ValidationAction::DeepScanRequired,
|
|
reason: "General validation issues detected.".to_string(),
|
|
affected_directories: directories,
|
|
priority: ValidationSeverity::Warning,
|
|
},
|
|
};
|
|
recommendations.push(recommendation);
|
|
}
|
|
|
|
if recommendations.is_empty() {
|
|
recommendations.push(ValidationRecommendation {
|
|
action: ValidationAction::NoActionRequired,
|
|
reason: "System validation passed successfully.".to_string(),
|
|
affected_directories: Vec::new(),
|
|
priority: ValidationSeverity::Info,
|
|
});
|
|
}
|
|
|
|
recommendations
|
|
}
|
|
|
|
/// Calculates overall health score based on issues
|
|
fn calculate_health_score(&self, issues: &Vec<ValidationIssue>) -> i32 {
|
|
if issues.is_empty() {
|
|
return 100;
|
|
}
|
|
|
|
let mut penalty = 0;
|
|
for issue in issues {
|
|
let issue_penalty = match issue.severity {
|
|
ValidationSeverity::Critical => 30,
|
|
ValidationSeverity::Error => 20,
|
|
ValidationSeverity::Warning => 10,
|
|
ValidationSeverity::Info => 5,
|
|
};
|
|
penalty += issue_penalty;
|
|
}
|
|
|
|
std::cmp::max(0, 100 - penalty)
|
|
}
|
|
} |