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