1use oxigdal_core::error::{IoError, OxiGdalError};
7
8pub type Result<T> = core::result::Result<T, CloudError>;
10
11#[derive(Debug, thiserror::Error)]
13pub enum CloudError {
14 #[error("I/O error: {0}")]
16 Io(#[from] IoError),
17
18 #[error("S3 error: {0}")]
20 S3(#[from] S3Error),
21
22 #[error("Azure error: {0}")]
24 Azure(#[from] AzureError),
25
26 #[error("GCS error: {0}")]
28 Gcs(#[from] GcsError),
29
30 #[error("HTTP error: {0}")]
32 Http(#[from] HttpError),
33
34 #[error("Authentication error: {0}")]
36 Auth(#[from] AuthError),
37
38 #[error("Retry error: {0}")]
40 Retry(#[from] RetryError),
41
42 #[error("Cache error: {0}")]
44 Cache(#[from] CacheError),
45
46 #[error("Invalid URL: {url}")]
48 InvalidUrl {
49 url: String,
51 },
52
53 #[error("Unsupported protocol: {protocol}")]
55 UnsupportedProtocol {
56 protocol: String,
58 },
59
60 #[error("Object not found: {key}")]
62 NotFound {
63 key: String,
65 },
66
67 #[error("Permission denied: {message}")]
69 PermissionDenied {
70 message: String,
72 },
73
74 #[error("Operation timeout: {message}")]
76 Timeout {
77 message: String,
79 },
80
81 #[error("Rate limit exceeded: {message}")]
83 RateLimitExceeded {
84 message: String,
86 },
87
88 #[error("Invalid configuration: {message}")]
90 InvalidConfiguration {
91 message: String,
93 },
94
95 #[error("Operation not supported: {operation}")]
97 NotSupported {
98 operation: String,
100 },
101
102 #[error("Internal error: {message}")]
104 Internal {
105 message: String,
107 },
108}
109
110#[derive(Debug, thiserror::Error)]
112pub enum S3Error {
113 #[error("S3 SDK error: {message}")]
115 Sdk {
116 message: String,
118 },
119
120 #[error("Bucket not found: {bucket}")]
122 BucketNotFound {
123 bucket: String,
125 },
126
127 #[error("Access denied to bucket '{bucket}': {message}")]
129 AccessDenied {
130 bucket: String,
132 message: String,
134 },
135
136 #[error("Invalid bucket name: {bucket}")]
138 InvalidBucketName {
139 bucket: String,
141 },
142
143 #[error("Object too large: {size} bytes (max: {max_size})")]
145 ObjectTooLarge {
146 size: u64,
148 max_size: u64,
150 },
151
152 #[error("Multipart upload error: {message}")]
154 MultipartUpload {
155 message: String,
157 },
158
159 #[error("STS assume role error: {message}")]
161 StsAssumeRole {
162 message: String,
164 },
165
166 #[error("Region error: {message}")]
168 Region {
169 message: String,
171 },
172}
173
174#[derive(Debug, thiserror::Error)]
176pub enum AzureError {
177 #[error("Azure SDK error: {message}")]
179 Sdk {
180 message: String,
182 },
183
184 #[error("Container not found: {container}")]
186 ContainerNotFound {
187 container: String,
189 },
190
191 #[error("Blob not found: {blob}")]
193 BlobNotFound {
194 blob: String,
196 },
197
198 #[error("Access denied to container '{container}': {message}")]
200 AccessDenied {
201 container: String,
203 message: String,
205 },
206
207 #[error("Invalid SAS token: {message}")]
209 InvalidSasToken {
210 message: String,
212 },
213
214 #[error("Account error: {message}")]
216 Account {
217 message: String,
219 },
220
221 #[error("Lease error: {message}")]
223 Lease {
224 message: String,
226 },
227}
228
229#[derive(Debug, thiserror::Error)]
231pub enum GcsError {
232 #[error("GCS SDK error: {message}")]
234 Sdk {
235 message: String,
237 },
238
239 #[error("Bucket not found: {bucket}")]
241 BucketNotFound {
242 bucket: String,
244 },
245
246 #[error("Object not found: {object}")]
248 ObjectNotFound {
249 object: String,
251 },
252
253 #[error("Access denied to bucket '{bucket}': {message}")]
255 AccessDenied {
256 bucket: String,
258 message: String,
260 },
261
262 #[error("Invalid project ID: {project_id}")]
264 InvalidProjectId {
265 project_id: String,
267 },
268
269 #[error("Service account error: {message}")]
271 ServiceAccount {
272 message: String,
274 },
275
276 #[error("Signed URL error: {message}")]
278 SignedUrl {
279 message: String,
281 },
282}
283
284#[derive(Debug, thiserror::Error)]
286pub enum HttpError {
287 #[error("Network error: {message}")]
289 Network {
290 message: String,
292 },
293
294 #[error("HTTP {status}: {message}")]
296 Status {
297 status: u16,
299 message: String,
301 },
302
303 #[error("Invalid header '{name}': {message}")]
305 InvalidHeader {
306 name: String,
308 message: String,
310 },
311
312 #[error("Request build error: {message}")]
314 RequestBuild {
315 message: String,
317 },
318
319 #[error("Response parse error: {message}")]
321 ResponseParse {
322 message: String,
324 },
325
326 #[error("TLS error: {message}")]
328 Tls {
329 message: String,
331 },
332}
333
334#[derive(Debug, thiserror::Error)]
336pub enum AuthError {
337 #[error("Credentials not found: {message}")]
339 CredentialsNotFound {
340 message: String,
342 },
343
344 #[error("Invalid credentials: {message}")]
346 InvalidCredentials {
347 message: String,
349 },
350
351 #[error("Token expired: {message}")]
353 TokenExpired {
354 message: String,
356 },
357
358 #[error("OAuth2 error: {message}")]
360 OAuth2 {
361 message: String,
363 },
364
365 #[error("Service account key error: {message}")]
367 ServiceAccountKey {
368 message: String,
370 },
371
372 #[error("API key error: {message}")]
374 ApiKey {
375 message: String,
377 },
378
379 #[error("SAS token error: {message}")]
381 SasToken {
382 message: String,
384 },
385
386 #[error("IAM role error: {message}")]
388 IamRole {
389 message: String,
391 },
392}
393
394#[derive(Debug, thiserror::Error)]
396pub enum RetryError {
397 #[error("Maximum retries exceeded: {attempts} attempts")]
399 MaxRetriesExceeded {
400 attempts: usize,
402 },
403
404 #[error("Circuit breaker open: {message}")]
406 CircuitBreakerOpen {
407 message: String,
409 },
410
411 #[error("Retry budget exhausted: {message}")]
413 BudgetExhausted {
414 message: String,
416 },
417
418 #[error("Non-retryable error: {message}")]
420 NonRetryable {
421 message: String,
423 },
424}
425
426#[derive(Debug, thiserror::Error)]
428pub enum CacheError {
429 #[error("Cache miss for key: {key}")]
431 Miss {
432 key: String,
434 },
435
436 #[error("Cache write error: {message}")]
438 WriteError {
439 message: String,
441 },
442
443 #[error("Cache read error: {message}")]
445 ReadError {
446 message: String,
448 },
449
450 #[error("Cache invalidation error: {message}")]
452 InvalidationError {
453 message: String,
455 },
456
457 #[error("Cache full: {message}")]
459 Full {
460 message: String,
462 },
463
464 #[error("Compression error: {message}")]
466 Compression {
467 message: String,
469 },
470
471 #[error("Decompression error: {message}")]
473 Decompression {
474 message: String,
476 },
477}
478
479impl From<OxiGdalError> for CloudError {
481 fn from(err: OxiGdalError) -> Self {
482 match err {
483 OxiGdalError::Io(e) => Self::Io(e),
484 OxiGdalError::NotSupported { operation } => Self::NotSupported { operation },
485 OxiGdalError::Internal { message } => Self::Internal { message },
486 _ => Self::Internal {
487 message: format!("{err}"),
488 },
489 }
490 }
491}
492
493#[cfg(feature = "std")]
494impl From<std::io::Error> for CloudError {
495 fn from(err: std::io::Error) -> Self {
496 Self::Io(err.into())
497 }
498}
499
500impl From<url::ParseError> for CloudError {
501 fn from(err: url::ParseError) -> Self {
502 Self::InvalidUrl {
503 url: err.to_string(),
504 }
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511
512 #[test]
513 fn test_error_display() {
514 let err = CloudError::NotFound {
515 key: "test/file.txt".to_string(),
516 };
517 assert!(err.to_string().contains("test/file.txt"));
518 }
519
520 #[test]
521 fn test_s3_error() {
522 let err = S3Error::BucketNotFound {
523 bucket: "my-bucket".to_string(),
524 };
525 assert!(err.to_string().contains("my-bucket"));
526 }
527
528 #[test]
529 fn test_azure_error() {
530 let err = AzureError::ContainerNotFound {
531 container: "my-container".to_string(),
532 };
533 assert!(err.to_string().contains("my-container"));
534 }
535
536 #[test]
537 fn test_gcs_error() {
538 let err = GcsError::BucketNotFound {
539 bucket: "my-bucket".to_string(),
540 };
541 assert!(err.to_string().contains("my-bucket"));
542 }
543
544 #[test]
545 fn test_auth_error() {
546 let err = AuthError::TokenExpired {
547 message: "Token expired at 2026-01-25".to_string(),
548 };
549 assert!(err.to_string().contains("expired"));
550 }
551
552 #[test]
553 fn test_retry_error() {
554 let err = RetryError::MaxRetriesExceeded { attempts: 5 };
555 assert!(err.to_string().contains("5"));
556 }
557
558 #[test]
559 fn test_cache_error() {
560 let err = CacheError::Miss {
561 key: "cache-key".to_string(),
562 };
563 assert!(err.to_string().contains("cache-key"));
564 }
565}