#![allow(dead_code)]
use std::path::PathBuf;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AuthError {
#[error(
"Missing CEDA credentials. Set CEDA_USERNAME and CEDA_PASSWORD environment variables or run 'auth setup'"
)]
MissingCredentials,
#[error("Environment variable error: {0}")]
EnvVar(#[from] std::env::VarError),
#[error("HTTP request failed during authentication")]
Http(#[from] reqwest::Error),
#[error("CEDA login failed. Please check your credentials and try again")]
LoginFailed,
#[error("CSRF token not found in login page. The CEDA login page format may have changed")]
CsrfTokenNotFound,
#[error("CEDA session expired or invalid. Please re-authenticate")]
SessionExpired,
#[error("Invalid username format: {reason}")]
InvalidUsername { reason: String },
#[error("Failed to save credentials to file")]
CredentialStorage(#[from] std::io::Error),
#[error("Permission denied accessing credential file: {path}")]
PermissionDenied { path: PathBuf },
}
#[derive(Error, Debug)]
pub enum DownloadError {
#[error("HTTP request failed")]
Http(#[from] reqwest::Error),
#[error("File already exists: {path}. Use --force to overwrite")]
FileExists { path: String },
#[error("File I/O error")]
Io(#[from] std::io::Error),
#[error("Download timed out after {seconds} seconds")]
Timeout { seconds: u64 },
#[error("Invalid URL: {url} - {error}")]
InvalidUrl { url: String, error: String },
#[error("Server error: HTTP {status}")]
ServerError { status: u16 },
#[error("Rate limit exceeded. Server responded with HTTP 429")]
RateLimitExceeded,
#[error("Server overloaded. Server responded with HTTP 503")]
ServerOverloaded,
#[error("Circuit breaker open due to repeated failures. Waiting before retry")]
CircuitBreakerOpen,
#[error("File hash mismatch. Expected: {expected}, got: {actual}")]
HashMismatch { expected: String, actual: String },
#[error("File size mismatch. Expected: {expected} bytes, got: {actual} bytes")]
SizeMismatch { expected: u64, actual: u64 },
#[error("Temporary file operation failed: {path}")]
TempFileError { path: PathBuf },
#[error("Atomic file operation failed: could not rename {temp_path} to {final_path}")]
AtomicOperationFailed {
temp_path: PathBuf,
final_path: PathBuf,
},
#[error("Maximum retry attempts ({max_retries}) exceeded for download")]
MaxRetriesExceeded { max_retries: u32 },
#[error("Incomplete download: received {received} bytes, expected {expected} bytes")]
IncompleteDownload { received: u64, expected: u64 },
#[error("Work item not found: {work_id}")]
WorkNotFound { work_id: String },
#[error("{0}")]
Other(String),
}
#[derive(Error, Debug)]
pub enum ManifestError {
#[error("Manifest file not found: {path}")]
NotFound { path: PathBuf },
#[error("Invalid manifest format at line {line}: {content}")]
InvalidFormat { line: usize, content: String },
#[error("JSON parsing error in manifest")]
JsonParse(#[from] serde_json::Error),
#[error("I/O error reading manifest")]
Io(#[from] std::io::Error),
#[error("Manifest hash verification failed for file: {file_path}")]
HashVerificationFailed { file_path: PathBuf },
#[error("Duplicate entry found in manifest: {hash}")]
DuplicateEntry { hash: String },
#[error("Invalid hash format: {hash}. Expected MD5 hex string")]
InvalidHash { hash: String },
#[error("Invalid file path in manifest: {path}")]
InvalidPath { path: String },
#[error("Manifest version mismatch. Expected: {expected}, found: {found}")]
VersionMismatch { expected: String, found: String },
#[error("Manifest corruption detected: {reason}")]
Corruption { reason: String },
}
#[derive(Error, Debug)]
pub enum CacheError {
#[error("Cache directory not accessible: {path}")]
DirectoryNotAccessible { path: PathBuf },
#[error("Cache index corrupted: {reason}")]
IndexCorrupted { reason: String },
#[error("File reservation conflict: {file_hash} is being downloaded by worker {worker_id}")]
ReservationConflict { file_hash: String, worker_id: usize },
#[error("Cache verification failed for {files_failed} files")]
VerificationFailed { files_failed: usize },
#[error("Insufficient disk space. Required: {required} bytes, available: {available} bytes")]
InsufficientSpace { required: u64, available: u64 },
#[error("Permission denied for cache operation: {operation}")]
PermissionDenied { operation: String },
#[error("Cache lock timeout after {seconds} seconds")]
LockTimeout { seconds: u64 },
#[error("Invalid cache state: {reason}")]
InvalidState { reason: String },
#[error("Cache size limit exceeded. Current: {current} bytes, limit: {limit} bytes")]
SizeLimitExceeded { current: u64, limit: u64 },
}
#[derive(Error, Debug)]
pub enum QueueError {
#[error("Worker pool exhausted. All {worker_count} workers are busy")]
WorkerPoolExhausted { worker_count: usize },
#[error("Task queue overflow. Queue capacity: {capacity}")]
QueueOverflow { capacity: usize },
#[error("Worker {worker_id} panicked or terminated unexpectedly")]
WorkerPanic { worker_id: usize },
#[error("Coordinator shutdown timeout after {seconds} seconds")]
ShutdownTimeout { seconds: u64 },
#[error("Deadlock detected: {reason}")]
Deadlock { reason: String },
#[error("Invalid task state transition from {from} to {to}")]
InvalidStateTransition { from: String, to: String },
#[error("Channel communication error")]
ChannelError,
#[error("Task timeout after {seconds} seconds")]
TaskTimeout { seconds: u64 },
}
#[derive(Error, Debug)]
pub enum WebScrapingError {
#[error("Web scraping HTTP request failed")]
Http(#[from] reqwest::Error),
#[error("HTML parsing failed: {reason}")]
HtmlParsing { reason: String },
#[error("Invalid CSS selector: {selector}")]
InvalidSelector { selector: String },
#[error("Expected element not found: {selector}")]
ElementNotFound { selector: String },
#[error("Web scraping rate limit exceeded")]
RateLimitExceeded,
#[error("Invalid URL discovered: {url}")]
InvalidUrl { url: String },
#[error("Web scraping timeout after {seconds} seconds")]
Timeout { seconds: u64 },
}
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Configuration file not found: {path}")]
NotFound { path: PathBuf },
#[error("Invalid configuration format")]
InvalidFormat(#[from] toml::de::Error),
#[error("Missing required configuration field: {field}")]
MissingField { field: String },
#[error("Invalid configuration value for {field}: {value}. {reason}")]
InvalidValue {
field: String,
value: String,
reason: String,
},
#[error("Environment variable expansion failed: {var}")]
EnvExpansionFailed { var: String },
#[error("Configuration validation failed: {errors:?}")]
ValidationFailed { errors: Vec<String> },
}
#[derive(Error, Debug)]
pub enum ProgressError {
#[error("Progress reporting channel closed")]
ChannelClosed,
#[error("Progress calculation error: {reason}")]
CalculationError { reason: String },
#[error("Terminal output error")]
TerminalError(#[from] std::io::Error),
#[error("Progress state inconsistency: {reason}")]
StateInconsistency { reason: String },
}
#[derive(Error, Debug)]
pub enum AppError {
#[error(transparent)]
Auth(#[from] AuthError),
#[error(transparent)]
Download(#[from] DownloadError),
#[error(transparent)]
Manifest(#[from] ManifestError),
#[error(transparent)]
Cache(#[from] CacheError),
#[error(transparent)]
Queue(#[from] QueueError),
#[error(transparent)]
WebScraping(#[from] WebScrapingError),
#[error(transparent)]
Config(#[from] ConfigError),
#[error(transparent)]
Progress(#[from] ProgressError),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("Application error: {message}")]
Generic { message: String },
}
impl AppError {
pub fn generic(message: impl Into<String>) -> Self {
Self::Generic {
message: message.into(),
}
}
pub fn is_recoverable(&self) -> bool {
match self {
AppError::Download(DownloadError::Timeout { .. })
| AppError::Download(DownloadError::RateLimitExceeded)
| AppError::Download(DownloadError::ServerOverloaded)
| AppError::Download(DownloadError::Http(_))
| AppError::Auth(AuthError::Http(_))
| AppError::WebScraping(WebScrapingError::Http(_))
| AppError::WebScraping(WebScrapingError::RateLimitExceeded)
| AppError::Cache(CacheError::LockTimeout { .. })
| AppError::Queue(QueueError::TaskTimeout { .. }) => true,
AppError::Auth(AuthError::LoginFailed)
| AppError::Download(DownloadError::FileExists { .. })
| AppError::Download(DownloadError::MaxRetriesExceeded { .. })
| AppError::Manifest(ManifestError::InvalidFormat { .. })
| AppError::Config(ConfigError::InvalidFormat(_)) => false,
_ => false,
}
}
pub fn category(&self) -> &'static str {
match self {
AppError::Auth(_) => "authentication",
AppError::Download(_) => "download",
AppError::Manifest(_) => "manifest",
AppError::Cache(_) => "cache",
AppError::Queue(_) => "queue",
AppError::WebScraping(_) => "scraping",
AppError::Config(_) => "config",
AppError::Progress(_) => "progress",
AppError::Io(_) => "io",
AppError::Generic { .. } => "generic",
}
}
}
pub type Result<T> = std::result::Result<T, AppError>;
pub type AuthResult<T> = std::result::Result<T, AuthError>;
pub type DownloadResult<T> = std::result::Result<T, DownloadError>;
pub type ManifestResult<T> = std::result::Result<T, ManifestError>;
pub type CacheResult<T> = std::result::Result<T, CacheError>;
pub type QueueResult<T> = std::result::Result<T, QueueError>;
impl From<CacheError> for DownloadError {
fn from(cache_error: CacheError) -> Self {
match cache_error {
CacheError::DirectoryNotAccessible { path } => DownloadError::TempFileError { path },
CacheError::VerificationFailed { .. } => DownloadError::HashMismatch {
expected: "unknown".to_string(),
actual: "unknown".to_string(),
},
CacheError::InvalidState { reason } => DownloadError::TempFileError {
path: PathBuf::from(reason),
},
_ => DownloadError::TempFileError {
path: PathBuf::from("cache_error"),
},
}
}
}