Skip to main content

minio/s3/
response_traits.rs

1//! Response traits for accessing S3 metadata from HTTP response headers.
2//!
3//! This module provides a collection of traits that enable typed, ergonomic access to
4//! metadata from S3 API responses. These traits extract data from HTTP headers and response
5//! bodies returned by various S3 operations.
6//!
7//! # Design Philosophy
8//!
9//! Rather than exposing raw headers directly, these traits provide:
10//! - **Type-safe access**: Automatic parsing and type conversion
11//! - **Consistent API**: Uniform method names across different response types
12//! - **Composability**: Mix and match traits based on what metadata is available
13//!
14//! # Metadata Sources
15//!
16//! Metadata is available from two primary sources:
17//!
18//! ## 1. HEAD Requests (Metadata Only)
19//!
20//! Operations like [`stat_object`](crate::s3::client::MinioClient::stat_object) use HEAD requests
21//! to retrieve object metadata without downloading the object body. These responses typically
22//! implement traits like:
23//! - [`HasVersion`]: Object version ID (via `x-amz-version-id` header)
24//! - [`HasObjectSize`]: Object size in bytes (via `x-amz-object-size` or `Content-Length` header)
25//! - [`HasEtagFromHeaders`]: Object ETag/hash (via `ETag` header)
26//! - [`HasChecksumHeaders`]: Object checksum values (via `x-amz-checksum-*` headers)
27//! - [`HasIsDeleteMarker`]: Whether the object is a delete marker (via `x-amz-delete-marker` header)
28//!
29//! ## 2. GET Requests (Metadata + Body)
30//!
31//! Operations like [`get_object`](crate::s3::client::MinioClient::get_object) return both
32//! metadata headers AND the object body. These responses can implement both header-based
33//! traits (above) and body-parsing traits like:
34//! - [`HasEtagFromBody`]: ETag parsed from XML response body
35//!
36//! # Example: StatObjectResponse
37//!
38//! The [`StatObjectResponse`](crate::s3::response::StatObjectResponse) demonstrates how
39//! multiple traits compose together. It uses a HEAD request and provides:
40//!
41//! ```rust,ignore
42//! impl HasBucket for StatObjectResponse {}
43//! impl HasRegion for StatObjectResponse {}
44//! impl HasObject for StatObjectResponse {}
45//! impl HasEtagFromHeaders for StatObjectResponse {}
46//! impl HasIsDeleteMarker for StatObjectResponse {}
47//! impl HasChecksumHeaders for StatObjectResponse {}
48//! impl HasVersion for StatObjectResponse {}       // Version ID from header
49//! impl HasObjectSize for StatObjectResponse {}    // Size from header
50//! ```
51//!
52//! This allows users to access metadata uniformly:
53//!
54//! ```rust,ignore
55//! let response = client.stat_object(&args).await?;
56//! let size = response.object_size();           // From HasObjectSize trait
57//! let version = response.version_id();          // From HasVersion trait
58//! let checksum = response.checksum_crc32c()?;  // From HasChecksumHeaders trait
59//! ```
60//!
61//! # Performance Considerations
62//!
63//! - **HEAD vs GET**: HEAD requests are faster when you only need metadata (no body transfer)
64//! - **Header parsing**: Trait methods use `#[inline]` for zero-cost abstractions
65//! - **Lazy evaluation**: Metadata is parsed on-demand, not upfront
66
67use crate::s3::error::ValidationErr;
68use crate::s3::header_constants::*;
69use crate::s3::types::{BucketName, ETag, ObjectKey, Region, S3Request, VersionId};
70use crate::s3::utils::{ChecksumAlgorithm, get_text_result, parse_bool, trim_quotes};
71use bytes::{Buf, Bytes};
72use http::HeaderMap;
73use std::collections::HashMap;
74use xmltree::Element;
75
76#[macro_export]
77/// Implements the `FromS3Response` trait for the specified types.
78macro_rules! impl_from_s3response {
79    ($($ty:ty),* $(,)?) => {
80        $(
81            #[async_trait::async_trait]
82            impl $crate::s3::types::FromS3Response for $ty {
83                async fn from_s3response(
84                    request: $crate::s3::types::S3Request,
85                    response: Result<reqwest::Response, $crate::s3::error::Error>,
86                ) -> Result<Self, $crate::s3::error::Error> {
87                    let mut resp: reqwest::Response = response?;
88                    Ok(Self {
89                        request,
90                        headers: std::mem::take(resp.headers_mut()),
91                        body: resp.bytes().await.map_err($crate::s3::error::ValidationErr::from)?,
92                    })
93                }
94            }
95        )*
96    };
97}
98
99#[macro_export]
100/// Implements the `FromS3Response` trait for the specified types with an additional `object_size` field.
101macro_rules! impl_from_s3response_with_size {
102    ($($ty:ty),* $(,)?) => {
103        $(
104            #[async_trait::async_trait]
105            impl $crate::s3::types::FromS3Response for $ty {
106                async fn from_s3response(
107                    request: $crate::s3::types::S3Request,
108                    response: Result<reqwest::Response, $crate::s3::error::Error>,
109                ) -> Result<Self, $crate::s3::error::Error> {
110                    let mut resp: reqwest::Response = response?;
111                    Ok(Self {
112                        request,
113                        headers: std::mem::take(resp.headers_mut()),
114                        body: resp.bytes().await.map_err($crate::s3::error::ValidationErr::from)?,
115                        object_size: 0, // Default value, can be set later
116                    })
117                }
118            }
119        )*
120    };
121}
122
123#[macro_export]
124/// Implements the `HasS3Fields` trait for the specified types.
125macro_rules! impl_has_s3fields {
126    ($($ty:ty),* $(,)?) => {
127        $(
128            impl $crate::s3::response_traits::HasS3Fields for $ty {
129                /// The request that was sent to the S3 API.
130                #[inline]
131                fn request(&self) -> &$crate::s3::types::S3Request {
132                    &self.request
133                }
134
135                /// The response of the S3 API.
136                #[inline]
137                fn headers(&self) -> &http::HeaderMap {
138                    &self.headers
139                }
140
141                /// The response of the S3 API.
142                #[inline]
143                fn body(&self) -> &bytes::Bytes {
144                    &self.body
145                }
146            }
147        )*
148    };
149}
150
151pub trait HasS3Fields {
152    /// The request that was sent to the S3 API.
153    fn request(&self) -> &S3Request;
154    /// HTTP headers returned by the server, containing metadata such as `Content-Type`, `ETag`, etc.
155    fn headers(&self) -> &HeaderMap;
156    /// The response body returned by the server, which may contain the object data or other information.
157    fn body(&self) -> &Bytes;
158}
159/// Returns the name of the S3 bucket.
160pub trait HasBucket: HasS3Fields {
161    /// Returns the name of the S3 bucket, if set.
162    #[inline]
163    fn bucket(&self) -> Option<&BucketName> {
164        self.request().bucket.as_ref()
165    }
166}
167/// Returns the object key (name) of the S3 object.
168pub trait HasObject: HasS3Fields {
169    /// Returns the object key (name) of the S3 object, if set.
170    #[inline]
171    fn object(&self) -> Option<&ObjectKey> {
172        self.request().object.as_ref()
173    }
174}
175/// Returns the region of the S3 bucket.
176pub trait HasRegion: HasS3Fields {
177    /// Returns the region of the S3 bucket.
178    #[inline]
179    fn region(&self) -> &Region {
180        &self.request().inner_region
181    }
182}
183
184/// Returns the version ID of the object (`x-amz-version-id`), if versioning is enabled for the bucket.
185pub trait HasVersion: HasS3Fields {
186    /// Returns the version ID of the object (`x-amz-version-id`), if versioning is enabled for the bucket.
187    #[inline]
188    fn version_id(&self) -> Option<VersionId> {
189        self.headers()
190            .get(X_AMZ_VERSION_ID)
191            .and_then(|v| v.to_str().ok())
192            .and_then(|s| VersionId::new(s).ok())
193    }
194}
195
196/// Returns the value of the `ETag` header from response headers (for operations that return ETag in headers).
197/// The ETag is typically a hash of the object content, but it may vary based on the storage backend.
198pub trait HasEtagFromHeaders: HasS3Fields {
199    /// Returns the value of the `ETag` header from response headers (for operations that return ETag in headers).
200    /// The ETag is typically a hash of the object content, but it may vary based on the storage backend.
201    #[inline]
202    fn etag(&self) -> Result<ETag, ValidationErr> {
203        let etag_str = self
204            .headers()
205            .get("etag")
206            .and_then(|v| v.to_str().ok())
207            .map(|s| s.trim_matches('"'))
208            .unwrap_or_default();
209        ETag::new(etag_str)
210    }
211}
212
213/// Returns the value of the `ETag` from the response body, which is a unique identifier for
214/// the object version. The ETag is typically a hash of the object content, but it may vary
215/// based on the storage backend.
216pub trait HasEtagFromBody: HasS3Fields {
217    /// Returns the value of the `ETag` from the response body, which is a unique identifier for
218    /// the object version. The ETag is typically a hash of the object content, but it may vary
219    /// based on the storage backend.
220    fn etag(&self) -> Result<ETag, ValidationErr> {
221        let root = xmltree::Element::parse(self.body().clone().reader())?;
222        let etag_str: String = get_text_result(&root, "ETag")?;
223        ETag::new(trim_quotes(etag_str))
224    }
225}
226
227/// Returns the size of the object in bytes, as specified by the `x-amz-object-size` header.
228pub trait HasObjectSize: HasS3Fields {
229    /// Returns the size of the object in bytes, as specified by the `x-amz-object-size` header.
230    #[inline]
231    fn object_size(&self) -> u64 {
232        self.headers()
233            .get(X_AMZ_OBJECT_SIZE)
234            .and_then(|v| v.to_str().ok())
235            .and_then(|s| s.parse::<u64>().ok())
236            .unwrap_or(0)
237    }
238}
239
240/// Provides access to the `x-amz-delete-marker` header value.
241///
242/// Indicates whether the specified object version that was permanently deleted was (true) or
243/// was not (false) a delete marker before deletion. In a simple DELETE, this header indicates
244/// whether (true) or not (false) the current version of the object is a delete marker.
245pub trait HasIsDeleteMarker: HasS3Fields {
246    /// Returns `true` if the object is a delete marker, `false` otherwise.
247    #[inline]
248    fn is_delete_marker(&self) -> Result<bool, ValidationErr> {
249        self.headers()
250            .get(X_AMZ_DELETE_MARKER)
251            .map_or(Ok(false), |v| parse_bool(v.to_str()?))
252    }
253}
254
255pub trait HasTagging: HasS3Fields {
256    /// Returns the tags associated with the bucket.
257    ///
258    /// If the bucket has no tags, this will return an empty `HashMap`.
259    #[inline]
260    fn tags(&self) -> Result<HashMap<String, String>, ValidationErr> {
261        let mut tags = HashMap::new();
262        if self.body().is_empty() {
263            // Note: body is empty when server responses with NoSuchTagSet
264            return Ok(tags);
265        }
266        let mut root = Element::parse(self.body().clone().reader())?;
267        let element = root
268            .get_mut_child("TagSet")
269            .ok_or(ValidationErr::xml_error("<TagSet> tag not found"))?;
270        while let Some(v) = element.take_child("Tag") {
271            tags.insert(get_text_result(&v, "Key")?, get_text_result(&v, "Value")?);
272        }
273        Ok(tags)
274    }
275}
276
277/// Provides checksum-related methods for S3 responses with headers.
278///
279/// This trait provides default implementations for extracting and detecting checksums
280/// from S3 response headers. Implement this trait for any response type that has
281/// `HeaderMap` access via `HasS3Fields`.
282pub trait HasChecksumHeaders: HasS3Fields {
283    /// Extracts the checksum value from response headers for the specified algorithm.
284    ///
285    /// Retrieves the base64-encoded checksum value from the appropriate S3 response header
286    /// (x-amz-checksum-crc32, x-amz-checksum-crc32c, x-amz-checksum-crc64nvme,
287    /// x-amz-checksum-sha1, or x-amz-checksum-sha256).
288    ///
289    /// # Arguments
290    ///
291    /// * `algorithm` - The checksum algorithm to retrieve
292    ///
293    /// # Returns
294    ///
295    /// - `Some(checksum)` if the header is present, containing the base64-encoded checksum value
296    /// - `None` if the header is not found
297    ///
298    /// # Use Cases
299    ///
300    /// - Compare with locally computed checksums for manual verification
301    /// - Store checksum values for audit or compliance records
302    /// - Verify integrity after downloading to disk
303    #[inline]
304    fn get_checksum(&self, algorithm: ChecksumAlgorithm) -> Option<String> {
305        let header_name = match algorithm {
306            ChecksumAlgorithm::CRC32 => X_AMZ_CHECKSUM_CRC32,
307            ChecksumAlgorithm::CRC32C => X_AMZ_CHECKSUM_CRC32C,
308            ChecksumAlgorithm::SHA1 => X_AMZ_CHECKSUM_SHA1,
309            ChecksumAlgorithm::SHA256 => X_AMZ_CHECKSUM_SHA256,
310            ChecksumAlgorithm::CRC64NVME => X_AMZ_CHECKSUM_CRC64NVME,
311        };
312
313        self.headers()
314            .get(header_name)
315            .and_then(|v| v.to_str().ok())
316            .map(|s| s.to_string())
317    }
318
319    /// Returns the checksum type from response headers.
320    ///
321    /// The checksum type indicates whether the checksum represents:
322    /// - `FULL_OBJECT` - A checksum computed over the entire object
323    /// - `COMPOSITE` - A checksum-of-checksums for multipart uploads
324    ///
325    /// # Returns
326    ///
327    /// - `Some(type_string)` if the `x-amz-checksum-type` header is present
328    /// - `None` if the header is not found
329    #[inline]
330    fn checksum_type(&self) -> Option<String> {
331        self.headers()
332            .get(X_AMZ_CHECKSUM_TYPE)
333            .and_then(|v| v.to_str().ok())
334            .map(|s| s.to_string())
335    }
336
337    /// Detects which checksum algorithm was used by the server (if any).
338    ///
339    /// Examines response headers to determine if the server computed a checksum
340    /// for this operation.
341    ///
342    /// # Returns
343    ///
344    /// - `Some(algorithm)` if a checksum header is found (CRC32, CRC32C, CRC64NVME, SHA1, or SHA256)
345    /// - `None` if no checksum headers are present
346    #[inline]
347    fn detect_checksum_algorithm(&self) -> Option<ChecksumAlgorithm> {
348        if self.headers().contains_key(X_AMZ_CHECKSUM_CRC32) {
349            Some(ChecksumAlgorithm::CRC32)
350        } else if self.headers().contains_key(X_AMZ_CHECKSUM_CRC32C) {
351            Some(ChecksumAlgorithm::CRC32C)
352        } else if self.headers().contains_key(X_AMZ_CHECKSUM_CRC64NVME) {
353            Some(ChecksumAlgorithm::CRC64NVME)
354        } else if self.headers().contains_key(X_AMZ_CHECKSUM_SHA1) {
355            Some(ChecksumAlgorithm::SHA1)
356        } else if self.headers().contains_key(X_AMZ_CHECKSUM_SHA256) {
357            Some(ChecksumAlgorithm::SHA256)
358        } else {
359            None
360        }
361    }
362}