Skip to main content

oxigdal_cloud/
error.rs

1//! Error types for cloud storage operations
2//!
3//! This module provides a comprehensive error hierarchy for all cloud storage operations,
4//! including S3, Azure Blob Storage, Google Cloud Storage, HTTP, authentication, and caching.
5
6use oxigdal_core::error::{IoError, OxiGdalError};
7
8/// Result type for cloud storage operations
9pub type Result<T> = core::result::Result<T, CloudError>;
10
11/// Main error type for cloud storage operations
12#[derive(Debug, thiserror::Error)]
13pub enum CloudError {
14    /// I/O error
15    #[error("I/O error: {0}")]
16    Io(#[from] IoError),
17
18    /// AWS S3 error
19    #[error("S3 error: {0}")]
20    S3(#[from] S3Error),
21
22    /// Azure Blob Storage error
23    #[error("Azure error: {0}")]
24    Azure(#[from] AzureError),
25
26    /// Google Cloud Storage error
27    #[error("GCS error: {0}")]
28    Gcs(#[from] GcsError),
29
30    /// HTTP error
31    #[error("HTTP error: {0}")]
32    Http(#[from] HttpError),
33
34    /// Authentication error
35    #[error("Authentication error: {0}")]
36    Auth(#[from] AuthError),
37
38    /// Retry error
39    #[error("Retry error: {0}")]
40    Retry(#[from] RetryError),
41
42    /// Cache error
43    #[error("Cache error: {0}")]
44    Cache(#[from] CacheError),
45
46    /// Invalid URL
47    #[error("Invalid URL: {url}")]
48    InvalidUrl {
49        /// The invalid URL
50        url: String,
51    },
52
53    /// Unsupported protocol
54    #[error("Unsupported protocol: {protocol}")]
55    UnsupportedProtocol {
56        /// Protocol name
57        protocol: String,
58    },
59
60    /// Object not found
61    #[error("Object not found: {key}")]
62    NotFound {
63        /// Object key/path
64        key: String,
65    },
66
67    /// Permission denied
68    #[error("Permission denied: {message}")]
69    PermissionDenied {
70        /// Error message
71        message: String,
72    },
73
74    /// Operation timeout
75    #[error("Operation timeout: {message}")]
76    Timeout {
77        /// Error message
78        message: String,
79    },
80
81    /// Rate limit exceeded
82    #[error("Rate limit exceeded: {message}")]
83    RateLimitExceeded {
84        /// Error message
85        message: String,
86    },
87
88    /// Invalid configuration
89    #[error("Invalid configuration: {message}")]
90    InvalidConfiguration {
91        /// Error message
92        message: String,
93    },
94
95    /// Operation not supported
96    #[error("Operation not supported: {operation}")]
97    NotSupported {
98        /// Operation description
99        operation: String,
100    },
101
102    /// Internal error
103    #[error("Internal error: {message}")]
104    Internal {
105        /// Error message
106        message: String,
107    },
108}
109
110/// AWS S3-specific errors
111#[derive(Debug, thiserror::Error)]
112pub enum S3Error {
113    /// SDK error
114    #[error("S3 SDK error: {message}")]
115    Sdk {
116        /// Error message
117        message: String,
118    },
119
120    /// Bucket not found
121    #[error("Bucket not found: {bucket}")]
122    BucketNotFound {
123        /// Bucket name
124        bucket: String,
125    },
126
127    /// Access denied
128    #[error("Access denied to bucket '{bucket}': {message}")]
129    AccessDenied {
130        /// Bucket name
131        bucket: String,
132        /// Error message
133        message: String,
134    },
135
136    /// Invalid bucket name
137    #[error("Invalid bucket name: {bucket}")]
138    InvalidBucketName {
139        /// Bucket name
140        bucket: String,
141    },
142
143    /// Object too large
144    #[error("Object too large: {size} bytes (max: {max_size})")]
145    ObjectTooLarge {
146        /// Object size
147        size: u64,
148        /// Maximum allowed size
149        max_size: u64,
150    },
151
152    /// Multipart upload error
153    #[error("Multipart upload error: {message}")]
154    MultipartUpload {
155        /// Error message
156        message: String,
157    },
158
159    /// STS assume role error
160    #[error("STS assume role error: {message}")]
161    StsAssumeRole {
162        /// Error message
163        message: String,
164    },
165
166    /// Region error
167    #[error("Region error: {message}")]
168    Region {
169        /// Error message
170        message: String,
171    },
172}
173
174/// Azure Blob Storage-specific errors
175#[derive(Debug, thiserror::Error)]
176pub enum AzureError {
177    /// SDK error
178    #[error("Azure SDK error: {message}")]
179    Sdk {
180        /// Error message
181        message: String,
182    },
183
184    /// Container not found
185    #[error("Container not found: {container}")]
186    ContainerNotFound {
187        /// Container name
188        container: String,
189    },
190
191    /// Blob not found
192    #[error("Blob not found: {blob}")]
193    BlobNotFound {
194        /// Blob name
195        blob: String,
196    },
197
198    /// Access denied
199    #[error("Access denied to container '{container}': {message}")]
200    AccessDenied {
201        /// Container name
202        container: String,
203        /// Error message
204        message: String,
205    },
206
207    /// Invalid SAS token
208    #[error("Invalid SAS token: {message}")]
209    InvalidSasToken {
210        /// Error message
211        message: String,
212    },
213
214    /// Account error
215    #[error("Account error: {message}")]
216    Account {
217        /// Error message
218        message: String,
219    },
220
221    /// Lease error
222    #[error("Lease error: {message}")]
223    Lease {
224        /// Error message
225        message: String,
226    },
227}
228
229/// Google Cloud Storage-specific errors
230#[derive(Debug, thiserror::Error)]
231pub enum GcsError {
232    /// SDK error
233    #[error("GCS SDK error: {message}")]
234    Sdk {
235        /// Error message
236        message: String,
237    },
238
239    /// Bucket not found
240    #[error("Bucket not found: {bucket}")]
241    BucketNotFound {
242        /// Bucket name
243        bucket: String,
244    },
245
246    /// Object not found
247    #[error("Object not found: {object}")]
248    ObjectNotFound {
249        /// Object name
250        object: String,
251    },
252
253    /// Access denied
254    #[error("Access denied to bucket '{bucket}': {message}")]
255    AccessDenied {
256        /// Bucket name
257        bucket: String,
258        /// Error message
259        message: String,
260    },
261
262    /// Invalid project ID
263    #[error("Invalid project ID: {project_id}")]
264    InvalidProjectId {
265        /// Project ID
266        project_id: String,
267    },
268
269    /// Service account error
270    #[error("Service account error: {message}")]
271    ServiceAccount {
272        /// Error message
273        message: String,
274    },
275
276    /// Signed URL error
277    #[error("Signed URL error: {message}")]
278    SignedUrl {
279        /// Error message
280        message: String,
281    },
282}
283
284/// HTTP-specific errors
285#[derive(Debug, thiserror::Error)]
286pub enum HttpError {
287    /// Network error
288    #[error("Network error: {message}")]
289    Network {
290        /// Error message
291        message: String,
292    },
293
294    /// HTTP status error
295    #[error("HTTP {status}: {message}")]
296    Status {
297        /// HTTP status code
298        status: u16,
299        /// Error message
300        message: String,
301    },
302
303    /// Invalid header
304    #[error("Invalid header '{name}': {message}")]
305    InvalidHeader {
306        /// Header name
307        name: String,
308        /// Error message
309        message: String,
310    },
311
312    /// Request build error
313    #[error("Request build error: {message}")]
314    RequestBuild {
315        /// Error message
316        message: String,
317    },
318
319    /// Response parse error
320    #[error("Response parse error: {message}")]
321    ResponseParse {
322        /// Error message
323        message: String,
324    },
325
326    /// TLS error
327    #[error("TLS error: {message}")]
328    Tls {
329        /// Error message
330        message: String,
331    },
332}
333
334/// Authentication-specific errors
335#[derive(Debug, thiserror::Error)]
336pub enum AuthError {
337    /// Credentials not found
338    #[error("Credentials not found: {message}")]
339    CredentialsNotFound {
340        /// Error message
341        message: String,
342    },
343
344    /// Invalid credentials
345    #[error("Invalid credentials: {message}")]
346    InvalidCredentials {
347        /// Error message
348        message: String,
349    },
350
351    /// Token expired
352    #[error("Token expired: {message}")]
353    TokenExpired {
354        /// Error message
355        message: String,
356    },
357
358    /// OAuth2 error
359    #[error("OAuth2 error: {message}")]
360    OAuth2 {
361        /// Error message
362        message: String,
363    },
364
365    /// Service account key error
366    #[error("Service account key error: {message}")]
367    ServiceAccountKey {
368        /// Error message
369        message: String,
370    },
371
372    /// API key error
373    #[error("API key error: {message}")]
374    ApiKey {
375        /// Error message
376        message: String,
377    },
378
379    /// SAS token error
380    #[error("SAS token error: {message}")]
381    SasToken {
382        /// Error message
383        message: String,
384    },
385
386    /// IAM role error
387    #[error("IAM role error: {message}")]
388    IamRole {
389        /// Error message
390        message: String,
391    },
392}
393
394/// Retry-specific errors
395#[derive(Debug, thiserror::Error)]
396pub enum RetryError {
397    /// Maximum retries exceeded
398    #[error("Maximum retries exceeded: {attempts} attempts")]
399    MaxRetriesExceeded {
400        /// Number of attempts
401        attempts: usize,
402    },
403
404    /// Circuit breaker open
405    #[error("Circuit breaker open: {message}")]
406    CircuitBreakerOpen {
407        /// Error message
408        message: String,
409    },
410
411    /// Retry budget exhausted
412    #[error("Retry budget exhausted: {message}")]
413    BudgetExhausted {
414        /// Error message
415        message: String,
416    },
417
418    /// Non-retryable error
419    #[error("Non-retryable error: {message}")]
420    NonRetryable {
421        /// Error message
422        message: String,
423    },
424}
425
426/// Cache-specific errors
427#[derive(Debug, thiserror::Error)]
428pub enum CacheError {
429    /// Cache miss
430    #[error("Cache miss for key: {key}")]
431    Miss {
432        /// Cache key
433        key: String,
434    },
435
436    /// Cache write error
437    #[error("Cache write error: {message}")]
438    WriteError {
439        /// Error message
440        message: String,
441    },
442
443    /// Cache read error
444    #[error("Cache read error: {message}")]
445    ReadError {
446        /// Error message
447        message: String,
448    },
449
450    /// Cache invalidation error
451    #[error("Cache invalidation error: {message}")]
452    InvalidationError {
453        /// Error message
454        message: String,
455    },
456
457    /// Cache full
458    #[error("Cache full: {message}")]
459    Full {
460        /// Error message
461        message: String,
462    },
463
464    /// Compression error
465    #[error("Compression error: {message}")]
466    Compression {
467        /// Error message
468        message: String,
469    },
470
471    /// Decompression error
472    #[error("Decompression error: {message}")]
473    Decompression {
474        /// Error message
475        message: String,
476    },
477}
478
479// Conversions from OxiGDAL errors
480impl 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}