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        max_uploads: Option<usize>,
109    },
110    ListObjects {
111        prefix: String,
112        delimiter: Option<String>,
113        marker: Option<String>,
114        max_keys: Option<usize>,
115    },
116    ListObjectsV2 {
117        prefix: String,
118        delimiter: Option<String>,
119        continuation_token: Option<String>,
120        start_after: Option<String>,
121        max_keys: Option<usize>,
122    },
123    GetBucketLocation,
124    PresignGet {
125        expiry_secs: u32,
126        custom_queries: Option<HashMap<String, String>>,
127    },
128    PresignPut {
129        expiry_secs: u32,
130        custom_headers: Option<HeaderMap>,
131        custom_queries: Option<HashMap<String, String>>,
132    },
133    PresignDelete {
134        expiry_secs: u32,
135    },
136    InitiateMultipartUpload {
137        content_type: &'a str,
138    },
139    UploadPart {
140        part_number: u32,
141        content: &'a [u8],
142        upload_id: &'a str,
143    },
144    AbortMultipartUpload {
145        upload_id: &'a str,
146    },
147    CompleteMultipartUpload {
148        upload_id: &'a str,
149        data: CompleteMultipartUploadData,
150    },
151    CreateBucket {
152        config: BucketConfiguration,
153    },
154    DeleteBucket,
155    ListBuckets,
156    GetBucketCors {
157        expected_bucket_owner: String,
158    },
159    PutBucketCors {
160        expected_bucket_owner: String,
161        configuration: CorsConfiguration,
162    },
163    DeleteBucketCors {
164        expected_bucket_owner: String,
165    },
166    GetBucketLifecycle,
167    PutBucketLifecycle {
168        configuration: BucketLifecycleConfiguration,
169    },
170    DeleteBucketLifecycle,
171    GetObjectAttributes {
172        expected_bucket_owner: String,
173        version_id: Option<String>,
174    },
175    DeleteObjects {
176        data: DeleteObjectsRequest,
177    },
178}
179
180impl<'a> Command<'a> {
181    pub fn http_verb(&self) -> HttpMethod {
182        match *self {
183            Command::GetObject
184            | Command::GetObjectTorrent
185            | Command::GetBucketCors { .. }
186            | Command::GetObjectRange { .. }
187            | Command::ListBuckets
188            | Command::ListObjects { .. }
189            | Command::ListObjectsV2 { .. }
190            | Command::GetBucketLocation
191            | Command::GetObjectTagging
192            | Command::GetBucketLifecycle
193            | Command::ListMultipartUploads { .. }
194            | Command::PresignGet { .. } => HttpMethod::Get,
195            Command::PutObject { .. }
196            | Command::CopyObject { from: _ }
197            | Command::PutObjectTagging { .. }
198            | Command::PresignPut { .. }
199            | Command::UploadPart { .. }
200            | Command::PutBucketCors { .. }
201            | Command::CreateBucket { .. }
202            | Command::PutBucketLifecycle { .. } => HttpMethod::Put,
203            Command::DeleteObject
204            | Command::DeleteObjectTagging
205            | Command::AbortMultipartUpload { .. }
206            | Command::PresignDelete { .. }
207            | Command::DeleteBucket
208            | Command::DeleteBucketCors { .. }
209            | Command::DeleteBucketLifecycle => HttpMethod::Delete,
210            Command::InitiateMultipartUpload { .. }
211            | Command::CompleteMultipartUpload { .. }
212            | Command::DeleteObjects { .. } => HttpMethod::Post,
213            Command::HeadObject => HttpMethod::Head,
214            Command::GetObjectAttributes { .. } => HttpMethod::Get,
215        }
216    }
217
218    pub fn content_length(&self) -> Result<usize, S3Error> {
219        let result = match &self {
220            Command::CopyObject { from: _ } => 0,
221            Command::PutObject { content, .. } => content.len(),
222            Command::PutObjectTagging { tags } => tags.len(),
223            Command::UploadPart { content, .. } => content.len(),
224            Command::CompleteMultipartUpload { data, .. } => data.len(),
225            Command::CreateBucket { config } => {
226                if let Some(payload) = config.location_constraint_payload() {
227                    Vec::from(payload).len()
228                } else {
229                    0
230                }
231            }
232            Command::PutBucketLifecycle { configuration } => {
233                quick_xml::se::to_string(configuration)?.len()
234            }
235            Command::PutBucketCors { configuration, .. } => configuration.to_string().len(),
236            Command::HeadObject => 0,
237            Command::DeleteObject => 0,
238            Command::DeleteObjectTagging => 0,
239            Command::GetObject => 0,
240            Command::GetObjectTorrent => 0,
241            Command::GetObjectRange { .. } => 0,
242            Command::GetObjectTagging => 0,
243            Command::ListMultipartUploads { .. } => 0,
244            Command::ListObjects { .. } => 0,
245            Command::ListObjectsV2 { .. } => 0,
246            Command::GetBucketLocation => 0,
247            Command::PresignGet { .. } => 0,
248            Command::PresignPut { .. } => 0,
249            Command::PresignDelete { .. } => 0,
250            Command::InitiateMultipartUpload { .. } => 0,
251            Command::AbortMultipartUpload { .. } => 0,
252            Command::DeleteBucket => 0,
253            Command::ListBuckets => 0,
254            Command::GetBucketCors { .. } => 0,
255            Command::DeleteBucketCors { .. } => 0,
256            Command::GetBucketLifecycle => 0,
257            Command::DeleteBucketLifecycle { .. } => 0,
258            Command::GetObjectAttributes { .. } => 0,
259            Command::DeleteObjects { data } => data.len(),
260        };
261        Ok(result)
262    }
263
264    pub fn content_type(&self) -> String {
265        match self {
266            Command::InitiateMultipartUpload { content_type } => content_type.to_string(),
267            Command::PutObject { content_type, .. } => content_type.to_string(),
268            Command::CompleteMultipartUpload { .. }
269            | Command::PutBucketLifecycle { .. }
270            | Command::PutBucketCors { .. } => "application/xml".into(),
271            Command::HeadObject => "text/plain".into(),
272            Command::DeleteObject => "text/plain".into(),
273            Command::DeleteObjectTagging => "text/plain".into(),
274            Command::GetObject => "text/plain".into(),
275            Command::GetObjectTorrent => "text/plain".into(),
276            Command::GetObjectRange { .. } => "text/plain".into(),
277            Command::GetObjectTagging => "text/plain".into(),
278            Command::ListMultipartUploads { .. } => "text/plain".into(),
279            Command::ListObjects { .. } => "text/plain".into(),
280            Command::ListObjectsV2 { .. } => "text/plain".into(),
281            Command::GetBucketLocation => "text/plain".into(),
282            Command::PresignGet { .. } => "text/plain".into(),
283            Command::PresignPut { .. } => "text/plain".into(),
284            Command::PresignDelete { .. } => "text/plain".into(),
285            Command::AbortMultipartUpload { .. } => "text/plain".into(),
286            Command::DeleteBucket => "text/plain".into(),
287            Command::ListBuckets => "text/plain".into(),
288            Command::GetBucketCors { .. } => "text/plain".into(),
289            Command::DeleteBucketCors { .. } => "text/plain".into(),
290            Command::GetBucketLifecycle => "text/plain".into(),
291            Command::DeleteBucketLifecycle { .. } => "text/plain".into(),
292            Command::CopyObject { .. } => "text/plain".into(),
293            Command::PutObjectTagging { .. } => "text/plain".into(),
294            Command::UploadPart { .. } => "text/plain".into(),
295            Command::CreateBucket { .. } => "text/plain".into(),
296            Command::GetObjectAttributes { .. } => "text/plain".into(),
297            Command::DeleteObjects { .. } => "application/xml".into(),
298        }
299    }
300
301    pub fn sha256(&self) -> Result<String, S3Error> {
302        let result = match &self {
303            Command::PutObject { content, .. } => {
304                let mut sha = Sha256::default();
305                sha.update(content);
306                hex::encode(sha.finalize().as_slice())
307            }
308            Command::PutObjectTagging { tags } => {
309                let mut sha = Sha256::default();
310                sha.update(tags.as_bytes());
311                hex::encode(sha.finalize().as_slice())
312            }
313            Command::CompleteMultipartUpload { data, .. } => {
314                let mut sha = Sha256::default();
315                sha.update(data.to_string().as_bytes());
316                hex::encode(sha.finalize().as_slice())
317            }
318            Command::CreateBucket { config } => {
319                if let Some(payload) = config.location_constraint_payload() {
320                    let mut sha = Sha256::default();
321                    sha.update(payload.as_bytes());
322                    hex::encode(sha.finalize().as_slice())
323                } else {
324                    EMPTY_PAYLOAD_SHA.into()
325                }
326            }
327            Command::PutBucketLifecycle { configuration } => {
328                let mut sha = Sha256::default();
329                sha.update(quick_xml::se::to_string(configuration)?.as_bytes());
330                hex::encode(sha.finalize().as_slice())
331            }
332            Command::PutBucketCors { configuration, .. } => {
333                let mut sha = Sha256::default();
334                sha.update(configuration.to_string().as_bytes());
335                hex::encode(sha.finalize().as_slice())
336            }
337            Command::HeadObject => EMPTY_PAYLOAD_SHA.into(),
338            Command::DeleteObject => EMPTY_PAYLOAD_SHA.into(),
339            Command::DeleteObjectTagging => EMPTY_PAYLOAD_SHA.into(),
340            Command::GetObject => EMPTY_PAYLOAD_SHA.into(),
341            Command::GetObjectTorrent => EMPTY_PAYLOAD_SHA.into(),
342            Command::GetObjectRange { .. } => EMPTY_PAYLOAD_SHA.into(),
343            Command::GetObjectTagging => EMPTY_PAYLOAD_SHA.into(),
344            Command::ListMultipartUploads { .. } => EMPTY_PAYLOAD_SHA.into(),
345            Command::ListObjects { .. } => EMPTY_PAYLOAD_SHA.into(),
346            Command::ListObjectsV2 { .. } => EMPTY_PAYLOAD_SHA.into(),
347            Command::GetBucketLocation => EMPTY_PAYLOAD_SHA.into(),
348            Command::PresignGet { .. } => EMPTY_PAYLOAD_SHA.into(),
349            Command::PresignPut { .. } => EMPTY_PAYLOAD_SHA.into(),
350            Command::PresignDelete { .. } => EMPTY_PAYLOAD_SHA.into(),
351            Command::AbortMultipartUpload { .. } => EMPTY_PAYLOAD_SHA.into(),
352            Command::DeleteBucket => EMPTY_PAYLOAD_SHA.into(),
353            Command::ListBuckets => EMPTY_PAYLOAD_SHA.into(),
354            Command::GetBucketCors { .. } => EMPTY_PAYLOAD_SHA.into(),
355            Command::DeleteBucketCors { .. } => EMPTY_PAYLOAD_SHA.into(),
356            Command::GetBucketLifecycle => EMPTY_PAYLOAD_SHA.into(),
357            Command::DeleteBucketLifecycle { .. } => EMPTY_PAYLOAD_SHA.into(),
358            Command::CopyObject { .. } => EMPTY_PAYLOAD_SHA.into(),
359            Command::UploadPart { .. } => EMPTY_PAYLOAD_SHA.into(),
360            Command::InitiateMultipartUpload { .. } => EMPTY_PAYLOAD_SHA.into(),
361            Command::GetObjectAttributes { .. } => EMPTY_PAYLOAD_SHA.into(),
362            Command::DeleteObjects { data } => {
363                let mut sha = Sha256::default();
364                sha.update(data.to_string().as_bytes());
365                hex::encode(sha.finalize().as_slice())
366            }
367        };
368        Ok(result)
369    }
370}