Skip to main content

s3/
command.rs

1//! This module defines and manages various commands used for interacting with Amazon S3, encapsulating common operations such as creating buckets, uploading objects, and managing multipart uploads.
2//! It also provides utilities for calculating necessary metadata (like content length and SHA-256 hashes) required for secure and efficient communication with the S3 service.
3//!
4//! ## Key Components
5//!
6//! - **HttpMethod Enum**
7//!   - Represents HTTP methods used in S3 operations, including `GET`, `PUT`, `DELETE`, `POST`, and `HEAD`.
8//!   - Implements `fmt::Display` for easy conversion to string representations suitable for HTTP requests.
9//!
10//! - **Multipart Struct**
11//!   - Represents a part of a multipart upload, containing the part number and the associated upload ID.
12//!   - Provides methods for constructing a new multipart part and generating a query string for the S3 API.
13//!
14//! - **Command Enum**
15//!   - The core of this module, encapsulating various S3 operations, such as:
16//!     - Object management (`GetObject`, `PutObject`, `DeleteObject`, etc.)
17//!     - Bucket management (`CreateBucket`, `DeleteBucket`, etc.)
18//!     - Multipart upload management (`InitiateMultipartUpload`, `UploadPart`, `CompleteMultipartUpload`, etc.)
19//!   - For each command, you can determine the associated HTTP method using `http_verb()` and calculate the content length or content type using `content_length()` and `content_type()` respectively.
20//!   - The `sha256()` method computes the SHA-256 hash of the request payload, a critical part of S3's security features.
21//!
22use std::collections::HashMap;
23
24use crate::error::S3Error;
25use crate::serde_types::{
26    BucketLifecycleConfiguration, CompleteMultipartUploadData, CorsConfiguration,
27    DeleteObjectsRequest,
28};
29
30use crate::EMPTY_PAYLOAD_SHA;
31use sha2::{Digest, Sha256};
32
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub enum HttpMethod {
35    Delete,
36    Get,
37    Put,
38    Post,
39    Head,
40}
41
42use std::fmt;
43
44impl fmt::Display for HttpMethod {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            HttpMethod::Delete => write!(f, "DELETE"),
48            HttpMethod::Get => write!(f, "GET"),
49            HttpMethod::Post => write!(f, "POST"),
50            HttpMethod::Put => write!(f, "PUT"),
51            HttpMethod::Head => write!(f, "HEAD"),
52        }
53    }
54}
55use crate::bucket_ops::BucketConfiguration;
56use http::HeaderMap;
57
58#[derive(Clone, Debug)]
59pub struct Multipart<'a> {
60    part_number: u32,
61    upload_id: &'a str,
62}
63
64impl<'a> Multipart<'a> {
65    pub fn query_string(&self) -> String {
66        format!(
67            "?partNumber={}&uploadId={}",
68            self.part_number, self.upload_id
69        )
70    }
71
72    pub fn new(part_number: u32, upload_id: &'a str) -> Self {
73        Multipart {
74            part_number,
75            upload_id,
76        }
77    }
78}
79
80#[derive(Clone, Debug)]
81pub enum Command<'a> {
82    HeadObject,
83    CopyObject {
84        from: &'a str,
85    },
86    DeleteObject,
87    DeleteObjectTagging,
88    GetObject,
89    GetObjectTorrent,
90    GetObjectRange {
91        start: u64,
92        end: Option<u64>,
93    },
94    GetObjectTagging,
95    PutObject {
96        content: &'a [u8],
97        content_type: &'a str,
98        custom_headers: Option<HeaderMap>,
99        multipart: Option<Multipart<'a>>,
100    },
101    PutObjectTagging {
102        tags: &'a str,
103    },
104    ListMultipartUploads {
105        prefix: Option<&'a str>,
106        delimiter: Option<&'a str>,
107        key_marker: Option<String>,
108        upload_id_marker: Option<String>,
109        max_uploads: Option<usize>,
110    },
111    ListObjects {
112        prefix: String,
113        delimiter: Option<String>,
114        marker: Option<String>,
115        max_keys: Option<usize>,
116    },
117    ListObjectsV2 {
118        prefix: String,
119        delimiter: Option<String>,
120        continuation_token: Option<String>,
121        start_after: Option<String>,
122        max_keys: Option<usize>,
123    },
124    GetBucketLocation,
125    PresignGet {
126        expiry_secs: u32,
127        custom_queries: Option<HashMap<String, String>>,
128    },
129    PresignPut {
130        expiry_secs: u32,
131        custom_headers: Option<HeaderMap>,
132        custom_queries: Option<HashMap<String, String>>,
133    },
134    PresignDelete {
135        expiry_secs: u32,
136    },
137    InitiateMultipartUpload {
138        content_type: &'a str,
139    },
140    UploadPart {
141        part_number: u32,
142        content: &'a [u8],
143        upload_id: &'a str,
144    },
145    AbortMultipartUpload {
146        upload_id: &'a str,
147    },
148    CompleteMultipartUpload {
149        upload_id: &'a str,
150        data: CompleteMultipartUploadData,
151    },
152    CreateBucket {
153        config: BucketConfiguration,
154    },
155    DeleteBucket,
156    ListBuckets,
157    GetBucketCors {
158        expected_bucket_owner: String,
159    },
160    PutBucketCors {
161        expected_bucket_owner: String,
162        configuration: CorsConfiguration,
163    },
164    DeleteBucketCors {
165        expected_bucket_owner: String,
166    },
167    GetBucketLifecycle,
168    PutBucketLifecycle {
169        configuration: BucketLifecycleConfiguration,
170    },
171    DeleteBucketLifecycle,
172    GetBucketPolicy,
173    PutBucketPolicy {
174        policy: String,
175    },
176    DeleteBucketPolicy,
177    GetObjectAttributes {
178        expected_bucket_owner: String,
179        version_id: Option<String>,
180    },
181    DeleteObjects {
182        data: DeleteObjectsRequest,
183    },
184}
185
186impl<'a> Command<'a> {
187    /// Whether this command can be replayed after an ambiguous transport failure.
188    ///
189    /// This intentionally describes request semantics rather than HTTP verbs:
190    /// some PUT/POST/DELETE operations create additional state when replayed.
191    pub(crate) fn is_retry_safe(&self) -> bool {
192        matches!(
193            self,
194            Command::HeadObject
195                | Command::GetObject
196                | Command::GetObjectTorrent
197                | Command::GetObjectRange { .. }
198                | Command::GetObjectTagging
199                | Command::ListMultipartUploads { .. }
200                | Command::ListObjects { .. }
201                | Command::ListObjectsV2 { .. }
202                | Command::GetBucketLocation
203                | Command::GetBucketCors { .. }
204                | Command::GetBucketLifecycle
205                | Command::GetBucketPolicy
206                | Command::ListBuckets
207                | Command::GetObjectAttributes { .. }
208        ) || matches!(
209            self,
210            Command::PutObject {
211                multipart: Some(_),
212                ..
213            }
214        )
215    }
216
217    pub fn http_verb(&self) -> HttpMethod {
218        match *self {
219            Command::GetObject
220            | Command::GetObjectTorrent
221            | Command::GetBucketCors { .. }
222            | Command::GetObjectRange { .. }
223            | Command::ListBuckets
224            | Command::ListObjects { .. }
225            | Command::ListObjectsV2 { .. }
226            | Command::GetBucketLocation
227            | Command::GetObjectTagging
228            | Command::GetBucketLifecycle
229            | Command::GetBucketPolicy
230            | Command::ListMultipartUploads { .. }
231            | Command::PresignGet { .. } => HttpMethod::Get,
232            Command::PutObject { .. }
233            | Command::CopyObject { from: _ }
234            | Command::PutObjectTagging { .. }
235            | Command::PresignPut { .. }
236            | Command::UploadPart { .. }
237            | Command::PutBucketCors { .. }
238            | Command::CreateBucket { .. }
239            | Command::PutBucketLifecycle { .. }
240            | Command::PutBucketPolicy { .. } => HttpMethod::Put,
241            Command::DeleteObject
242            | Command::DeleteObjectTagging
243            | Command::AbortMultipartUpload { .. }
244            | Command::PresignDelete { .. }
245            | Command::DeleteBucket
246            | Command::DeleteBucketCors { .. }
247            | Command::DeleteBucketLifecycle
248            | Command::DeleteBucketPolicy => HttpMethod::Delete,
249            Command::InitiateMultipartUpload { .. }
250            | Command::CompleteMultipartUpload { .. }
251            | Command::DeleteObjects { .. } => HttpMethod::Post,
252            Command::HeadObject => HttpMethod::Head,
253            Command::GetObjectAttributes { .. } => HttpMethod::Get,
254        }
255    }
256
257    pub fn content_length(&self) -> Result<usize, S3Error> {
258        let result = match &self {
259            Command::CopyObject { from: _ } => 0,
260            Command::PutObject { content, .. } => content.len(),
261            Command::PutObjectTagging { tags } => tags.len(),
262            Command::UploadPart { content, .. } => content.len(),
263            Command::CompleteMultipartUpload { data, .. } => data.len(),
264            Command::CreateBucket { config } => {
265                if let Some(payload) = config.location_constraint_payload() {
266                    Vec::from(payload).len()
267                } else {
268                    0
269                }
270            }
271            Command::PutBucketLifecycle { configuration } => {
272                quick_xml::se::to_string(configuration)?.len()
273            }
274            Command::PutBucketCors { configuration, .. } => configuration.to_string().len(),
275            Command::HeadObject => 0,
276            Command::DeleteObject => 0,
277            Command::DeleteObjectTagging => 0,
278            Command::GetObject => 0,
279            Command::GetObjectTorrent => 0,
280            Command::GetObjectRange { .. } => 0,
281            Command::GetObjectTagging => 0,
282            Command::ListMultipartUploads { .. } => 0,
283            Command::ListObjects { .. } => 0,
284            Command::ListObjectsV2 { .. } => 0,
285            Command::GetBucketLocation => 0,
286            Command::PresignGet { .. } => 0,
287            Command::PresignPut { .. } => 0,
288            Command::PresignDelete { .. } => 0,
289            Command::InitiateMultipartUpload { .. } => 0,
290            Command::AbortMultipartUpload { .. } => 0,
291            Command::DeleteBucket => 0,
292            Command::ListBuckets => 0,
293            Command::GetBucketCors { .. } => 0,
294            Command::DeleteBucketCors { .. } => 0,
295            Command::GetBucketLifecycle => 0,
296            Command::DeleteBucketLifecycle { .. } => 0,
297            Command::GetBucketPolicy => 0,
298            Command::PutBucketPolicy { policy } => policy.len(),
299            Command::DeleteBucketPolicy => 0,
300            Command::GetObjectAttributes { .. } => 0,
301            Command::DeleteObjects { data } => data.len(),
302        };
303        Ok(result)
304    }
305
306    pub fn content_type(&self) -> String {
307        match self {
308            Command::InitiateMultipartUpload { content_type } => content_type.to_string(),
309            Command::PutObject { content_type, .. } => content_type.to_string(),
310            Command::CompleteMultipartUpload { .. }
311            | Command::PutBucketLifecycle { .. }
312            | Command::PutBucketCors { .. } => "application/xml".into(),
313            Command::HeadObject => "text/plain".into(),
314            Command::DeleteObject => "text/plain".into(),
315            Command::DeleteObjectTagging => "text/plain".into(),
316            Command::GetObject => "text/plain".into(),
317            Command::GetObjectTorrent => "text/plain".into(),
318            Command::GetObjectRange { .. } => "text/plain".into(),
319            Command::GetObjectTagging => "text/plain".into(),
320            Command::ListMultipartUploads { .. } => "text/plain".into(),
321            Command::ListObjects { .. } => "text/plain".into(),
322            Command::ListObjectsV2 { .. } => "text/plain".into(),
323            Command::GetBucketLocation => "text/plain".into(),
324            Command::PresignGet { .. } => "text/plain".into(),
325            Command::PresignPut { .. } => "text/plain".into(),
326            Command::PresignDelete { .. } => "text/plain".into(),
327            Command::AbortMultipartUpload { .. } => "text/plain".into(),
328            Command::DeleteBucket => "text/plain".into(),
329            Command::ListBuckets => "text/plain".into(),
330            Command::GetBucketCors { .. } => "text/plain".into(),
331            Command::DeleteBucketCors { .. } => "text/plain".into(),
332            Command::GetBucketLifecycle => "text/plain".into(),
333            Command::DeleteBucketLifecycle { .. } => "text/plain".into(),
334            Command::GetBucketPolicy => "text/plain".into(),
335            Command::PutBucketPolicy { .. } => "application/json".into(),
336            Command::DeleteBucketPolicy => "text/plain".into(),
337            Command::CopyObject { .. } => "text/plain".into(),
338            Command::PutObjectTagging { .. } => "text/plain".into(),
339            Command::UploadPart { .. } => "text/plain".into(),
340            Command::CreateBucket { .. } => "text/plain".into(),
341            Command::GetObjectAttributes { .. } => "text/plain".into(),
342            Command::DeleteObjects { .. } => "application/xml".into(),
343        }
344    }
345
346    pub fn sha256(&self) -> Result<String, S3Error> {
347        let result = match &self {
348            Command::PutObject { content, .. } => {
349                let mut sha = Sha256::default();
350                sha.update(content);
351                hex::encode(sha.finalize().as_slice())
352            }
353            Command::PutObjectTagging { tags } => {
354                let mut sha = Sha256::default();
355                sha.update(tags.as_bytes());
356                hex::encode(sha.finalize().as_slice())
357            }
358            Command::CompleteMultipartUpload { data, .. } => {
359                let mut sha = Sha256::default();
360                sha.update(data.to_string().as_bytes());
361                hex::encode(sha.finalize().as_slice())
362            }
363            Command::CreateBucket { config } => {
364                if let Some(payload) = config.location_constraint_payload() {
365                    let mut sha = Sha256::default();
366                    sha.update(payload.as_bytes());
367                    hex::encode(sha.finalize().as_slice())
368                } else {
369                    EMPTY_PAYLOAD_SHA.into()
370                }
371            }
372            Command::PutBucketLifecycle { configuration } => {
373                let mut sha = Sha256::default();
374                sha.update(quick_xml::se::to_string(configuration)?.as_bytes());
375                hex::encode(sha.finalize().as_slice())
376            }
377            Command::PutBucketCors { configuration, .. } => {
378                let mut sha = Sha256::default();
379                sha.update(configuration.to_string().as_bytes());
380                hex::encode(sha.finalize().as_slice())
381            }
382            Command::HeadObject => EMPTY_PAYLOAD_SHA.into(),
383            Command::DeleteObject => EMPTY_PAYLOAD_SHA.into(),
384            Command::DeleteObjectTagging => EMPTY_PAYLOAD_SHA.into(),
385            Command::GetObject => EMPTY_PAYLOAD_SHA.into(),
386            Command::GetObjectTorrent => EMPTY_PAYLOAD_SHA.into(),
387            Command::GetObjectRange { .. } => EMPTY_PAYLOAD_SHA.into(),
388            Command::GetObjectTagging => EMPTY_PAYLOAD_SHA.into(),
389            Command::ListMultipartUploads { .. } => EMPTY_PAYLOAD_SHA.into(),
390            Command::ListObjects { .. } => EMPTY_PAYLOAD_SHA.into(),
391            Command::ListObjectsV2 { .. } => EMPTY_PAYLOAD_SHA.into(),
392            Command::GetBucketLocation => EMPTY_PAYLOAD_SHA.into(),
393            Command::PresignGet { .. } => EMPTY_PAYLOAD_SHA.into(),
394            Command::PresignPut { .. } => EMPTY_PAYLOAD_SHA.into(),
395            Command::PresignDelete { .. } => EMPTY_PAYLOAD_SHA.into(),
396            Command::AbortMultipartUpload { .. } => EMPTY_PAYLOAD_SHA.into(),
397            Command::DeleteBucket => EMPTY_PAYLOAD_SHA.into(),
398            Command::ListBuckets => EMPTY_PAYLOAD_SHA.into(),
399            Command::GetBucketCors { .. } => EMPTY_PAYLOAD_SHA.into(),
400            Command::DeleteBucketCors { .. } => EMPTY_PAYLOAD_SHA.into(),
401            Command::GetBucketLifecycle => EMPTY_PAYLOAD_SHA.into(),
402            Command::DeleteBucketLifecycle { .. } => EMPTY_PAYLOAD_SHA.into(),
403            Command::GetBucketPolicy => EMPTY_PAYLOAD_SHA.into(),
404            Command::PutBucketPolicy { policy } => {
405                let mut sha = Sha256::default();
406                sha.update(policy.as_bytes());
407                hex::encode(sha.finalize().as_slice())
408            }
409            Command::DeleteBucketPolicy => EMPTY_PAYLOAD_SHA.into(),
410            Command::CopyObject { .. } => EMPTY_PAYLOAD_SHA.into(),
411            Command::UploadPart { .. } => EMPTY_PAYLOAD_SHA.into(),
412            Command::InitiateMultipartUpload { .. } => EMPTY_PAYLOAD_SHA.into(),
413            Command::GetObjectAttributes { .. } => EMPTY_PAYLOAD_SHA.into(),
414            Command::DeleteObjects { data } => {
415                let mut sha = Sha256::default();
416                sha.update(data.to_string().as_bytes());
417                hex::encode(sha.finalize().as_slice())
418            }
419        };
420        Ok(result)
421    }
422}