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};
28
29use crate::EMPTY_PAYLOAD_SHA;
30use sha2::{Digest, Sha256};
31
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub enum HttpMethod {
34    Delete,
35    Get,
36    Put,
37    Post,
38    Head,
39}
40
41use std::fmt;
42
43impl fmt::Display for HttpMethod {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            HttpMethod::Delete => write!(f, "DELETE"),
47            HttpMethod::Get => write!(f, "GET"),
48            HttpMethod::Post => write!(f, "POST"),
49            HttpMethod::Put => write!(f, "PUT"),
50            HttpMethod::Head => write!(f, "HEAD"),
51        }
52    }
53}
54use crate::bucket_ops::BucketConfiguration;
55use http::HeaderMap;
56
57#[derive(Clone, Debug)]
58pub struct Multipart<'a> {
59    part_number: u32,
60    upload_id: &'a str,
61}
62
63impl<'a> Multipart<'a> {
64    pub fn query_string(&self) -> String {
65        format!(
66            "?partNumber={}&uploadId={}",
67            self.part_number, self.upload_id
68        )
69    }
70
71    pub fn new(part_number: u32, upload_id: &'a str) -> Self {
72        Multipart {
73            part_number,
74            upload_id,
75        }
76    }
77}
78
79#[derive(Clone, Debug)]
80pub enum Command<'a> {
81    HeadObject,
82    CopyObject {
83        from: &'a str,
84    },
85    DeleteObject,
86    DeleteObjectTagging,
87    GetObject,
88    GetObjectTorrent,
89    GetObjectRange {
90        start: u64,
91        end: Option<u64>,
92    },
93    GetObjectTagging,
94    PutObject {
95        content: &'a [u8],
96        content_type: &'a str,
97        custom_headers: Option<http::HeaderMap>,
98        multipart: Option<Multipart<'a>>,
99    },
100    PutObjectTagging {
101        tags: &'a str,
102    },
103    ListMultipartUploads {
104        prefix: Option<&'a str>,
105        delimiter: Option<&'a str>,
106        key_marker: Option<String>,
107        max_uploads: Option<usize>,
108    },
109    ListObjects {
110        prefix: String,
111        delimiter: Option<String>,
112        marker: Option<String>,
113        max_keys: Option<usize>,
114    },
115    ListObjectsV2 {
116        prefix: String,
117        delimiter: Option<String>,
118        continuation_token: Option<String>,
119        start_after: Option<String>,
120        max_keys: Option<usize>,
121    },
122    GetBucketLocation,
123    PresignGet {
124        expiry_secs: u32,
125        custom_queries: Option<HashMap<String, String>>,
126    },
127    PresignPut {
128        expiry_secs: u32,
129        custom_headers: Option<HeaderMap>,
130        custom_queries: Option<HashMap<String, String>>,
131    },
132    PresignDelete {
133        expiry_secs: u32,
134    },
135    InitiateMultipartUpload {
136        content_type: &'a str,
137    },
138    UploadPart {
139        part_number: u32,
140        content: &'a [u8],
141        upload_id: &'a str,
142    },
143    AbortMultipartUpload {
144        upload_id: &'a str,
145    },
146    CompleteMultipartUpload {
147        upload_id: &'a str,
148        data: CompleteMultipartUploadData,
149    },
150    CreateBucket {
151        config: BucketConfiguration,
152    },
153    DeleteBucket,
154    ListBuckets,
155    GetBucketCors {
156        expected_bucket_owner: String,
157    },
158    PutBucketCors {
159        expected_bucket_owner: String,
160        configuration: CorsConfiguration,
161    },
162    DeleteBucketCors {
163        expected_bucket_owner: String,
164    },
165    GetBucketLifecycle,
166    PutBucketLifecycle {
167        configuration: BucketLifecycleConfiguration,
168    },
169    DeleteBucketLifecycle,
170    GetObjectAttributes {
171        expected_bucket_owner: String,
172        version_id: Option<String>,
173    },
174}
175
176impl<'a> Command<'a> {
177    pub fn http_verb(&self) -> HttpMethod {
178        match *self {
179            Command::GetObject
180            | Command::GetObjectTorrent
181            | Command::GetBucketCors { .. }
182            | Command::GetObjectRange { .. }
183            | Command::ListBuckets
184            | Command::ListObjects { .. }
185            | Command::ListObjectsV2 { .. }
186            | Command::GetBucketLocation
187            | Command::GetObjectTagging
188            | Command::GetBucketLifecycle
189            | Command::ListMultipartUploads { .. }
190            | Command::PresignGet { .. } => HttpMethod::Get,
191            Command::PutObject { .. }
192            | Command::CopyObject { from: _ }
193            | Command::PutObjectTagging { .. }
194            | Command::PresignPut { .. }
195            | Command::UploadPart { .. }
196            | Command::PutBucketCors { .. }
197            | Command::CreateBucket { .. }
198            | Command::PutBucketLifecycle { .. } => HttpMethod::Put,
199            Command::DeleteObject
200            | Command::DeleteObjectTagging
201            | Command::AbortMultipartUpload { .. }
202            | Command::PresignDelete { .. }
203            | Command::DeleteBucket
204            | Command::DeleteBucketCors { .. }
205            | Command::DeleteBucketLifecycle => HttpMethod::Delete,
206            Command::InitiateMultipartUpload { .. } | Command::CompleteMultipartUpload { .. } => {
207                HttpMethod::Post
208            }
209            Command::HeadObject => HttpMethod::Head,
210            Command::GetObjectAttributes { .. } => HttpMethod::Get,
211        }
212    }
213
214    pub fn content_length(&self) -> Result<usize, S3Error> {
215        let result = match &self {
216            Command::CopyObject { from: _ } => 0,
217            Command::PutObject { content, .. } => content.len(),
218            Command::PutObjectTagging { tags } => tags.len(),
219            Command::UploadPart { content, .. } => content.len(),
220            Command::CompleteMultipartUpload { data, .. } => data.len(),
221            Command::CreateBucket { config } => {
222                if let Some(payload) = config.location_constraint_payload() {
223                    Vec::from(payload).len()
224                } else {
225                    0
226                }
227            }
228            Command::PutBucketLifecycle { configuration } => {
229                quick_xml::se::to_string(configuration)?.len()
230            }
231            Command::PutBucketCors { configuration, .. } => configuration.to_string().len(),
232            Command::HeadObject => 0,
233            Command::DeleteObject => 0,
234            Command::DeleteObjectTagging => 0,
235            Command::GetObject => 0,
236            Command::GetObjectTorrent => 0,
237            Command::GetObjectRange { .. } => 0,
238            Command::GetObjectTagging => 0,
239            Command::ListMultipartUploads { .. } => 0,
240            Command::ListObjects { .. } => 0,
241            Command::ListObjectsV2 { .. } => 0,
242            Command::GetBucketLocation => 0,
243            Command::PresignGet { .. } => 0,
244            Command::PresignPut { .. } => 0,
245            Command::PresignDelete { .. } => 0,
246            Command::InitiateMultipartUpload { .. } => 0,
247            Command::AbortMultipartUpload { .. } => 0,
248            Command::DeleteBucket => 0,
249            Command::ListBuckets => 0,
250            Command::GetBucketCors { .. } => 0,
251            Command::DeleteBucketCors { .. } => 0,
252            Command::GetBucketLifecycle => 0,
253            Command::DeleteBucketLifecycle { .. } => 0,
254            Command::GetObjectAttributes { .. } => 0,
255        };
256        Ok(result)
257    }
258
259    pub fn content_type(&self) -> String {
260        match self {
261            Command::InitiateMultipartUpload { content_type } => content_type.to_string(),
262            Command::PutObject { content_type, .. } => content_type.to_string(),
263            Command::CompleteMultipartUpload { .. }
264            | Command::PutBucketLifecycle { .. }
265            | Command::PutBucketCors { .. } => "application/xml".into(),
266            Command::HeadObject => "text/plain".into(),
267            Command::DeleteObject => "text/plain".into(),
268            Command::DeleteObjectTagging => "text/plain".into(),
269            Command::GetObject => "text/plain".into(),
270            Command::GetObjectTorrent => "text/plain".into(),
271            Command::GetObjectRange { .. } => "text/plain".into(),
272            Command::GetObjectTagging => "text/plain".into(),
273            Command::ListMultipartUploads { .. } => "text/plain".into(),
274            Command::ListObjects { .. } => "text/plain".into(),
275            Command::ListObjectsV2 { .. } => "text/plain".into(),
276            Command::GetBucketLocation => "text/plain".into(),
277            Command::PresignGet { .. } => "text/plain".into(),
278            Command::PresignPut { .. } => "text/plain".into(),
279            Command::PresignDelete { .. } => "text/plain".into(),
280            Command::AbortMultipartUpload { .. } => "text/plain".into(),
281            Command::DeleteBucket => "text/plain".into(),
282            Command::ListBuckets => "text/plain".into(),
283            Command::GetBucketCors { .. } => "text/plain".into(),
284            Command::DeleteBucketCors { .. } => "text/plain".into(),
285            Command::GetBucketLifecycle => "text/plain".into(),
286            Command::DeleteBucketLifecycle { .. } => "text/plain".into(),
287            Command::CopyObject { .. } => "text/plain".into(),
288            Command::PutObjectTagging { .. } => "text/plain".into(),
289            Command::UploadPart { .. } => "text/plain".into(),
290            Command::CreateBucket { .. } => "text/plain".into(),
291            Command::GetObjectAttributes { .. } => "text/plain".into(),
292        }
293    }
294
295    pub fn sha256(&self) -> Result<String, S3Error> {
296        let result = match &self {
297            Command::PutObject { content, .. } => {
298                let mut sha = Sha256::default();
299                sha.update(content);
300                hex::encode(sha.finalize().as_slice())
301            }
302            Command::PutObjectTagging { tags } => {
303                let mut sha = Sha256::default();
304                sha.update(tags.as_bytes());
305                hex::encode(sha.finalize().as_slice())
306            }
307            Command::CompleteMultipartUpload { data, .. } => {
308                let mut sha = Sha256::default();
309                sha.update(data.to_string().as_bytes());
310                hex::encode(sha.finalize().as_slice())
311            }
312            Command::CreateBucket { config } => {
313                if let Some(payload) = config.location_constraint_payload() {
314                    let mut sha = Sha256::default();
315                    sha.update(payload.as_bytes());
316                    hex::encode(sha.finalize().as_slice())
317                } else {
318                    EMPTY_PAYLOAD_SHA.into()
319                }
320            }
321            Command::PutBucketLifecycle { configuration } => {
322                let mut sha = Sha256::default();
323                sha.update(quick_xml::se::to_string(configuration)?.as_bytes());
324                hex::encode(sha.finalize().as_slice())
325            }
326            Command::PutBucketCors { configuration, .. } => {
327                let mut sha = Sha256::default();
328                sha.update(configuration.to_string().as_bytes());
329                hex::encode(sha.finalize().as_slice())
330            }
331            Command::HeadObject => EMPTY_PAYLOAD_SHA.into(),
332            Command::DeleteObject => EMPTY_PAYLOAD_SHA.into(),
333            Command::DeleteObjectTagging => EMPTY_PAYLOAD_SHA.into(),
334            Command::GetObject => EMPTY_PAYLOAD_SHA.into(),
335            Command::GetObjectTorrent => EMPTY_PAYLOAD_SHA.into(),
336            Command::GetObjectRange { .. } => EMPTY_PAYLOAD_SHA.into(),
337            Command::GetObjectTagging => EMPTY_PAYLOAD_SHA.into(),
338            Command::ListMultipartUploads { .. } => EMPTY_PAYLOAD_SHA.into(),
339            Command::ListObjects { .. } => EMPTY_PAYLOAD_SHA.into(),
340            Command::ListObjectsV2 { .. } => EMPTY_PAYLOAD_SHA.into(),
341            Command::GetBucketLocation => EMPTY_PAYLOAD_SHA.into(),
342            Command::PresignGet { .. } => EMPTY_PAYLOAD_SHA.into(),
343            Command::PresignPut { .. } => EMPTY_PAYLOAD_SHA.into(),
344            Command::PresignDelete { .. } => EMPTY_PAYLOAD_SHA.into(),
345            Command::AbortMultipartUpload { .. } => EMPTY_PAYLOAD_SHA.into(),
346            Command::DeleteBucket => EMPTY_PAYLOAD_SHA.into(),
347            Command::ListBuckets => EMPTY_PAYLOAD_SHA.into(),
348            Command::GetBucketCors { .. } => EMPTY_PAYLOAD_SHA.into(),
349            Command::DeleteBucketCors { .. } => EMPTY_PAYLOAD_SHA.into(),
350            Command::GetBucketLifecycle => EMPTY_PAYLOAD_SHA.into(),
351            Command::DeleteBucketLifecycle { .. } => EMPTY_PAYLOAD_SHA.into(),
352            Command::CopyObject { .. } => EMPTY_PAYLOAD_SHA.into(),
353            Command::UploadPart { .. } => EMPTY_PAYLOAD_SHA.into(),
354            Command::InitiateMultipartUpload { .. } => EMPTY_PAYLOAD_SHA.into(),
355            Command::GetObjectAttributes { .. } => EMPTY_PAYLOAD_SHA.into(),
356        };
357        Ok(result)
358    }
359}