Skip to main content

s3_wire/operation/
object.rs

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