Skip to main content

xtax_blob_storage/
error.rs

1use std::fmt;
2
3/// Categorised error for a single key in a batch operation.
4#[derive(Debug, Clone)]
5pub enum PerKeyError {
6    /// The key was not found.
7    ///
8    /// Idempotent — on delete this is NOT an error (the key is treated as
9    /// successfully processed). On get / get_with_metadata the backend
10    /// returns `BlobStorageError::NotFound` directly.
11    NotFound,
12
13    /// The operation failed due to insufficient permissions.
14    PermissionDenied(String),
15
16    /// Any other unexpected error. The `message` contains the original error
17    /// description.
18    Unknown { message: String },
19}
20
21impl fmt::Display for PerKeyError {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        match self {
24            PerKeyError::NotFound => write!(f, "not found"),
25            PerKeyError::PermissionDenied(msg) => write!(f, "permission denied: {msg}"),
26            PerKeyError::Unknown { message } => write!(f, "unknown: {message}"),
27        }
28    }
29}
30
31/// A single failed key in a batch operation, with its categorised error.
32#[derive(Debug, Clone)]
33pub struct KeyError {
34    /// The blob key that failed.
35    pub key: String,
36    /// The categorised error.
37    pub error: PerKeyError,
38}
39
40/// Batch error — returned when at least one key in a batch operation failed.
41///
42/// Contains **all** keys that succeeded and those that failed.
43/// The caller can programmatically decide what to do next (e.g. retry failed keys).
44#[derive(Debug, Clone)]
45pub struct BatchError {
46    /// Keys that were processed successfully (including `NotFound` on delete).
47    pub succeeded: Vec<String>,
48    /// Keys that failed, with per-key error details.
49    pub errors: Vec<KeyError>,
50}
51
52impl BatchError {
53    /// Total number of keys processed.
54    pub fn total_count(&self) -> usize {
55        self.succeeded.len() + self.errors.len()
56    }
57
58    /// Number of keys that failed.
59    pub fn failed_count(&self) -> usize {
60        self.errors.len()
61    }
62}
63
64impl fmt::Display for BatchError {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        write!(
67            f,
68            "batch operation failed: {} keys failed ({} succeeded, {} total)",
69            self.failed_count(),
70            self.succeeded.len(),
71            self.total_count(),
72        )
73    }
74}
75
76impl std::error::Error for BatchError {}
77
78/// Blob storage error.
79#[derive(Debug, thiserror::Error)]
80pub enum BlobStorageError {
81    /// The requested blob was not found.
82    #[error("blob not found: {0}")]
83    NotFound(String),
84
85    /// A blob with this key already exists.
86    #[error("blob already exists: {0}")]
87    AlreadyExists(String),
88
89    /// The operation is not supported by this backend.
90    #[error("operation not supported: {0}")]
91    NotSupported(String),
92
93    /// The backend is misconfigured — for example, the S3 bucket does not
94    /// exist, or the FS root directory has been deleted.
95    ///
96    /// This is distinct from [`Storage`](Self::Storage) errors: it indicates
97    /// a backend configuration problem, not a transient storage failure.
98    #[error("backend misconfigured: {0}")]
99    BackendMisconfigured(String),
100
101    /// The provided input is invalid (empty key, path traversal, etc.).
102    #[error("invalid input: {0}")]
103    InvalidInput(String),
104
105    /// Backend storage error. The inner `String` provides context;
106    /// the optional `source` carries the underlying cause.
107    #[error("storage error: {message}")]
108    Storage {
109        message: String,
110        #[source]
111        source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
112    },
113
114    /// Encryption-layer error.
115    #[error("encryption error: {message}")]
116    Encryption {
117        message: String,
118        #[source]
119        source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
120    },
121
122    /// The caller does not have permission to perform this operation.
123    #[error("permission denied: {0}")]
124    PermissionDenied(String),
125
126    /// Batch operation partially failed.
127    /// Contains details about which keys succeeded and which failed.
128    #[error("batch error: {0}")]
129    Batch(#[from] BatchError),
130}
131
132pub type Result<T> = std::result::Result<T, BlobStorageError>;
133
134impl From<std::io::Error> for BlobStorageError {
135    fn from(e: std::io::Error) -> Self {
136        Self::Storage {
137            message: "I/O error".to_string(),
138            source: Some(Box::new(e)),
139        }
140    }
141}
142
143impl From<String> for BlobStorageError {
144    fn from(msg: String) -> Self {
145        Self::Storage {
146            message: msg,
147            source: None,
148        }
149    }
150}
151
152impl From<&str> for BlobStorageError {
153    fn from(msg: &str) -> Self {
154        Self::Storage {
155            message: msg.to_string(),
156            source: None,
157        }
158    }
159}
160
161impl From<xtax_encryption::EncryptionError> for BlobStorageError {
162    fn from(e: xtax_encryption::EncryptionError) -> Self {
163        Self::Encryption {
164            message: e.to_string(),
165            source: Some(Box::new(e)),
166        }
167    }
168}