rusty_s3/actions/multipart_upload/
complete.rs

1use std::iter;
2use std::time::Duration;
3
4use jiff::Timestamp;
5use serde::Serialize;
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(Serialize)]
67        #[serde(rename = "CompleteMultipartUpload")]
68        struct CompleteMultipartUploadSerde<'a> {
69            #[serde(rename = "Part")]
70            parts: Vec<Part<'a>>,
71        }
72
73        #[derive(Serialize)]
74        struct Part<'a> {
75            #[serde(rename = "$value")]
76            nodes: Vec<Node<'a>>,
77        }
78
79        #[derive(Serialize)]
80        enum Node<'a> {
81            ETag(&'a str),
82            PartNumber(u16),
83        }
84
85        let parts = self
86            .etags
87            .enumerate()
88            .map(|(i, etag)| Part {
89                nodes: vec![
90                    Node::ETag(etag),
91                    Node::PartNumber(u16::try_from(i).expect("convert to u16") + 1),
92                ],
93            })
94            .collect::<Vec<_>>();
95
96        let req = CompleteMultipartUploadSerde { parts };
97
98        quick_xml::se::to_string(&req).unwrap()
99    }
100}
101
102impl<'a, I> S3Action<'a> for CompleteMultipartUpload<'a, I>
103where
104    I: Iterator<Item = &'a str>,
105{
106    const METHOD: Method = Method::Post;
107
108    fn query_mut(&mut self) -> &mut Map<'a> {
109        &mut self.query
110    }
111
112    fn headers_mut(&mut self) -> &mut Map<'a> {
113        &mut self.headers
114    }
115
116    fn sign_with_time(&self, expires_in: Duration, time: &Timestamp) -> Url {
117        let url = self.bucket.object_url(self.object).unwrap();
118        let query = iter::once(("uploadId", self.upload_id));
119
120        match self.credentials {
121            Some(credentials) => sign(
122                time,
123                Self::METHOD,
124                url,
125                credentials.key(),
126                credentials.secret(),
127                credentials.token(),
128                self.bucket.region(),
129                expires_in.as_secs(),
130                SortingIterator::new(query, self.query.iter()),
131                self.headers.iter(),
132            ),
133            None => crate::signing::util::add_query_params(url, query),
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use pretty_assertions::assert_eq;
141
142    use super::*;
143    use crate::{Bucket, Credentials, UrlStyle};
144
145    #[test]
146    fn aws_example() {
147        // Fri, 24 May 2013 00:00:00 GMT
148        let date = Timestamp::from_second(1369353600).unwrap();
149        let expires_in = Duration::from_secs(86400);
150
151        let endpoint = "https://s3.amazonaws.com".parse().unwrap();
152        let bucket = Bucket::new(
153            endpoint,
154            UrlStyle::VirtualHost,
155            "examplebucket",
156            "us-east-1",
157        )
158        .unwrap();
159        let credentials = Credentials::new(
160            "AKIAIOSFODNN7EXAMPLE",
161            "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
162        );
163
164        let etags = ["123456789", "abcdef"];
165        let action = CompleteMultipartUpload::new(
166            &bucket,
167            Some(&credentials),
168            "test.txt",
169            "abcd",
170            etags.iter().copied(),
171        );
172
173        let url = action.sign_with_time(expires_in, &date);
174        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";
175
176        assert_eq!(expected, url.as_str());
177
178        let expected = "<CompleteMultipartUpload><Part><ETag>123456789</ETag><PartNumber>1</PartNumber></Part><Part><ETag>abcdef</ETag><PartNumber>2</PartNumber></Part></CompleteMultipartUpload>";
179        assert_eq!(action.body(), expected);
180    }
181
182    #[test]
183    fn anonymous_custom_query() {
184        let expires_in = Duration::from_secs(86400);
185
186        let endpoint = "https://s3.amazonaws.com".parse().unwrap();
187        let bucket = Bucket::new(
188            endpoint,
189            UrlStyle::VirtualHost,
190            "examplebucket",
191            "us-east-1",
192        )
193        .unwrap();
194
195        let etags = ["123456789", "abcdef"];
196        let action =
197            CompleteMultipartUpload::new(&bucket, None, "test.txt", "abcd", etags.iter().copied());
198        let url = action.sign(expires_in);
199        let expected = "https://examplebucket.s3.amazonaws.com/test.txt?uploadId=abcd";
200
201        assert_eq!(expected, url.as_str());
202    }
203}