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#[derive(Clone, Debug, Default, Eq, PartialEq)]
12pub struct ObjectMetadata {
13 pub e_tag: Option<String>,
15 pub content_length: u64,
17 pub content_type: Option<String>,
19 pub last_modified: Option<time::OffsetDateTime>,
21 pub version_id: Option<String>,
23 pub user_metadata: BTreeMap<String, String>,
25 pub checksum: Checksum,
27 pub request_ids: RequestIds,
29}
30
31#[derive(derive_more::Debug)]
33pub struct PutObjectRequest {
34 pub key: ObjectKey,
36 #[debug("{:?}", "<stream>")]
38 pub body: ByteStream,
39 pub content_type: Option<String>,
41 #[debug("{:?}", self.user_metadata.keys().collect::<Vec<_>>())]
43 pub user_metadata: BTreeMap<String, String>,
44 pub conditions: Conditions,
46 pub checksum_algorithm: Option<ChecksumAlgorithm>,
48 #[debug("{:?}", "<redacted>")]
51 pub headers: HeaderMap,
52}
53
54impl PutObjectRequest {
55 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 pub fn with_headers(mut self, headers: HeaderMap) -> Self {
70 self.headers = headers;
71 self
72 }
73}
74
75#[derive(Clone, Debug, Default, Eq, PartialEq)]
77pub struct PutObjectOutput {
78 pub e_tag: Option<String>,
80 pub version_id: Option<String>,
82 pub checksum: Checksum,
84 pub request_ids: RequestIds,
86}
87
88#[derive(Clone, derive_more::Debug, Eq, PartialEq)]
90pub struct GetObjectRequest {
91 pub key: ObjectKey,
93 pub range: Option<ByteRange>,
95 pub conditions: Conditions,
97 pub version_id: Option<String>,
99 #[debug("{:?}", "<redacted>")]
102 pub headers: HeaderMap,
103}
104
105impl GetObjectRequest {
106 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 pub fn with_headers(mut self, headers: HeaderMap) -> Self {
119 self.headers = headers;
120 self
121 }
122}
123
124pub struct GetObjectOutput {
126 pub metadata: ObjectMetadata,
128 pub body: ResponseStream,
130 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#[derive(Clone, derive_more::Debug, Eq, PartialEq)]
147pub struct HeadObjectRequest {
148 pub key: ObjectKey,
150 pub conditions: Conditions,
152 pub version_id: Option<String>,
154 #[debug("{:?}", "<redacted>")]
157 pub headers: HeaderMap,
158}
159
160impl HeadObjectRequest {
161 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 pub fn with_headers(mut self, headers: HeaderMap) -> Self {
173 self.headers = headers;
174 self
175 }
176}
177
178pub type HeadObjectOutput = ObjectMetadata;
180
181#[derive(Clone, derive_more::Debug, Eq, PartialEq)]
183pub struct DeleteObjectRequest {
184 pub key: ObjectKey,
186 pub version_id: Option<String>,
188 pub if_match: Option<String>,
190 #[debug("{:?}", "<redacted>")]
193 pub headers: HeaderMap,
194}
195
196impl DeleteObjectRequest {
197 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 pub fn with_headers(mut self, headers: HeaderMap) -> Self {
209 self.headers = headers;
210 self
211 }
212}
213
214#[derive(Clone, Debug, Default, Eq, PartialEq)]
216pub struct DeleteObjectOutput {
217 pub delete_marker: bool,
219 pub version_id: Option<String>,
221 pub request_ids: RequestIds,
223}
224
225#[derive(Clone, Debug, Eq, PartialEq)]
227pub struct DeleteObjectIdentifier {
228 pub key: ObjectKey,
230 pub version_id: Option<String>,
232 pub if_match: Option<String>,
234}
235
236impl DeleteObjectIdentifier {
237 pub fn new(key: ObjectKey) -> Self {
239 Self {
240 key,
241 version_id: None,
242 if_match: None,
243 }
244 }
245}
246
247pub const MAX_DELETE_OBJECTS: usize = 1_000;
249
250#[derive(Clone, derive_more::Debug, Eq, PartialEq)]
252pub struct DeleteObjectsRequest {
253 objects: Vec<DeleteObjectIdentifier>,
254 pub quiet: bool,
256 #[debug("{:?}", "<redacted>")]
259 pub headers: HeaderMap,
260}
261
262impl DeleteObjectsRequest {
263 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 pub fn with_headers(mut self, headers: HeaderMap) -> Self {
283 self.headers = headers;
284 self
285 }
286
287 pub fn objects(&self) -> &[DeleteObjectIdentifier] {
289 &self.objects
290 }
291}
292
293#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
295pub enum DeleteObjectsError {
296 #[error("a multi-object delete request cannot be empty")]
298 Empty,
299 #[error("multi-object delete has {actual} entries; the maximum is {maximum}")]
301 TooMany {
302 actual: usize,
304 maximum: usize,
306 },
307}
308
309#[derive(Clone, Debug, Eq, PartialEq)]
311pub struct DeletedObject {
312 pub key: ObjectKey,
314 pub version_id: Option<String>,
316 pub delete_marker: bool,
318 pub delete_marker_version_id: Option<String>,
320}
321
322#[derive(Clone, Debug, Eq, PartialEq)]
324pub struct DeleteError {
325 pub key: String,
327 pub version_id: Option<String>,
329 pub code: Option<String>,
331 pub message: Option<String>,
333}
334
335#[derive(Clone, Debug, Default, Eq, PartialEq)]
337pub struct DeleteObjectsOutput {
338 pub deleted: Vec<DeletedObject>,
340 pub errors: Vec<DeleteError>,
342 pub request_ids: RequestIds,
344}
345
346#[derive(Clone, Debug, Eq, PartialEq)]
348pub struct CopySource {
349 pub bucket: String,
351 pub key: ObjectKey,
353 pub version_id: Option<String>,
355}
356
357#[derive(Clone, Debug, Default, Eq, PartialEq)]
363pub enum CopyMetadataDirective {
364 #[default]
366 Copy,
367 Replace {
369 content_type: Option<String>,
371 user_metadata: BTreeMap<String, String>,
373 },
374}
375
376#[derive(Clone, derive_more::Debug, Eq, PartialEq)]
378pub struct CopyObjectRequest {
379 pub source: CopySource,
381 pub destination: ObjectKey,
383 pub source_conditions: Conditions,
385 #[debug(
387 "{:?}",
388 match self.metadata {
389 CopyMetadataDirective::Copy => "copy",
390 CopyMetadataDirective::Replace { .. } => "replace",
391 }
392 )]
393 pub metadata: CopyMetadataDirective,
394 #[debug("{:?}", "<redacted>")]
397 pub headers: HeaderMap,
398}
399
400impl CopyObjectRequest {
401 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 pub fn with_headers(mut self, headers: HeaderMap) -> Self {
414 self.headers = headers;
415 self
416 }
417}
418
419#[derive(Clone, Debug, Default, Eq, PartialEq)]
421pub struct ObjectOwner {
422 pub id: Option<String>,
424 pub display_name: Option<String>,
426}
427
428#[derive(Clone, Debug, Default, Eq, PartialEq)]
430pub struct CopyObjectOutput {
431 pub e_tag: Option<String>,
433 pub last_modified: Option<time::OffsetDateTime>,
435 pub version_id: Option<String>,
437 pub checksum: Checksum,
439 pub request_ids: RequestIds,
441}
442
443#[derive(Clone, Copy, Debug, Eq, PartialEq)]
445pub struct PageSize(NonZeroU16);
446
447impl PageSize {
448 pub const MAX: u16 = 1_000;
450
451 pub fn new(value: u16) -> Option<Self> {
453 NonZeroU16::new(value)
454 .filter(|value| value.get() <= Self::MAX)
455 .map(Self)
456 }
457
458 pub const fn get(self) -> u16 {
460 self.0.get()
461 }
462}
463
464impl Default for PageSize {
465 fn default() -> Self {
466 Self(NonZeroU16::new(Self::MAX).expect("page-size maximum must be non-zero"))
468 }
469}
470
471#[derive(Clone, Default, derive_more::Debug, Eq, PartialEq)]
473pub struct ListObjectsV2Request {
474 pub prefix: Option<String>,
476 pub delimiter: Option<String>,
478 pub continuation_token: Option<String>,
480 pub start_after: Option<ObjectKey>,
482 pub max_keys: PageSize,
484 pub fetch_owner: bool,
486 #[debug("{:?}", "<redacted>")]
489 pub headers: HeaderMap,
490}
491
492impl ListObjectsV2Request {
493 pub fn with_headers(mut self, headers: HeaderMap) -> Self {
495 self.headers = headers;
496 self
497 }
498}
499
500#[derive(Clone, Debug, Eq, PartialEq)]
502pub struct ListedObject {
503 pub key: ObjectKey,
505 pub last_modified: Option<time::OffsetDateTime>,
507 pub e_tag: Option<String>,
509 pub size: u64,
511 pub storage_class: Option<String>,
513 pub owner: Option<ObjectOwner>,
515 pub checksum_algorithms: Vec<String>,
517 pub checksum_type: Option<ChecksumType>,
519}
520
521#[derive(Clone, Debug, Default, Eq, PartialEq)]
523pub struct ListObjectsV2Output {
524 pub objects: Vec<ListedObject>,
526 pub common_prefixes: Vec<String>,
528 pub is_truncated: bool,
530 pub next_continuation_token: Option<String>,
532 pub key_count: Option<u32>,
534 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}