Skip to main content

s3_wire/operation/
object.rs

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