use reqwest::StatusCode;
use serde::{Deserialize, Deserializer};
#[derive(Debug, thiserror::Error)]
pub enum StorageError {
#[error("storage API error ({status}): {body}")]
Api {
status: StatusCode,
body: ApiErrorBody,
},
#[error("invalid storage path {path:?}: {reason}")]
InvalidPath {
path: String,
reason: PathError,
},
#[error("project URL cannot be a base URL")]
InvalidBaseUrl,
#[error("signed URL from the storage API has no token: {0:?}")]
MissingSignedToken(String),
#[error("storage API did not sign {path:?}: {message}")]
SignFailed {
path: String,
message: String,
},
#[error(transparent)]
Url(#[from] url::ParseError),
#[error(transparent)]
Http(#[from] reqwest::Error),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
InvalidHeader(#[from] reqwest::header::InvalidHeaderValue),
}
impl StorageError {
#[must_use]
pub fn api_status(&self) -> Option<StatusCode> {
let Self::Api { status, body } = self else {
return None;
};
let reported = match body {
ApiErrorBody::Storage(StorageErrorBody {
status_code: Some(code),
..
}) => code.parse::<u16>().ok(),
ApiErrorBody::Storage(_) | ApiErrorBody::Raw(_) => None,
};
Some(
reported
.and_then(|code| StatusCode::from_u16(code).ok())
.unwrap_or(*status),
)
}
#[must_use]
pub const fn code(&self) -> Option<&StorageErrorCode> {
let Self::Api {
body: ApiErrorBody::Storage(body),
..
} = self
else {
return None;
};
body.code.as_ref()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum PathError {
#[error("path is empty")]
Empty,
#[error("path contains an empty segment")]
EmptySegment,
#[error("path contains a `.` or `..` segment")]
DotSegment,
#[error("path contains a tab, carriage return, or line feed")]
ControlCharacter,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApiErrorBody {
Storage(StorageErrorBody),
Raw(String),
}
impl ApiErrorBody {
pub(crate) fn decode(text: String) -> Self {
match serde_json::from_str::<StorageErrorBody>(&text) {
Ok(body)
if body.status_code.is_some()
|| body.code.is_some()
|| body.error.is_some()
|| body.message.is_some() =>
{
Self::Storage(body)
}
Ok(_) | Err(_) => Self::Raw(text),
}
}
}
impl core::fmt::Display for ApiErrorBody {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Storage(body) => {
let label = body
.code
.as_ref()
.map(StorageErrorCode::as_str)
.or(body.error.as_deref());
match (label, body.message.as_deref()) {
(Some(label), Some(message)) => write!(formatter, "{label}: {message}"),
(Some(text), None) | (None, Some(text)) => formatter.write_str(text),
(None, None) => Ok(()),
}
}
Self::Raw(text) => formatter.write_str(text),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct StorageErrorBody {
#[serde(rename = "statusCode", default, deserialize_with = "string_or_number")]
pub status_code: Option<String>,
#[serde(default)]
pub code: Option<StorageErrorCode>,
#[serde(default)]
pub error: Option<String>,
#[serde(default)]
pub message: Option<String>,
}
fn string_or_number<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<String>, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum Code {
Text(String),
Number(u64),
}
Ok(
Option::<Code>::deserialize(deserializer)?.map(|code| match code {
Code::Text(text) => text,
Code::Number(number) => number.to_string(),
}),
)
}
macro_rules! storage_error_codes {
($($variant:ident => $wire:literal,)+) => {
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
#[serde(from = "String")]
#[non_exhaustive]
pub enum StorageErrorCode {
$(
#[doc = concat!("`", $wire, "`")]
$variant,
)+
Other(String),
}
impl StorageErrorCode {
#[must_use]
pub fn as_str(&self) -> &str {
match self {
$(Self::$variant => $wire,)+
Self::Other(code) => code,
}
}
}
impl From<String> for StorageErrorCode {
fn from(code: String) -> Self {
match code.as_str() {
$($wire => Self::$variant,)+
_ => Self::Other(code),
}
}
}
};
}
storage_error_codes! {
NoSuchBucket => "NoSuchBucket",
NoSuchKey => "NoSuchKey",
NoSuchUpload => "NoSuchUpload",
InvalidJwt => "InvalidJWT",
InvalidRequest => "InvalidRequest",
InvalidArgument => "InvalidArgument",
TenantNotFound => "TenantNotFound",
EntityTooLarge => "EntityTooLarge",
EntityTooSmall => "EntityTooSmall",
InternalError => "InternalError",
ResourceAlreadyExists => "ResourceAlreadyExists",
ResourceNotEmpty => "ResourceNotEmpty",
InvalidBucketName => "InvalidBucketName",
InvalidKey => "InvalidKey",
InvalidRange => "InvalidRange",
InvalidMimeType => "InvalidMimeType",
InvalidUploadId => "InvalidUploadId",
KeyAlreadyExists => "KeyAlreadyExists",
BucketAlreadyExists => "BucketAlreadyExists",
DatabaseTimeout => "DatabaseTimeout",
DatabaseReadOnly => "DatabaseReadOnly",
InvalidSignature => "InvalidSignature",
ExpiredToken => "ExpiredToken",
SignatureDoesNotMatch => "SignatureDoesNotMatch",
AccessDenied => "AccessDenied",
ResourceLocked => "ResourceLocked",
ResourceReferenced => "ResourceReferenced",
DatabaseError => "DatabaseError",
MissingContentLength => "MissingContentLength",
MissingParameter => "MissingParameter",
InvalidParameter => "InvalidParameter",
InvalidUploadSignature => "InvalidUploadSignature",
LockTimeout => "LockTimeout",
PreconditionFailed => "PreconditionFailed",
InvalidChecksum => "InvalidChecksum",
SlowDown => "SlowDown",
FeatureNotEnabled => "FeatureNotEnabled",
NotSupported => "NotSupported",
UnknownError => "UnknownError",
}
impl core::fmt::Display for StorageErrorCode {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter.write_str(self.as_str())
}
}