Skip to main content

s3_unspool/
error.rs

1use std::error::Error as StdError;
2use std::fmt::Display;
3use std::io;
4
5use aws_smithy_types::error::display::DisplayErrorContext;
6use aws_smithy_types::error::metadata::ProvideErrorMetadata;
7use thiserror::Error;
8
9/// Result type returned by `s3-unspool` operations.
10pub type Result<T> = std::result::Result<T, Error>;
11
12/// Error type returned by `s3-unspool` operations.
13#[derive(Debug, Error)]
14pub enum Error {
15    /// An `s3://` URI could not be parsed.
16    #[error("invalid S3 URI `{uri}`: {reason}")]
17    InvalidS3Uri {
18        /// Original URI string.
19        uri: String,
20        /// Human-readable parse failure.
21        reason: String,
22    },
23    /// A local upload path is invalid or unsupported.
24    #[error("invalid local path `{path}`: {reason}")]
25    InvalidLocalPath {
26        /// Local path that failed validation.
27        path: String,
28        /// Human-readable validation failure.
29        reason: String,
30    },
31    /// A [`SyncOptions`](crate::SyncOptions) or [`UploadOptions`](crate::UploadOptions)
32    /// value is invalid.
33    #[error("invalid option: {0}")]
34    InvalidOption(String),
35    /// A ZIP entry path is invalid or unsupported.
36    #[error("invalid ZIP entry `{path}`: {reason}")]
37    InvalidZipEntry {
38        /// ZIP entry path that failed validation.
39        path: String,
40        /// Human-readable validation failure.
41        reason: String,
42    },
43    /// The source ZIP contains the same normalized path more than once.
44    #[error("duplicate ZIP file path `{0}`")]
45    DuplicateZipPath(String),
46    /// A ZIP entry is larger than the supported single-`PutObject` limit.
47    #[error("entry `{path}` is {size} bytes, larger than the S3 single PutObject limit")]
48    EntryTooLarge {
49        /// ZIP entry path.
50        path: String,
51        /// Entry size in bytes.
52        size: u64,
53    },
54    /// An AWS S3 operation failed.
55    #[error("S3 {operation} failed for s3://{bucket}/{key}: {message}")]
56    S3 {
57        /// S3 operation name.
58        operation: &'static str,
59        /// S3 bucket used by the failed operation.
60        bucket: String,
61        /// S3 key used by the failed operation.
62        key: String,
63        /// Error message from the AWS SDK.
64        message: String,
65    },
66    /// A conditional destination write failed because the destination changed after listing.
67    #[error("conditional write failed for s3://{bucket}/{key}: {message}")]
68    ConditionalConflict {
69        /// Destination bucket.
70        bucket: String,
71        /// Destination key.
72        key: String,
73        /// Conflict message from S3.
74        message: String,
75    },
76    /// A multipart upload failed, and aborting it also failed.
77    #[error("{original}; additionally failed to abort multipart upload: {abort}")]
78    MultipartAbort {
79        /// Original upload failure.
80        original: Box<Error>,
81        /// Abort failure that may leave an orphaned multipart upload.
82        abort: Box<Error>,
83    },
84    /// Reading or writing ZIP data failed.
85    #[error("ZIP operation failed: {0}")]
86    Zip(#[from] async_zip::error::ZipError),
87    /// Local I/O failed while reading upload sources or streaming data.
88    #[error("I/O failed: {0}")]
89    Io(#[from] io::Error),
90    /// A background Tokio task failed.
91    #[error("worker task failed: {0}")]
92    Join(#[from] tokio::task::JoinError),
93    /// Building an AWS SDK request failed before it was sent.
94    #[error("AWS SDK build failed: {0}")]
95    Build(String),
96}
97
98pub(crate) fn aws_error_message(error: &(impl ProvideErrorMetadata + Display)) -> String {
99    match (error.code(), error.message()) {
100        (Some(code), Some(message)) if !message.is_empty() && message != code => {
101            format!("{code}: {message}")
102        }
103        (Some(code), _) => code.to_string(),
104        (None, Some(message)) if !message.is_empty() => message.to_string(),
105        _ => error.to_string(),
106    }
107}
108
109pub(crate) fn aws_error_context(error: &(impl ProvideErrorMetadata + StdError)) -> String {
110    format!("{}", DisplayErrorContext(error))
111}
112
113#[cfg(test)]
114mod tests {
115    use aws_smithy_types::error::ErrorMetadata;
116
117    use super::*;
118
119    #[test]
120    fn aws_error_message_prefers_code_and_message() {
121        let metadata = ErrorMetadata::builder()
122            .code("NoSuchKey")
123            .message("The specified key does not exist.")
124            .build();
125
126        assert_eq!(
127            aws_error_message(&metadata),
128            "NoSuchKey: The specified key does not exist."
129        );
130    }
131
132    #[test]
133    fn aws_error_message_falls_back_to_display() {
134        let metadata = ErrorMetadata::builder().build();
135
136        assert_eq!(aws_error_message(&metadata), "Error");
137    }
138
139    #[test]
140    fn aws_error_context_includes_error_chain() {
141        let metadata = ErrorMetadata::builder()
142            .code("NoSuchKey")
143            .message("The specified key does not exist.")
144            .build();
145
146        let context = aws_error_context(&metadata);
147
148        assert!(context.contains("NoSuchKey"));
149        assert!(context.contains("The specified key does not exist."));
150    }
151}