Skip to main content

s3_wire/operation/
object.rs

1use std::{collections::BTreeMap, fmt, num::NonZeroU16};
2
3use http::HeaderMap;
4
5use super::{
6    ByteRange, Checksum, ChecksumAlgorithm, ChecksumType, Conditions, ObjectKey, RequestIds,
7};
8use crate::stream::{ByteStream, ResponseStream};
9
10/// Metadata common to object retrieval and inspection responses.
11#[derive(Clone, Debug, Default, Eq, PartialEq)]
12pub struct ObjectMetadata {
13    /// Entity tag returned by the service.
14    pub e_tag: Option<String>,
15    /// Object size in bytes.
16    pub content_length: u64,
17    /// Object media type.
18    pub content_type: Option<String>,
19    /// Object modification time.
20    pub last_modified: Option<time::OffsetDateTime>,
21    /// Version identifier, when bucket versioning is enabled.
22    pub version_id: Option<String>,
23    /// Caller-defined `x-amz-meta-*` values.
24    pub user_metadata: BTreeMap<String, String>,
25    /// Checksums supplied by the service.
26    pub checksum: Checksum,
27    /// Service request identifiers.
28    pub request_ids: RequestIds,
29}
30
31/// Request to upload one object.
32#[derive(derive_more::Debug)]
33pub struct PutObjectRequest {
34    /// Destination object key.
35    pub key: ObjectKey,
36    /// Upload body. Replayability is determined by its selected source.
37    #[debug("{:?}", "<stream>")]
38    pub body: ByteStream,
39    /// Optional media type.
40    pub content_type: Option<String>,
41    /// Caller-defined object metadata.
42    #[debug("{:?}", self.user_metadata.keys().collect::<Vec<_>>())]
43    pub user_metadata: BTreeMap<String, String>,
44    /// Preconditions for the write.
45    pub conditions: Conditions,
46    /// Ask S3 to calculate or validate this checksum algorithm.
47    pub checksum_algorithm: Option<ChecksumAlgorithm>,
48    /// Additional request headers. Values are signed and repeated values are preserved.
49    /// Generated-name collisions are errors; signing- and transport-owned headers are rejected.
50    #[debug("{:?}", "<redacted>")]
51    pub headers: HeaderMap,
52}
53
54impl PutObjectRequest {
55    /// Constructs a request with no optional headers.
56    pub fn new(key: ObjectKey, body: ByteStream) -> Self {
57        Self {
58            key,
59            body,
60            content_type: None,
61            user_metadata: BTreeMap::new(),
62            conditions: Conditions::default(),
63            checksum_algorithm: None,
64            headers: HeaderMap::new(),
65        }
66    }
67
68    /// Replaces the request's additional headers.
69    pub fn with_headers(mut self, headers: HeaderMap) -> Self {
70        self.headers = headers;
71        self
72    }
73}
74
75/// Result of uploading one object.
76#[derive(Clone, Debug, Default, Eq, PartialEq)]
77pub struct PutObjectOutput {
78    /// Entity tag assigned by the service.
79    pub e_tag: Option<String>,
80    /// Version identifier, when enabled.
81    pub version_id: Option<String>,
82    /// Returned checksum values.
83    pub checksum: Checksum,
84    /// Service request identifiers.
85    pub request_ids: RequestIds,
86}
87
88/// Request to download one object.
89#[derive(Clone, derive_more::Debug, Eq, PartialEq)]
90pub struct GetObjectRequest {
91    /// Object key.
92    pub key: ObjectKey,
93    /// Optional byte range.
94    pub range: Option<ByteRange>,
95    /// Optional preconditions.
96    pub conditions: Conditions,
97    /// Specific object version to retrieve.
98    pub version_id: Option<String>,
99    /// Additional request headers. Values are signed and repeated values are preserved.
100    /// Generated-name collisions are errors; signing- and transport-owned headers are rejected.
101    #[debug("{:?}", "<redacted>")]
102    pub headers: HeaderMap,
103}
104
105impl GetObjectRequest {
106    /// Constructs a full-object request without preconditions.
107    pub fn new(key: ObjectKey) -> Self {
108        Self {
109            key,
110            range: None,
111            conditions: Conditions::default(),
112            version_id: None,
113            headers: HeaderMap::new(),
114        }
115    }
116
117    /// Replaces the request's additional headers.
118    pub fn with_headers(mut self, headers: HeaderMap) -> Self {
119        self.headers = headers;
120        self
121    }
122}
123
124/// A streaming object download and its response metadata.
125pub struct GetObjectOutput {
126    /// Response metadata parsed before the body is consumed.
127    pub metadata: ObjectMetadata,
128    /// Streaming response body.
129    pub body: ResponseStream,
130    /// Inclusive range returned for a ranged request.
131    pub content_range: Option<(u64, u64, Option<u64>)>,
132}
133
134impl fmt::Debug for GetObjectOutput {
135    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
136        formatter
137            .debug_struct("GetObjectOutput")
138            .field("metadata", &self.metadata)
139            .field("body", &"<stream>")
140            .field("content_range", &self.content_range)
141            .finish()
142    }
143}
144
145/// Request to inspect object metadata without retrieving the body.
146#[derive(Clone, derive_more::Debug, Eq, PartialEq)]
147pub struct HeadObjectRequest {
148    /// Object key.
149    pub key: ObjectKey,
150    /// Optional preconditions.
151    pub conditions: Conditions,
152    /// Specific object version to inspect.
153    pub version_id: Option<String>,
154    /// Additional request headers. Values are signed and repeated values are preserved.
155    /// Generated-name collisions are errors; signing- and transport-owned headers are rejected.
156    #[debug("{:?}", "<redacted>")]
157    pub headers: HeaderMap,
158}
159
160impl HeadObjectRequest {
161    /// Constructs an unconditional request for the latest version.
162    pub fn new(key: ObjectKey) -> Self {
163        Self {
164            key,
165            conditions: Conditions::default(),
166            version_id: None,
167            headers: HeaderMap::new(),
168        }
169    }
170
171    /// Replaces the request's additional headers.
172    pub fn with_headers(mut self, headers: HeaderMap) -> Self {
173        self.headers = headers;
174        self
175    }
176}
177
178/// Result of inspecting an object.
179pub type HeadObjectOutput = ObjectMetadata;
180
181/// Request to delete one object.
182#[derive(Clone, derive_more::Debug, Eq, PartialEq)]
183pub struct DeleteObjectRequest {
184    /// Object key.
185    pub key: ObjectKey,
186    /// Specific version to remove.
187    pub version_id: Option<String>,
188    /// Optional entity-tag precondition.
189    pub if_match: Option<String>,
190    /// Additional request headers. Values are signed and repeated values are preserved.
191    /// Generated-name collisions are errors; signing- and transport-owned headers are rejected.
192    #[debug("{:?}", "<redacted>")]
193    pub headers: HeaderMap,
194}
195
196impl DeleteObjectRequest {
197    /// Constructs a request for the latest object version.
198    pub fn new(key: ObjectKey) -> Self {
199        Self {
200            key,
201            version_id: None,
202            if_match: None,
203            headers: HeaderMap::new(),
204        }
205    }
206
207    /// Replaces the request's additional headers.
208    pub fn with_headers(mut self, headers: HeaderMap) -> Self {
209        self.headers = headers;
210        self
211    }
212}
213
214/// Result of deleting one object.
215#[derive(Clone, Debug, Default, Eq, PartialEq)]
216pub struct DeleteObjectOutput {
217    /// Whether the response represents a delete marker.
218    pub delete_marker: bool,
219    /// Removed version identifier, when present.
220    pub version_id: Option<String>,
221    /// Service request identifiers.
222    pub request_ids: RequestIds,
223}
224
225/// One object selected for a multi-object delete request.
226#[derive(Clone, Debug, Eq, PartialEq)]
227pub struct DeleteObjectIdentifier {
228    /// Object key.
229    pub key: ObjectKey,
230    /// Specific version to remove.
231    pub version_id: Option<String>,
232    /// Optional entity-tag precondition.
233    pub if_match: Option<String>,
234}
235
236impl DeleteObjectIdentifier {
237    /// Constructs an identifier for the latest object version.
238    pub fn new(key: ObjectKey) -> Self {
239        Self {
240            key,
241            version_id: None,
242            if_match: None,
243        }
244    }
245}
246
247/// Maximum number of entries accepted by S3's multi-object delete API.
248pub const MAX_DELETE_OBJECTS: usize = 1_000;
249
250/// A validated multi-object delete request.
251#[derive(Clone, derive_more::Debug, Eq, PartialEq)]
252pub struct DeleteObjectsRequest {
253    objects: Vec<DeleteObjectIdentifier>,
254    /// Suppresses per-key success entries when true.
255    pub quiet: bool,
256    /// Additional request headers. Values are signed and repeated values are preserved.
257    /// Generated-name collisions are errors; signing- and transport-owned headers are rejected.
258    #[debug("{:?}", "<redacted>")]
259    pub headers: HeaderMap,
260}
261
262impl DeleteObjectsRequest {
263    /// Validates that the batch contains between one and 1,000 entries.
264    pub fn new(objects: Vec<DeleteObjectIdentifier>) -> Result<Self, DeleteObjectsError> {
265        if objects.is_empty() {
266            return Err(DeleteObjectsError::Empty);
267        }
268        if objects.len() > MAX_DELETE_OBJECTS {
269            return Err(DeleteObjectsError::TooMany {
270                actual: objects.len(),
271                maximum: MAX_DELETE_OBJECTS,
272            });
273        }
274        Ok(Self {
275            objects,
276            quiet: false,
277            headers: HeaderMap::new(),
278        })
279    }
280
281    /// Replaces the request's additional headers.
282    pub fn with_headers(mut self, headers: HeaderMap) -> Self {
283        self.headers = headers;
284        self
285    }
286
287    /// Returns the validated delete entries.
288    pub fn objects(&self) -> &[DeleteObjectIdentifier] {
289        &self.objects
290    }
291}
292
293/// Invalid multi-object delete batch.
294#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
295pub enum DeleteObjectsError {
296    /// The batch has no entries.
297    #[error("a multi-object delete request cannot be empty")]
298    Empty,
299    /// The batch exceeds the S3 limit.
300    #[error("multi-object delete has {actual} entries; the maximum is {maximum}")]
301    TooMany {
302        /// Actual entry count.
303        actual: usize,
304        /// Maximum accepted entry count.
305        maximum: usize,
306    },
307}
308
309/// One successfully deleted object.
310#[derive(Clone, Debug, Eq, PartialEq)]
311pub struct DeletedObject {
312    /// Deleted key.
313    pub key: ObjectKey,
314    /// Deleted version identifier.
315    pub version_id: Option<String>,
316    /// Whether a delete marker was created or removed.
317    pub delete_marker: bool,
318    /// Delete-marker version identifier.
319    pub delete_marker_version_id: Option<String>,
320}
321
322/// Per-object failure returned from a multi-object delete.
323#[derive(Clone, Debug, Eq, PartialEq)]
324pub struct DeleteError {
325    /// Key that was not deleted. Invalid server keys are retained as text.
326    pub key: String,
327    /// Version identifier, when present.
328    pub version_id: Option<String>,
329    /// S3 error code.
330    pub code: Option<String>,
331    /// Service-supplied diagnostic message.
332    pub message: Option<String>,
333}
334
335/// Result of a multi-object delete request.
336#[derive(Clone, Debug, Default, Eq, PartialEq)]
337pub struct DeleteObjectsOutput {
338    /// Successfully deleted entries.
339    pub deleted: Vec<DeletedObject>,
340    /// Entries the service failed to delete.
341    pub errors: Vec<DeleteError>,
342    /// Service request identifiers.
343    pub request_ids: RequestIds,
344}
345
346/// Source of a server-side object copy.
347#[derive(Clone, Debug, Eq, PartialEq)]
348pub struct CopySource {
349    /// Source bucket name.
350    pub bucket: String,
351    /// Source object key.
352    pub key: ObjectKey,
353    /// Source version identifier.
354    pub version_id: Option<String>,
355}
356
357/// Metadata behavior for a server-side copy.
358///
359/// S3's `REPLACE` directive replaces the complete metadata set. Keeping that
360/// choice explicit prevents changing one header from accidentally discarding
361/// all source user metadata.
362#[derive(Clone, Debug, Default, Eq, PartialEq)]
363pub enum CopyMetadataDirective {
364    /// Copy the source object's media type and user metadata unchanged.
365    #[default]
366    Copy,
367    /// Replace the complete metadata set with the supplied values.
368    Replace {
369        /// Replacement media type. When omitted, S3 applies its default.
370        content_type: Option<String>,
371        /// Complete replacement set of caller-defined metadata.
372        user_metadata: BTreeMap<String, String>,
373    },
374}
375
376/// Request to copy an object within S3.
377#[derive(Clone, derive_more::Debug, Eq, PartialEq)]
378pub struct CopyObjectRequest {
379    /// Copy source.
380    pub source: CopySource,
381    /// Destination object key in the configured bucket.
382    pub destination: ObjectKey,
383    /// Preconditions evaluated against the source object.
384    pub source_conditions: Conditions,
385    /// Whether to copy or completely replace source metadata.
386    #[debug(
387        "{:?}",
388        match self.metadata {
389            CopyMetadataDirective::Copy => "copy",
390            CopyMetadataDirective::Replace { .. } => "replace",
391        }
392    )]
393    pub metadata: CopyMetadataDirective,
394    /// Additional request headers. Values are signed and repeated values are preserved.
395    /// Generated-name collisions are errors; signing- and transport-owned headers are rejected.
396    #[debug("{:?}", "<redacted>")]
397    pub headers: HeaderMap,
398}
399
400impl CopyObjectRequest {
401    /// Constructs an unconditional copy that preserves all source metadata.
402    pub fn new(source: CopySource, destination: ObjectKey) -> Self {
403        Self {
404            source,
405            destination,
406            source_conditions: Conditions::default(),
407            metadata: CopyMetadataDirective::Copy,
408            headers: HeaderMap::new(),
409        }
410    }
411
412    /// Replaces the request's additional headers.
413    pub fn with_headers(mut self, headers: HeaderMap) -> Self {
414        self.headers = headers;
415        self
416    }
417}
418
419/// Owner information optionally returned for a listed object.
420#[derive(Clone, Debug, Default, Eq, PartialEq)]
421pub struct ObjectOwner {
422    /// Canonical account identifier.
423    pub id: Option<String>,
424    /// Legacy display name, when the service still supplies one.
425    pub display_name: Option<String>,
426}
427
428/// Result of a server-side copy.
429#[derive(Clone, Debug, Default, Eq, PartialEq)]
430pub struct CopyObjectOutput {
431    /// Entity tag of the copied object.
432    pub e_tag: Option<String>,
433    /// Modification time reported by the service.
434    pub last_modified: Option<time::OffsetDateTime>,
435    /// Destination version identifier.
436    pub version_id: Option<String>,
437    /// Returned checksum values.
438    pub checksum: Checksum,
439    /// Service request identifiers.
440    pub request_ids: RequestIds,
441}
442
443/// Validated S3 listing page size in the range 1 through 1,000.
444#[derive(Clone, Copy, Debug, Eq, PartialEq)]
445pub struct PageSize(NonZeroU16);
446
447impl PageSize {
448    /// Maximum page size accepted by S3 listing operations.
449    pub const MAX: u16 = 1_000;
450
451    /// Constructs a page size if it is in the supported range.
452    pub fn new(value: u16) -> Option<Self> {
453        NonZeroU16::new(value)
454            .filter(|value| value.get() <= Self::MAX)
455            .map(Self)
456    }
457
458    /// Returns the validated value.
459    pub const fn get(self) -> u16 {
460        self.0.get()
461    }
462}
463
464impl Default for PageSize {
465    fn default() -> Self {
466        // `MAX` is statically non-zero.
467        Self(NonZeroU16::new(Self::MAX).expect("page-size maximum must be non-zero"))
468    }
469}
470
471/// Request for one `ListObjectsV2` page.
472#[derive(Clone, Default, derive_more::Debug, Eq, PartialEq)]
473pub struct ListObjectsV2Request {
474    /// Only keys beginning with this exact prefix are returned.
475    pub prefix: Option<String>,
476    /// Groups keys using this delimiter.
477    pub delimiter: Option<String>,
478    /// Opaque token from a preceding response.
479    pub continuation_token: Option<String>,
480    /// Starts after this key when no continuation token is used.
481    pub start_after: Option<ObjectKey>,
482    /// Maximum entries requested from the service.
483    pub max_keys: PageSize,
484    /// Requests owner information for every entry.
485    pub fetch_owner: bool,
486    /// Additional request headers. Values are signed and repeated values are preserved.
487    /// Generated-name collisions are errors; signing- and transport-owned headers are rejected.
488    #[debug("{:?}", "<redacted>")]
489    pub headers: HeaderMap,
490}
491
492impl ListObjectsV2Request {
493    /// Replaces the request's additional headers.
494    pub fn with_headers(mut self, headers: HeaderMap) -> Self {
495        self.headers = headers;
496        self
497    }
498}
499
500/// One object returned by `ListObjectsV2`.
501#[derive(Clone, Debug, Eq, PartialEq)]
502pub struct ListedObject {
503    /// Object key.
504    pub key: ObjectKey,
505    /// Modification time.
506    pub last_modified: Option<time::OffsetDateTime>,
507    /// Entity tag.
508    pub e_tag: Option<String>,
509    /// Object size in bytes.
510    pub size: u64,
511    /// Storage class, when supplied.
512    pub storage_class: Option<String>,
513    /// Owner information requested with `fetch_owner`.
514    pub owner: Option<ObjectOwner>,
515    /// Checksum algorithms associated with this object.
516    pub checksum_algorithms: Vec<String>,
517    /// Whether reported checksums represent the full object or a composite.
518    pub checksum_type: Option<ChecksumType>,
519}
520
521/// One parsed `ListObjectsV2` page.
522#[derive(Clone, Debug, Default, Eq, PartialEq)]
523pub struct ListObjectsV2Output {
524    /// Objects in the page.
525    pub objects: Vec<ListedObject>,
526    /// Grouped key prefixes.
527    pub common_prefixes: Vec<String>,
528    /// Whether another page exists.
529    pub is_truncated: bool,
530    /// Opaque token for retrieving the next page.
531    pub next_continuation_token: Option<String>,
532    /// Number of keys represented in this page.
533    pub key_count: Option<u32>,
534    /// Service request identifiers.
535    pub request_ids: RequestIds,
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541
542    #[test]
543    fn delete_batches_are_strictly_bounded() {
544        assert_eq!(
545            DeleteObjectsRequest::new(Vec::new()).unwrap_err(),
546            DeleteObjectsError::Empty
547        );
548        let key = ObjectKey::new("key").unwrap();
549        let too_many = (0..=MAX_DELETE_OBJECTS)
550            .map(|_| DeleteObjectIdentifier::new(key.clone()))
551            .collect();
552        assert!(matches!(
553            DeleteObjectsRequest::new(too_many),
554            Err(DeleteObjectsError::TooMany { .. })
555        ));
556    }
557
558    #[test]
559    fn custom_header_values_are_redacted() {
560        let mut request = GetObjectRequest::new(ObjectKey::new("key").unwrap());
561        request.headers.insert(
562            "x-amz-server-side-encryption-customer-key",
563            "sentinel-sse-c-key".parse().unwrap(),
564        );
565        let debug = format!("{request:?}");
566        assert!(debug.contains(r#"headers: "<redacted>""#));
567        assert!(!debug.contains("sentinel-sse-c-key"));
568    }
569
570    #[test]
571    fn page_sizes_reject_zero_and_values_over_service_limit() {
572        assert!(PageSize::new(0).is_none());
573        assert_eq!(PageSize::new(1).unwrap().get(), 1);
574        assert_eq!(PageSize::new(1_000).unwrap().get(), 1_000);
575        assert!(PageSize::new(1_001).is_none());
576    }
577}