rusty_s3/actions/multipart_upload/
complete.rs1use 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#[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 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 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}