use rustack_s3_model::error::{S3Error, S3ErrorCode};
#[derive(Debug, thiserror::Error)]
pub enum S3ServiceError {
#[error("The specified bucket does not exist: {bucket}")]
NoSuchBucket {
bucket: String,
},
#[error("The requested bucket name is not available: {bucket}")]
BucketAlreadyExists {
bucket: String,
},
#[error(
"Your previous request to create the named bucket succeeded and you already own it: \
{bucket}"
)]
BucketAlreadyOwnedByYou {
bucket: String,
},
#[error("The bucket you tried to delete is not empty: {bucket}")]
BucketNotEmpty {
bucket: String,
},
#[error("The specified key does not exist: {key}")]
NoSuchKey {
key: String,
},
#[error("The specified version does not exist: key={key}, version_id={version_id}")]
NoSuchVersion {
key: String,
version_id: String,
},
#[error("The specified upload does not exist: {upload_id}")]
NoSuchUpload {
upload_id: String,
},
#[error("The list of parts was not in ascending order")]
InvalidPartOrder,
#[error("One or more of the specified parts could not be found")]
InvalidPart,
#[error("Your proposed upload is smaller than the minimum allowed object size")]
EntityTooSmall,
#[error("Your proposed upload exceeds the maximum allowed object size")]
EntityTooLarge,
#[error("Invalid bucket name: {name}: {reason}")]
InvalidBucketName {
name: String,
reason: String,
},
#[error("Invalid argument: {message}")]
InvalidArgument {
message: String,
},
#[error("The requested range is not satisfiable")]
InvalidRange,
#[error("Invalid tag: {message}")]
InvalidTag {
message: String,
},
#[error("The XML you provided was not well-formed")]
MalformedXml,
#[error("Access Denied")]
AccessDenied,
#[error("The specified method is not allowed against this resource")]
MethodNotAllowed,
#[error("A header you provided implies functionality that is not implemented")]
NotImplemented,
#[error("At least one of the preconditions you specified did not hold")]
PreconditionFailed,
#[error("The conditional request cannot be processed")]
ConditionalRequestConflict,
#[error("Not Modified")]
NotModified,
#[error("The operation is not valid for the object's storage class")]
InvalidObjectState,
#[error("The source object of the COPY action is not in the active tier")]
ObjectNotInActiveTierError,
#[error("The Content-MD5 you specified is not valid")]
InvalidDigest,
#[error("The Content-MD5 you specified did not match what we received")]
BadDigest,
#[error("You must provide the Content-Length HTTP header")]
MissingContentLength,
#[error("Your key is too long")]
KeyTooLong,
#[error("Your request was too big")]
MaxMessageLengthExceeded,
#[error("The CORS configuration does not exist")]
NoSuchCorsConfiguration,
#[error("The TagSet does not exist")]
NoSuchTagSet,
#[error("The lifecycle configuration does not exist")]
NoSuchLifecycleConfiguration,
#[error("The bucket policy does not exist")]
NoSuchBucketPolicy,
#[error("The specified bucket does not have a website configuration")]
NoSuchWebsiteConfiguration,
#[error("The public access block configuration was not found")]
NoSuchPublicAccessBlockConfiguration,
#[error("The server-side encryption configuration was not found")]
ServerSideEncryptionConfigurationNotFoundError,
#[error("Object Lock configuration does not exist for this bucket")]
ObjectLockConfigurationNotFoundError,
#[error("The bucket ownership controls were not found")]
OwnershipControlsNotFoundError,
#[error("The replication configuration was not found")]
ReplicationConfigurationNotFoundError,
#[error(transparent)]
Internal(#[from] anyhow::Error),
}
impl S3ServiceError {
#[must_use]
pub fn into_s3_error(self) -> S3Error {
S3Error::from(self)
}
}
impl From<S3ServiceError> for S3Error {
fn from(err: S3ServiceError) -> Self {
let code = error_code(&err);
let message = match &err {
S3ServiceError::InvalidArgument { message }
| S3ServiceError::InvalidTag { message } => message.clone(),
S3ServiceError::InvalidBucketName { name, reason } => {
format!("Invalid bucket name: {name}: {reason}")
}
S3ServiceError::Internal(e) => e.to_string(),
_ => code.default_message().to_owned(),
};
S3Error::with_message(code, message)
}
}
fn error_code(err: &S3ServiceError) -> S3ErrorCode {
match err {
S3ServiceError::NoSuchBucket { .. } => S3ErrorCode::NoSuchBucket,
S3ServiceError::BucketAlreadyExists { .. } => S3ErrorCode::BucketAlreadyExists,
S3ServiceError::BucketAlreadyOwnedByYou { .. } => S3ErrorCode::BucketAlreadyOwnedByYou,
S3ServiceError::BucketNotEmpty { .. } => S3ErrorCode::BucketNotEmpty,
S3ServiceError::NoSuchKey { .. } => S3ErrorCode::NoSuchKey,
S3ServiceError::NoSuchVersion { .. } => S3ErrorCode::NoSuchVersion,
S3ServiceError::NoSuchUpload { .. } => S3ErrorCode::NoSuchUpload,
S3ServiceError::InvalidPartOrder => S3ErrorCode::InvalidPartOrder,
S3ServiceError::InvalidPart => S3ErrorCode::InvalidPart,
S3ServiceError::EntityTooSmall => S3ErrorCode::EntityTooSmall,
S3ServiceError::EntityTooLarge => S3ErrorCode::EntityTooLarge,
S3ServiceError::InvalidBucketName { .. } => S3ErrorCode::InvalidBucketName,
S3ServiceError::InvalidArgument { .. } | S3ServiceError::InvalidTag { .. } => {
S3ErrorCode::InvalidArgument
}
S3ServiceError::InvalidRange => S3ErrorCode::InvalidRange,
S3ServiceError::MalformedXml => S3ErrorCode::MalformedXML,
S3ServiceError::AccessDenied => S3ErrorCode::AccessDenied,
S3ServiceError::MethodNotAllowed => S3ErrorCode::MethodNotAllowed,
S3ServiceError::NotImplemented => S3ErrorCode::NotImplemented,
S3ServiceError::PreconditionFailed => S3ErrorCode::PreconditionFailed,
S3ServiceError::ConditionalRequestConflict => S3ErrorCode::ConditionalRequestConflict,
S3ServiceError::NotModified => S3ErrorCode::NotModified,
S3ServiceError::InvalidObjectState => S3ErrorCode::InvalidObjectState,
S3ServiceError::ObjectNotInActiveTierError => S3ErrorCode::ObjectNotInActiveTierError,
S3ServiceError::InvalidDigest => S3ErrorCode::InvalidDigest,
S3ServiceError::BadDigest => S3ErrorCode::BadDigest,
S3ServiceError::MissingContentLength => S3ErrorCode::MissingContentLength,
S3ServiceError::KeyTooLong => S3ErrorCode::KeyTooLongError,
S3ServiceError::MaxMessageLengthExceeded => S3ErrorCode::MaxMessageLengthExceeded,
S3ServiceError::NoSuchCorsConfiguration => S3ErrorCode::NoSuchCORSConfiguration,
S3ServiceError::NoSuchTagSet => S3ErrorCode::NoSuchTagSet,
S3ServiceError::NoSuchLifecycleConfiguration => S3ErrorCode::NoSuchLifecycleConfiguration,
S3ServiceError::NoSuchBucketPolicy => S3ErrorCode::NoSuchBucketPolicy,
S3ServiceError::NoSuchWebsiteConfiguration => S3ErrorCode::NoSuchWebsiteConfiguration,
S3ServiceError::NoSuchPublicAccessBlockConfiguration => {
S3ErrorCode::NoSuchPublicAccessBlockConfiguration
}
S3ServiceError::ServerSideEncryptionConfigurationNotFoundError => {
S3ErrorCode::ServerSideEncryptionConfigurationNotFoundError
}
S3ServiceError::ObjectLockConfigurationNotFoundError => {
S3ErrorCode::ObjectLockConfigurationNotFoundError
}
S3ServiceError::OwnershipControlsNotFoundError => {
S3ErrorCode::OwnershipControlsNotFoundError
}
S3ServiceError::ReplicationConfigurationNotFoundError => {
S3ErrorCode::ReplicationConfigurationNotFoundError
}
S3ServiceError::Internal(_) => S3ErrorCode::InternalError,
}
}
pub type S3ServiceResult<T> = Result<T, S3ServiceError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_convert_no_such_bucket_to_s3_error() {
let err = S3ServiceError::NoSuchBucket {
bucket: "my-bucket".to_owned(),
};
let s3_err: S3Error = err.into();
assert_eq!(s3_err.code, S3ErrorCode::NoSuchBucket);
assert_eq!(s3_err.message, "The specified bucket does not exist");
}
#[test]
fn test_should_convert_no_such_key_to_s3_error() {
let err = S3ServiceError::NoSuchKey {
key: "path/to/obj".to_owned(),
};
let s3_err: S3Error = err.into();
assert_eq!(s3_err.code, S3ErrorCode::NoSuchKey);
}
#[test]
fn test_should_convert_bucket_already_exists_to_s3_error() {
let err = S3ServiceError::BucketAlreadyExists {
bucket: "taken".to_owned(),
};
let s3_err: S3Error = err.into();
assert_eq!(s3_err.code, S3ErrorCode::BucketAlreadyExists);
}
#[test]
fn test_should_convert_bucket_already_owned_to_s3_error() {
let err = S3ServiceError::BucketAlreadyOwnedByYou {
bucket: "mine".to_owned(),
};
let s3_err: S3Error = err.into();
assert_eq!(s3_err.code, S3ErrorCode::BucketAlreadyOwnedByYou);
}
#[test]
fn test_should_convert_bucket_not_empty_to_s3_error() {
let err = S3ServiceError::BucketNotEmpty {
bucket: "full".to_owned(),
};
let s3_err: S3Error = err.into();
assert_eq!(s3_err.code, S3ErrorCode::BucketNotEmpty);
}
#[test]
fn test_should_convert_invalid_bucket_name_to_s3_error() {
let err = S3ServiceError::InvalidBucketName {
name: "BAD".to_owned(),
reason: "uppercase".to_owned(),
};
let s3_err: S3Error = err.into();
assert_eq!(s3_err.code, S3ErrorCode::InvalidBucketName);
}
#[test]
fn test_should_convert_entity_too_small_to_s3_error() {
let err = S3ServiceError::EntityTooSmall;
let s3_err: S3Error = err.into();
assert_eq!(s3_err.code, S3ErrorCode::EntityTooSmall);
}
#[test]
fn test_should_convert_access_denied_to_s3_error() {
let err = S3ServiceError::AccessDenied;
let s3_err: S3Error = err.into();
assert_eq!(s3_err.code, S3ErrorCode::AccessDenied);
}
#[test]
fn test_should_convert_internal_error_to_s3_error() {
let err = S3ServiceError::Internal(anyhow::anyhow!("disk I/O failure"));
let s3_err: S3Error = err.into();
assert_eq!(s3_err.code, S3ErrorCode::InternalError);
}
#[test]
fn test_should_use_into_s3_error_method() {
let err = S3ServiceError::InvalidRange;
let s3_err = err.into_s3_error();
assert_eq!(s3_err.code, S3ErrorCode::InvalidRange);
}
#[test]
fn test_should_convert_no_such_upload_to_s3_error() {
let err = S3ServiceError::NoSuchUpload {
upload_id: "abc123".to_owned(),
};
let s3_err: S3Error = err.into();
assert_eq!(s3_err.code, S3ErrorCode::NoSuchUpload);
}
#[test]
fn test_should_convert_precondition_failed_to_s3_error() {
let err = S3ServiceError::PreconditionFailed;
let s3_err: S3Error = err.into();
assert_eq!(s3_err.code, S3ErrorCode::PreconditionFailed);
}
#[test]
fn test_should_convert_config_not_found_errors() {
let cases: Vec<(S3ServiceError, S3ErrorCode)> = vec![
(
S3ServiceError::NoSuchCorsConfiguration,
S3ErrorCode::NoSuchCORSConfiguration,
),
(S3ServiceError::NoSuchTagSet, S3ErrorCode::NoSuchTagSet),
(
S3ServiceError::NoSuchLifecycleConfiguration,
S3ErrorCode::NoSuchLifecycleConfiguration,
),
(
S3ServiceError::NoSuchBucketPolicy,
S3ErrorCode::NoSuchBucketPolicy,
),
(
S3ServiceError::NoSuchWebsiteConfiguration,
S3ErrorCode::NoSuchWebsiteConfiguration,
),
(
S3ServiceError::NoSuchPublicAccessBlockConfiguration,
S3ErrorCode::NoSuchPublicAccessBlockConfiguration,
),
(
S3ServiceError::ObjectLockConfigurationNotFoundError,
S3ErrorCode::ObjectLockConfigurationNotFoundError,
),
(
S3ServiceError::OwnershipControlsNotFoundError,
S3ErrorCode::OwnershipControlsNotFoundError,
),
(
S3ServiceError::ReplicationConfigurationNotFoundError,
S3ErrorCode::ReplicationConfigurationNotFoundError,
),
(
S3ServiceError::ServerSideEncryptionConfigurationNotFoundError,
S3ErrorCode::ServerSideEncryptionConfigurationNotFoundError,
),
];
for (err, expected_code) in cases {
let s3_err: S3Error = err.into();
assert_eq!(s3_err.code, expected_code);
}
}
#[test]
fn test_should_convert_not_modified_to_304() {
let err = S3ServiceError::NotModified;
let s3_err: S3Error = err.into();
assert_eq!(s3_err.code, S3ErrorCode::NotModified);
assert_eq!(s3_err.status_code, http::StatusCode::NOT_MODIFIED);
}
#[test]
fn test_should_convert_conditional_request_conflict_to_409() {
let err = S3ServiceError::ConditionalRequestConflict;
let s3_err: S3Error = err.into();
assert_eq!(s3_err.code, S3ErrorCode::ConditionalRequestConflict);
assert_eq!(s3_err.status_code, http::StatusCode::CONFLICT);
}
#[test]
fn test_should_convert_bad_digest_to_400() {
let err = S3ServiceError::BadDigest;
let s3_err: S3Error = err.into();
assert_eq!(s3_err.code, S3ErrorCode::BadDigest);
assert_eq!(s3_err.status_code, http::StatusCode::BAD_REQUEST);
}
#[test]
fn test_should_convert_max_message_length_exceeded_to_400() {
let err = S3ServiceError::MaxMessageLengthExceeded;
let s3_err: S3Error = err.into();
assert_eq!(s3_err.code, S3ErrorCode::MaxMessageLengthExceeded);
assert_eq!(s3_err.status_code, http::StatusCode::BAD_REQUEST);
}
}