use thiserror::Error;
pub type StorageResult<T> = Result<T, StorageError>;
#[derive(Error, Debug)]
pub enum StorageError {
#[error("Configuration error: {0}")]
InvalidConfiguration(String),
#[error("Authentication error: {0}")]
AuthenticationError(String),
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error("File not found: {0}")]
FileNotFound(String),
#[error("File already exists: {0}")]
FileAlreadyExists(String),
#[error("Network error: {0}")]
NetworkError(String),
#[error("Filesystem error: {0}")]
FilesystemError(#[from] std::io::Error),
#[error("Invalid URL: {0}")]
InvalidUrl(#[from] url::ParseError),
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
#[error("Base64 error: {0}")]
Base64Error(#[from] base64::DecodeError),
#[error("Invalid file format: {0}")]
InvalidFileFormat(String),
#[error("File too large: {size} bytes, maximum allowed: {max_size} bytes")]
FileTooLarge {
size: u64,
max_size: u64,
},
#[error("Checksum mismatch: expected {expected}, got {actual}")]
ChecksumMismatch {
expected: String,
actual: String,
},
#[error("Upload interrupted: {0}")]
UploadInterrupted(String),
#[error("Download interrupted: {0}")]
DownloadInterrupted(String),
#[error("Multipart upload error: {0}")]
MultipartUploadError(String),
#[error("Presigned URL generation failed: {0}")]
PresignedUrlError(String),
#[error("Temporary storage error: {0}")]
TemporaryStorageError(String),
#[error("Storage quota exceeded: {used}/{limit} bytes")]
QuotaExceeded {
used: u64,
limit: u64,
},
#[error("Rate limited: try again in {retry_after} seconds")]
RateLimited {
retry_after: u64,
},
#[error("Invalid range request: {0}")]
InvalidRange(String),
#[error("Operation timed out after {timeout} seconds")]
Timeout {
timeout: u64,
},
#[error("Operation failed: {operation} - {message}")]
OperationFailed {
operation: String,
message: String,
},
#[error("Unsupported operation '{operation}' for backend '{backend}'")]
UnsupportedOperation {
operation: String,
backend: String,
},
#[error("S3 operation failed: {message}")]
S3OperationFailed {
message: String,
request_id: Option<String>,
},
#[error("GCS error: {0}")]
GcsError(String),
#[error("MinIO error: {0}")]
MinIOError(String),
#[error("HTTP error: {0}")]
HttpError(#[from] reqwest::Error),
#[error("Security error: {operation} - {message}")]
SecurityError {
operation: String,
message: String,
},
#[error("{0}")]
Other(String),
}
impl StorageError {
pub fn config_error(message: impl Into<String>) -> Self {
Self::InvalidConfiguration(message.into())
}
pub fn auth_error(message: impl Into<String>) -> Self {
Self::AuthenticationError(message.into())
}
pub fn not_found(file_path: impl Into<String>) -> Self {
Self::FileNotFound(file_path.into())
}
pub fn operation_failed(operation: impl Into<String>, message: impl Into<String>) -> Self {
Self::OperationFailed {
operation: operation.into(),
message: message.into(),
}
}
pub fn is_retryable(&self) -> bool {
matches!(self,
Self::NetworkError(_)
| Self::HttpError(_)
| Self::Timeout { .. }
| Self::RateLimited { .. }
)
}
pub fn is_client_error(&self) -> bool {
match self {
Self::AuthenticationError(_)
| Self::PermissionDenied(_)
| Self::FileNotFound(_)
| Self::FileAlreadyExists(_)
| Self::InvalidUrl(_)
| Self::InvalidFileFormat(_)
| Self::FileTooLarge { .. }
| Self::ChecksumMismatch { .. }
| Self::InvalidRange(_)
| Self::UnsupportedOperation { .. } => true,
Self::HttpError(e) => e.status().is_some_and(|s| s.is_client_error()),
_ => false,
}
}
pub fn is_server_error(&self) -> bool {
match self {
Self::NetworkError(_)
| Self::TemporaryStorageError(_) => true,
Self::HttpError(e) => e.status().is_some_and(|s| s.is_server_error()),
_ => false,
}
}
pub fn category(&self) -> &'static str {
match self {
Self::InvalidConfiguration(_) => "configuration",
Self::AuthenticationError(_) | Self::PermissionDenied(_) => "authentication",
Self::FileNotFound(_) | Self::FileAlreadyExists(_) => "file_system",
Self::NetworkError(_) | Self::HttpError(_) => "network",
Self::FilesystemError(_) => "filesystem",
Self::SerializationError(_) | Self::Base64Error(_) => "serialization",
Self::FileTooLarge { .. } | Self::QuotaExceeded { .. } => "quota",
Self::RateLimited { .. } => "rate_limit",
Self::Timeout { .. } => "timeout",
_ => "other",
}
}
}
impl From<reqwest::Response> for StorageError {
fn from(response: reqwest::Response) -> Self {
let status = response.status();
let url = response.url().clone();
match status.as_u16() {
400 => Self::InvalidConfiguration(format!("Bad request to {}", url)),
401 => Self::AuthenticationError("Unauthorized".to_string()),
403 => Self::PermissionDenied(format!("Access denied to {}", url)),
404 => Self::FileNotFound(format!("Resource not found at {}", url)),
409 => Self::FileAlreadyExists(format!("Resource already exists at {}", url)),
413 => Self::FileTooLarge { size: 0, max_size: 0 },
416 => Self::InvalidRange("Invalid range request".to_string()),
429 => Self::RateLimited { retry_after: 60 },
500..=599 => Self::NetworkError(format!("Server error {} from {}", status, url)),
_ => Self::Other(format!("HTTP {} from {}", status, url)),
}
}
}