Readur/src/models/source_error.rs

419 lines
14 KiB
Rust

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<Self, Self::Err> {
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<Uuid>,
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<Utc>,
pub last_failure_at: DateTime<Utc>,
pub last_retry_at: Option<DateTime<Utc>>,
pub next_retry_at: Option<DateTime<Utc>>,
// Error details
pub error_message: Option<String>,
pub error_code: Option<String>,
pub http_status_code: Option<i32>,
// Performance metrics
pub response_time_ms: Option<i32>,
pub response_size_bytes: Option<i64>,
// Resource characteristics
pub resource_size_bytes: Option<i64>,
pub resource_depth: Option<i32>,
pub estimated_item_count: Option<i32>,
// Source-specific diagnostic data
pub diagnostic_data: serde_json::Value,
// User actions
pub user_excluded: bool,
pub user_notes: Option<String>,
// Retry configuration
pub retry_strategy: String,
pub max_retries: i32,
pub retry_delay_seconds: i32,
// Resolution tracking
pub resolved: bool,
pub resolved_at: Option<DateTime<Utc>>,
pub resolution_method: Option<String>,
pub resolution_notes: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// 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<Uuid>,
pub resource_path: String,
pub error_type: SourceErrorType,
pub error_message: String,
pub error_code: Option<String>,
pub http_status_code: Option<i32>,
pub response_time_ms: Option<i32>,
pub response_size_bytes: Option<i64>,
pub resource_size_bytes: Option<i64>,
pub diagnostic_data: Option<serde_json::Value>,
}
/// 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<String>, // 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<Utc>,
pub last_failure_at: DateTime<Utc>,
pub next_retry_at: Option<DateTime<Utc>>,
pub error_message: Option<String>,
pub http_status_code: Option<i32>,
pub user_excluded: bool,
pub user_notes: Option<String>,
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<i32>,
pub estimated_item_count: Option<i32>,
pub response_time_ms: Option<i32>,
pub response_size_mb: Option<f64>,
pub resource_size_mb: Option<f64>,
pub recommended_action: String,
pub can_retry: bool,
pub user_action_required: bool,
pub source_specific_info: HashMap<String, serde_json::Value>,
}
/// 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<Uuid>,
pub operation: String, // e.g., "list_directory", "read_file", "get_metadata"
pub response_time: Option<std::time::Duration>,
pub response_size: Option<usize>,
pub server_type: Option<String>,
pub server_version: Option<String>,
pub additional_context: HashMap<String, serde_json::Value>,
}
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<String>, server_version: Option<String>) -> 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<String, i64>,
pub by_error_type: HashMap<String, i64>,
}
/// Request model for retrying a failed resource
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct RetryFailureRequest {
pub reset_consecutive_count: Option<bool>,
pub notes: Option<String>,
}
/// Request model for excluding a resource from scanning
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ExcludeResourceRequest {
pub reason: String,
pub notes: Option<String>,
pub permanent: Option<bool>,
}
/// Query parameters for listing failures
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ListFailuresQuery {
pub source_type: Option<ErrorSourceType>,
pub source_id: Option<Uuid>,
pub error_type: Option<SourceErrorType>,
pub severity: Option<SourceErrorSeverity>,
pub include_resolved: Option<bool>,
pub include_excluded: Option<bool>,
pub ready_for_retry: Option<bool>,
pub limit: Option<i32>,
pub offset: Option<i32>,
}
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),
}
}
}