1use thiserror::Error;
4
5pub type StorageResult<T> = Result<T, StorageError>;
7
8#[derive(Error, Debug)]
10pub enum StorageError {
11 #[error("Configuration error: {0}")]
13 InvalidConfiguration(String),
14
15 #[error("Authentication error: {0}")]
17 AuthenticationError(String),
18
19 #[error("Permission denied: {0}")]
21 PermissionDenied(String),
22
23 #[error("File not found: {0}")]
25 FileNotFound(String),
26
27 #[error("File already exists: {0}")]
29 FileAlreadyExists(String),
30
31 #[error("Network error: {0}")]
33 NetworkError(String),
34
35
36 #[error("Filesystem error: {0}")]
38 FilesystemError(#[from] std::io::Error),
39
40 #[error("Invalid URL: {0}")]
42 InvalidUrl(#[from] url::ParseError),
43
44 #[error("Serialization error: {0}")]
46 SerializationError(#[from] serde_json::Error),
47
48 #[error("Base64 error: {0}")]
50 Base64Error(#[from] base64::DecodeError),
51
52 #[error("Invalid file format: {0}")]
54 InvalidFileFormat(String),
55
56 #[error("File too large: {size} bytes, maximum allowed: {max_size} bytes")]
58 FileTooLarge {
59 size: u64,
60 max_size: u64,
61 },
62
63 #[error("Checksum mismatch: expected {expected}, got {actual}")]
65 ChecksumMismatch {
66 expected: String,
67 actual: String,
68 },
69
70 #[error("Upload interrupted: {0}")]
72 UploadInterrupted(String),
73
74 #[error("Download interrupted: {0}")]
76 DownloadInterrupted(String),
77
78 #[error("Multipart upload error: {0}")]
80 MultipartUploadError(String),
81
82 #[error("Presigned URL generation failed: {0}")]
84 PresignedUrlError(String),
85
86 #[error("Temporary storage error: {0}")]
88 TemporaryStorageError(String),
89
90 #[error("Storage quota exceeded: {used}/{limit} bytes")]
92 QuotaExceeded {
93 used: u64,
94 limit: u64,
95 },
96
97 #[error("Rate limited: try again in {retry_after} seconds")]
99 RateLimited {
100 retry_after: u64,
101 },
102
103 #[error("Invalid range request: {0}")]
105 InvalidRange(String),
106
107 #[error("Operation timed out after {timeout} seconds")]
109 Timeout {
110 timeout: u64,
111 },
112
113 #[error("Operation failed: {operation} - {message}")]
115 OperationFailed {
116 operation: String,
117 message: String,
118 },
119
120 #[error("Unsupported operation '{operation}' for backend '{backend}'")]
122 UnsupportedOperation {
123 operation: String,
124 backend: String,
125 },
126
127 #[error("S3 operation failed: {message}")]
129 S3OperationFailed {
130 message: String,
131 request_id: Option<String>,
132 },
133
134 #[error("GCS error: {0}")]
136 GcsError(String),
137
138 #[error("MinIO error: {0}")]
140 MinIOError(String),
141
142 #[error("HTTP error: {0}")]
144 HttpError(#[from] reqwest::Error),
145
146 #[error("Security error: {operation} - {message}")]
148 SecurityError {
149 operation: String,
150 message: String,
151 },
152
153 #[error("{0}")]
155 Other(String),
156}
157
158impl StorageError {
159 pub fn config_error(message: impl Into<String>) -> Self {
161 Self::InvalidConfiguration(message.into())
162 }
163
164 pub fn auth_error(message: impl Into<String>) -> Self {
166 Self::AuthenticationError(message.into())
167 }
168
169 pub fn not_found(file_path: impl Into<String>) -> Self {
171 Self::FileNotFound(file_path.into())
172 }
173
174 pub fn operation_failed(operation: impl Into<String>, message: impl Into<String>) -> Self {
176 Self::OperationFailed {
177 operation: operation.into(),
178 message: message.into(),
179 }
180 }
181
182
183 pub fn is_retryable(&self) -> bool {
185 matches!(self,
186 Self::NetworkError(_)
187 | Self::HttpError(_)
188 | Self::Timeout { .. }
189 | Self::RateLimited { .. }
190 )
191 }
192
193 pub fn is_client_error(&self) -> bool {
195 match self {
196 Self::AuthenticationError(_)
197 | Self::PermissionDenied(_)
198 | Self::FileNotFound(_)
199 | Self::FileAlreadyExists(_)
200 | Self::InvalidUrl(_)
201 | Self::InvalidFileFormat(_)
202 | Self::FileTooLarge { .. }
203 | Self::ChecksumMismatch { .. }
204 | Self::InvalidRange(_)
205 | Self::UnsupportedOperation { .. } => true,
206 Self::HttpError(e) => e.status().is_some_and(|s| s.is_client_error()),
207 _ => false,
208 }
209 }
210
211 pub fn is_server_error(&self) -> bool {
213 match self {
214 Self::NetworkError(_)
215 | Self::TemporaryStorageError(_) => true,
216 Self::HttpError(e) => e.status().is_some_and(|s| s.is_server_error()),
217 _ => false,
218 }
219 }
220
221 pub fn category(&self) -> &'static str {
223 match self {
224 Self::InvalidConfiguration(_) => "configuration",
225 Self::AuthenticationError(_) | Self::PermissionDenied(_) => "authentication",
226 Self::FileNotFound(_) | Self::FileAlreadyExists(_) => "file_system",
227 Self::NetworkError(_) | Self::HttpError(_) => "network",
228 Self::FilesystemError(_) => "filesystem",
229 Self::SerializationError(_) | Self::Base64Error(_) => "serialization",
230 Self::FileTooLarge { .. } | Self::QuotaExceeded { .. } => "quota",
231 Self::RateLimited { .. } => "rate_limit",
232 Self::Timeout { .. } => "timeout",
233 _ => "other",
234 }
235 }
236}
237
238impl From<reqwest::Response> for StorageError {
240 fn from(response: reqwest::Response) -> Self {
241 let status = response.status();
242 let url = response.url().clone();
243
244 match status.as_u16() {
245 400 => Self::InvalidConfiguration(format!("Bad request to {}", url)),
246 401 => Self::AuthenticationError("Unauthorized".to_string()),
247 403 => Self::PermissionDenied(format!("Access denied to {}", url)),
248 404 => Self::FileNotFound(format!("Resource not found at {}", url)),
249 409 => Self::FileAlreadyExists(format!("Resource already exists at {}", url)),
250 413 => Self::FileTooLarge { size: 0, max_size: 0 },
251 416 => Self::InvalidRange("Invalid range request".to_string()),
252 429 => Self::RateLimited { retry_after: 60 },
253 500..=599 => Self::NetworkError(format!("Server error {} from {}", status, url)),
254 _ => Self::Other(format!("HTTP {} from {}", status, url)),
255 }
256 }
257}