use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; use std::collections::HashMap; use std::fmt; use uuid::Uuid; use anyhow::Result; use utoipa::ToSchema; /// Generic source types that can be monitored for errors #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type, ToSchema)] #[sqlx(type_name = "source_error_source_type", rename_all = "lowercase")] pub enum ErrorSourceType { #[sqlx(rename = "webdav")] WebDAV, #[sqlx(rename = "s3")] S3, #[sqlx(rename = "local")] Local, #[sqlx(rename = "dropbox")] Dropbox, #[sqlx(rename = "gdrive")] GDrive, #[sqlx(rename = "onedrive")] OneDrive, } impl fmt::Display for ErrorSourceType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ErrorSourceType::WebDAV => write!(f, "webdav"), ErrorSourceType::S3 => write!(f, "s3"), ErrorSourceType::Local => write!(f, "local"), ErrorSourceType::Dropbox => write!(f, "dropbox"), ErrorSourceType::GDrive => write!(f, "gdrive"), ErrorSourceType::OneDrive => write!(f, "onedrive"), } } } /// Generic error types that can occur across all source types #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type, ToSchema)] #[sqlx(type_name = "source_error_type", rename_all = "lowercase")] pub enum SourceErrorType { #[sqlx(rename = "timeout")] Timeout, #[sqlx(rename = "permission_denied")] PermissionDenied, #[sqlx(rename = "network_error")] NetworkError, #[sqlx(rename = "server_error")] ServerError, #[sqlx(rename = "path_too_long")] PathTooLong, #[sqlx(rename = "invalid_characters")] InvalidCharacters, #[sqlx(rename = "too_many_items")] TooManyItems, #[sqlx(rename = "depth_limit")] DepthLimit, #[sqlx(rename = "size_limit")] SizeLimit, #[sqlx(rename = "xml_parse_error")] XmlParseError, #[sqlx(rename = "json_parse_error")] JsonParseError, #[sqlx(rename = "quota_exceeded")] QuotaExceeded, #[sqlx(rename = "rate_limited")] RateLimited, #[sqlx(rename = "not_found")] NotFound, #[sqlx(rename = "conflict")] Conflict, #[sqlx(rename = "unsupported_operation")] UnsupportedOperation, #[sqlx(rename = "unknown")] Unknown, } impl fmt::Display for SourceErrorType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { SourceErrorType::Timeout => write!(f, "timeout"), SourceErrorType::PermissionDenied => write!(f, "permission_denied"), SourceErrorType::NetworkError => write!(f, "network_error"), SourceErrorType::ServerError => write!(f, "server_error"), SourceErrorType::PathTooLong => write!(f, "path_too_long"), SourceErrorType::InvalidCharacters => write!(f, "invalid_characters"), SourceErrorType::TooManyItems => write!(f, "too_many_items"), SourceErrorType::DepthLimit => write!(f, "depth_limit"), SourceErrorType::SizeLimit => write!(f, "size_limit"), SourceErrorType::XmlParseError => write!(f, "xml_parse_error"), SourceErrorType::JsonParseError => write!(f, "json_parse_error"), SourceErrorType::QuotaExceeded => write!(f, "quota_exceeded"), SourceErrorType::RateLimited => write!(f, "rate_limited"), SourceErrorType::NotFound => write!(f, "not_found"), SourceErrorType::Conflict => write!(f, "conflict"), SourceErrorType::UnsupportedOperation => write!(f, "unsupported_operation"), SourceErrorType::Unknown => write!(f, "unknown"), } } } /// Error severity levels for determining retry strategy and user notification priority #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type, ToSchema)] #[sqlx(type_name = "source_error_severity", rename_all = "lowercase")] pub enum SourceErrorSeverity { #[sqlx(rename = "low")] Low, #[sqlx(rename = "medium")] Medium, #[sqlx(rename = "high")] High, #[sqlx(rename = "critical")] Critical, } impl fmt::Display for SourceErrorSeverity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { SourceErrorSeverity::Low => write!(f, "low"), SourceErrorSeverity::Medium => write!(f, "medium"), SourceErrorSeverity::High => write!(f, "high"), SourceErrorSeverity::Critical => write!(f, "critical"), } } } /// Retry strategies for handling failures #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum RetryStrategy { Exponential, Linear, Fixed, } impl fmt::Display for RetryStrategy { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { RetryStrategy::Exponential => write!(f, "exponential"), RetryStrategy::Linear => write!(f, "linear"), RetryStrategy::Fixed => write!(f, "fixed"), } } } impl std::str::FromStr for RetryStrategy { type Err = anyhow::Error; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "exponential" => Ok(RetryStrategy::Exponential), "linear" => Ok(RetryStrategy::Linear), "fixed" => Ok(RetryStrategy::Fixed), _ => Err(anyhow::anyhow!("Invalid retry strategy: {}", s)), } } } /// Complete source scan failure record #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct SourceScanFailure { pub id: Uuid, pub user_id: Uuid, pub source_type: ErrorSourceType, pub source_id: Option, pub resource_path: String, // Failure classification pub error_type: SourceErrorType, pub error_severity: SourceErrorSeverity, pub failure_count: i32, pub consecutive_failures: i32, // Timestamps pub first_failure_at: DateTime, pub last_failure_at: DateTime, pub last_retry_at: Option>, pub next_retry_at: Option>, // Error details pub error_message: Option, pub error_code: Option, pub http_status_code: Option, // Performance metrics pub response_time_ms: Option, pub response_size_bytes: Option, // Resource characteristics pub resource_size_bytes: Option, pub resource_depth: Option, pub estimated_item_count: Option, // Source-specific diagnostic data pub diagnostic_data: serde_json::Value, // User actions pub user_excluded: bool, pub user_notes: Option, // Retry configuration pub retry_strategy: String, pub max_retries: i32, pub retry_delay_seconds: i32, // Resolution tracking pub resolved: bool, pub resolved_at: Option>, pub resolution_method: Option, pub resolution_notes: Option, pub created_at: DateTime, pub updated_at: DateTime, } /// Model for creating new source scan failures #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateSourceScanFailure { pub user_id: Uuid, pub source_type: ErrorSourceType, pub source_id: Option, pub resource_path: String, pub error_type: SourceErrorType, pub error_message: String, pub error_code: Option, pub http_status_code: Option, pub response_time_ms: Option, pub response_size_bytes: Option, pub resource_size_bytes: Option, pub diagnostic_data: Option, } /// Response model for API endpoints with enhanced diagnostics #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct SourceScanFailureResponse { pub id: Uuid, pub source_type: ErrorSourceType, pub source_name: Option, // From joined sources table pub resource_path: String, pub error_type: SourceErrorType, pub error_severity: SourceErrorSeverity, pub failure_count: i32, pub consecutive_failures: i32, pub first_failure_at: DateTime, pub last_failure_at: DateTime, pub next_retry_at: Option>, pub error_message: Option, pub http_status_code: Option, pub user_excluded: bool, pub user_notes: Option, pub resolved: bool, pub diagnostic_summary: SourceFailureDiagnostics, } /// Diagnostic information for helping users understand and resolve failures #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct SourceFailureDiagnostics { pub resource_depth: Option, pub estimated_item_count: Option, pub response_time_ms: Option, pub response_size_mb: Option, pub resource_size_mb: Option, pub recommended_action: String, pub can_retry: bool, pub user_action_required: bool, pub source_specific_info: HashMap, } /// Classification result for errors #[derive(Debug, Clone)] pub struct ErrorClassification { pub error_type: SourceErrorType, pub severity: SourceErrorSeverity, pub retry_strategy: RetryStrategy, pub retry_delay_seconds: u32, pub max_retries: u32, pub user_friendly_message: String, pub recommended_action: String, pub diagnostic_data: serde_json::Value, } /// Trait for source-specific error classification pub trait SourceErrorClassifier: Send + Sync { /// Classify an error into the generic error tracking system fn classify_error(&self, error: &anyhow::Error, context: &ErrorContext) -> ErrorClassification; /// Get source-specific diagnostic information fn extract_diagnostics(&self, error: &anyhow::Error, context: &ErrorContext) -> serde_json::Value; /// Build user-friendly error message with source-specific guidance fn build_user_friendly_message(&self, failure: &SourceScanFailure) -> String; /// Determine if an error should be automatically retried fn should_retry(&self, failure: &SourceScanFailure) -> bool; /// Get the source type this classifier handles fn source_type(&self) -> ErrorSourceType; } /// Context information available during error classification #[derive(Debug, Clone)] pub struct ErrorContext { pub resource_path: String, pub source_id: Option, pub operation: String, // e.g., "list_directory", "read_file", "get_metadata" pub response_time: Option, pub response_size: Option, pub server_type: Option, pub server_version: Option, pub additional_context: HashMap, } impl ErrorContext { pub fn new(resource_path: String) -> Self { Self { resource_path, source_id: None, operation: "unknown".to_string(), response_time: None, response_size: None, server_type: None, server_version: None, additional_context: HashMap::new(), } } pub fn with_source_id(mut self, source_id: Uuid) -> Self { self.source_id = Some(source_id); self } pub fn with_operation(mut self, operation: String) -> Self { self.operation = operation; self } pub fn with_response_time(mut self, duration: std::time::Duration) -> Self { self.response_time = Some(duration); self } pub fn with_response_size(mut self, size: usize) -> Self { self.response_size = Some(size); self } pub fn with_server_info(mut self, server_type: Option, server_version: Option) -> Self { self.server_type = server_type; self.server_version = server_version; self } pub fn with_context(mut self, key: String, value: serde_json::Value) -> Self { self.additional_context.insert(key, value); self } } /// Statistics for source scan failures #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct SourceScanFailureStats { pub active_failures: i64, pub resolved_failures: i64, pub excluded_resources: i64, pub critical_failures: i64, pub high_failures: i64, pub medium_failures: i64, pub low_failures: i64, pub ready_for_retry: i64, pub by_source_type: HashMap, pub by_error_type: HashMap, } /// Request model for retrying a failed resource #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct RetryFailureRequest { pub reset_consecutive_count: Option, pub notes: Option, } /// Request model for excluding a resource from scanning #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct ExcludeResourceRequest { pub reason: String, pub notes: Option, pub permanent: Option, } /// Query parameters for listing failures #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct ListFailuresQuery { pub source_type: Option, pub source_id: Option, pub error_type: Option, pub severity: Option, pub include_resolved: Option, pub include_excluded: Option, pub ready_for_retry: Option, pub limit: Option, pub offset: Option, } impl Default for ListFailuresQuery { fn default() -> Self { Self { source_type: None, source_id: None, error_type: None, severity: None, include_resolved: Some(false), include_excluded: Some(false), ready_for_retry: None, limit: Some(50), offset: Some(0), } } }