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