use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum EngineError {
#[error("Network error: {message}")]
Network {
kind: NetworkErrorKind,
message: String,
retryable: bool,
},
#[error("Storage error at {path:?}: {message}")]
Storage {
kind: StorageErrorKind,
path: PathBuf,
message: String,
},
#[error("Protocol error: {message}")]
Protocol {
kind: ProtocolErrorKind,
message: String,
},
#[error("Invalid input for '{field}': {message}")]
InvalidInput {
field: &'static str,
message: String,
},
#[error("Resource limit exceeded: {resource} (limit: {limit})")]
ResourceLimit {
resource: &'static str,
limit: usize,
},
#[error("Download not found: {0}")]
NotFound(String),
#[error("Download already exists: {0}")]
AlreadyExists(String),
#[error("Invalid state: cannot {action} while {current_state}")]
InvalidState {
action: &'static str,
current_state: String,
},
#[error("Engine is shutting down")]
Shutdown,
#[error("Database error: {0}")]
Database(String),
#[error("Internal error: {0}")]
Internal(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NetworkErrorKind {
DnsResolution,
ConnectionRefused,
ConnectionReset,
Timeout,
Tls,
HttpStatus(u16),
Unreachable,
TooManyRedirects,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StorageErrorKind {
NotFound,
PermissionDenied,
DiskFull,
PathTraversal,
AlreadyExists,
InvalidPath,
Io,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProtocolErrorKind {
InvalidUrl,
RangeNotSupported,
InvalidResponse,
InvalidTorrent,
InvalidMagnet,
HashMismatch,
TrackerError,
PeerProtocol,
BencodeParse,
PexError,
DhtError,
LpdError,
MetadataError,
}
impl EngineError {
pub fn is_retryable(&self) -> bool {
match self {
Self::Network { retryable, .. } => *retryable,
Self::Storage { kind, .. } => matches!(kind, StorageErrorKind::Io),
Self::Protocol { kind, .. } => matches!(
kind,
ProtocolErrorKind::TrackerError | ProtocolErrorKind::PeerProtocol
),
_ => false,
}
}
pub fn network(kind: NetworkErrorKind, message: impl Into<String>) -> Self {
let retryable = matches!(
kind,
NetworkErrorKind::Timeout
| NetworkErrorKind::ConnectionReset
| NetworkErrorKind::Unreachable
| NetworkErrorKind::HttpStatus(408)
| NetworkErrorKind::HttpStatus(429)
| NetworkErrorKind::HttpStatus(500..=599)
);
Self::Network {
kind,
message: message.into(),
retryable,
}
}
pub fn storage(
kind: StorageErrorKind,
path: impl Into<PathBuf>,
message: impl Into<String>,
) -> Self {
Self::Storage {
kind,
path: path.into(),
message: message.into(),
}
}
pub fn protocol(kind: ProtocolErrorKind, message: impl Into<String>) -> Self {
Self::Protocol {
kind,
message: message.into(),
}
}
pub fn invalid_input(field: &'static str, message: impl Into<String>) -> Self {
Self::InvalidInput {
field,
message: message.into(),
}
}
pub fn is_not_found(&self) -> bool {
matches!(self, Self::NotFound(_))
}
pub fn is_network(&self) -> bool {
matches!(self, Self::Network { .. })
}
pub fn is_shutdown(&self) -> bool {
matches!(self, Self::Shutdown)
}
}
pub type Result<T> = std::result::Result<T, EngineError>;
impl From<std::io::Error> for EngineError {
fn from(err: std::io::Error) -> Self {
use std::io::ErrorKind;
let kind = match err.kind() {
ErrorKind::NotFound => StorageErrorKind::NotFound,
ErrorKind::PermissionDenied => StorageErrorKind::PermissionDenied,
ErrorKind::AlreadyExists => StorageErrorKind::AlreadyExists,
_ => StorageErrorKind::Io,
};
Self::Storage {
kind,
path: PathBuf::new(),
message: err.to_string(),
}
}
}
#[cfg(any(feature = "http", feature = "torrent"))]
impl From<reqwest::Error> for EngineError {
fn from(err: reqwest::Error) -> Self {
let kind = if err.is_timeout() {
NetworkErrorKind::Timeout
} else if err.is_connect() {
NetworkErrorKind::ConnectionRefused
} else if err.is_redirect() {
NetworkErrorKind::TooManyRedirects
} else if err.is_body() || err.is_decode() {
NetworkErrorKind::ConnectionReset
} else if let Some(status) = err.status() {
NetworkErrorKind::HttpStatus(status.as_u16())
} else {
NetworkErrorKind::Other
};
Self::network(kind, err.to_string())
}
}
impl From<url::ParseError> for EngineError {
fn from(err: url::ParseError) -> Self {
Self::Protocol {
kind: ProtocolErrorKind::InvalidUrl,
message: err.to_string(),
}
}
}
#[cfg(feature = "storage")]
impl From<rusqlite::Error> for EngineError {
fn from(err: rusqlite::Error) -> Self {
Self::Database(err.to_string())
}
}
impl From<serde_json::Error> for EngineError {
fn from(err: serde_json::Error) -> Self {
Self::Internal(format!("JSON error: {}", err))
}
}
impl From<tokio::sync::broadcast::error::SendError<crate::protocol::DownloadEvent>>
for EngineError
{
fn from(_: tokio::sync::broadcast::error::SendError<crate::protocol::DownloadEvent>) -> Self {
Self::Shutdown
}
}
impl From<EngineError> for crate::protocol::ProtocolError {
fn from(e: EngineError) -> Self {
use crate::protocol::ProtocolError;
match e {
EngineError::NotFound(id) => ProtocolError::NotFound { id },
EngineError::InvalidState {
action,
current_state,
} => ProtocolError::InvalidState {
action: action.to_string(),
current_state,
},
EngineError::InvalidInput { field, message } => ProtocolError::InvalidInput {
field: field.to_string(),
message,
},
EngineError::Network {
message, retryable, ..
} => ProtocolError::Network { message, retryable },
EngineError::Storage { message, .. } => ProtocolError::Storage { message },
EngineError::Protocol { message, .. } => ProtocolError::Network {
message,
retryable: false,
},
EngineError::Shutdown => ProtocolError::Shutdown,
EngineError::AlreadyExists(id) => ProtocolError::InvalidInput {
field: "id".to_string(),
message: format!("Download already exists: {}", id),
},
EngineError::ResourceLimit { resource, limit } => ProtocolError::InvalidInput {
field: resource.to_string(),
message: format!("Resource limit exceeded (limit: {})", limit),
},
EngineError::Database(msg) => ProtocolError::Storage { message: msg },
EngineError::Internal(msg) => ProtocolError::Internal { message: msg },
}
}
}