Skip to main content

rusty_s3/actions/multipart_upload/
complete.rs

1use std::iter;
2use std::time::Duration;
3
4use instant_xml::ToXml;
5use jiff::Timestamp;
6use url::Url;
7
8use crate::actions::Method;
9use crate::actions::S3Action;
10use crate::signing::sign;
11use crate::sorting_iter::SortingIterator;
12use crate::{Bucket, Credentials, Map};
13
14/// Complete a multipart upload.
15///
16/// Find out more about `CompleteMultipartUpload` from the [AWS API Reference][api]
17///
18/// [api]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateMultipartUpload.html
19#[allow(clippy::module_name_repetitions)]
20#[derive(Debug, Clone)]
21pub struct CompleteMultipartUpload<'a, I> {
22    bucket: &'a Bucket,
23    credentials: Option<&'a Credentials>,
24    object: &'a str,
25    upload_id: &'a str,
26
27    etags: I,
28
29    query: Map<'a>,
30    headers: Map<'a>,
31}
32
33impl<'a, I> CompleteMultipartUpload<'a, I> {
34    #[inline]
35    pub const fn new(
36        bucket: &'a Bucket,
37        credentials: Option<&'a Credentials>,
38        object: &'a str,
39        upload_id: &'a str,
40        etags: I,
41    ) -> Self {
42        Self {
43            bucket,
44            credentials,
45            object,
46
47            upload_id,
48            etags,
49
50            query: Map::new(),
51            headers: Map::new(),
52        }
53    }
54}
55
56impl<'a, I> CompleteMultipartUpload<'a, I>
57where
58    I: Iterator<Item = &'a str>,
59{
60    /// Generate the XML body for the request.
61    ///
62    /// # Panics
63    ///
64    /// Panics if an index is not representable as a `u16`.
65    pub fn body(self) -> String {
66        #[derive(ToXml)]
67        #[xml(rename = "CompleteMultipartUpload")]
68        struct CompleteMultipartUploadBody<'a> {
69            parts: Vec<Part<'a>>,
70        }
71
72        #[derive(ToXml)]
73        #[xml(rename = "Part")]
74        struct Part<'a> {
75            #[xml(rename = "ETag")]
76            etag: &'a str,
77            #[xml(rename = "PartNumber")]
78            part_number: u16,
79        }
80
81        let parts = self
82            .etags
83            .enumerate()
84            .map(|(i, etag)| Part {
85                etag,
86                part_number: u16::try_from(i).expect("convert to u16") + 1,
87            })
88            .collect::<Vec<_>>();
89
90        let req = CompleteMultipartUploadBody { parts };
91
92        instant_xml::to_string(&req).unwrap()
93    }
94}
95
96impl<'a, I> S3Action<'a> for CompleteMultipartUpload<'a, I>
97where
98    I: Iterator<Item = &'a str>,
99{
100    const METHOD: Method = Method::Post;
101
102    fn query_mut(&mut self) -> &mut Map<'a> {
103        &mut self.query
104    }
105
106    fn headers_mut(&mut self) -> &mut Map<'a> {
107        &mut self.headers
108    }
109
110    fn sign_with_time(&self, expires_in: Duration, time: &Timestamp) -> Url {
111        let url = self.bucket.object_url(self.object).unwrap();
112        let query = iter::once(("uploadId", self.upload_id));
113
114        match self.credentials {
115            Some(credentials) => sign(
116                time,
117                Self::METHOD,
118                url,
119                credentials.key(),
120                credentials.secret(),
121                credentials.token(),
122                self.bucket.region(),
123                expires_in.as_secs(),
124                SortingIterator::new(query, self.query.iter()),
125                self.headers.iter(),
126            ),
127            None => crate::signing::util::add_query_params(url, query),
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use pretty_assertions::assert_eq;
135
136    use super::*;
137    use crate::{Bucket, Credentials, UrlStyle};
138
139    #[test]
140    fn aws_example() {
141        // Fri, 24 May 2013 00:00:00 GMT
142        let date = Timestamp::from_second(1369353600).unwrap();
143        let expires_in = Duration::from_secs(86400);
144
145        let endpoint = "https://s3.amazonaws.com".parse().unwrap();
146        let bucket = Bucket::new(
147            endpoint,
148            UrlStyle::VirtualHost,
149            "examplebucket",
150            "us-east-1",
151        )
152        .unwrap();
153        let credentials = Credentials::new(
154            "AKIAIOSFODNN7EXAMPLE",
155            "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
156        );
157
158        let etags = ["123456789", "abcdef"];
159        let action = CompleteMultipartUpload::new(
160            &bucket,
161            Some(&credentials),
162            "test.txt",
163            "abcd",
164            etags.iter().copied(),
165        );
166
167        let url = action.sign_with_time(expires_in, &date);
168        let expected = "https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130524T000000Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&uploadId=abcd&X-Amz-Signature=19b9d341ce3c6ebd9f049882e875dcad4adc493d9d46d55148f4113146c53dd8";
169
170        assert_eq!(expected, url.as_str());
171
172        let expected = "<CompleteMultipartUpload><Part><ETag>123456789</ETag><PartNumber>1</PartNumber></Part><Part><ETag>abcdef</ETag><PartNumber>2</PartNumber></Part></CompleteMultipartUpload>";
173        assert_eq!(action.body(), expected);
174    }
175
176    #[test]
177    fn anonymous_custom_query() {
178        let expires_in = Duration::from_secs(86400);
179
180        let endpoint = "https://s3.amazonaws.com".parse().unwrap();
181        let bucket = Bucket::new(
182            endpoint,
183            UrlStyle::VirtualHost,
184            "examplebucket",
185            "us-east-1",
186        )
187        .unwrap();
188
189        let etags = ["123456789", "abcdef"];
190        let action =
191            CompleteMultipartUpload::new(&bucket, None, "test.txt", "abcd", etags.iter().copied());
192        let url = action.sign(expires_in);
193        let expected = "https://examplebucket.s3.amazonaws.com/test.txt?uploadId=abcd";
194
195        assert_eq!(expected, url.as_str());
196    }
197}