rusty_oss/actions/multipart_upload/
complete.rs1use std::iter;
2use std::time::Duration;
3
4use serde::Serialize;
5use time::OffsetDateTime;
6use url::Url;
7
8use crate::actions::Method;
9use crate::actions::OSSAction;
10use crate::signing::sign;
11use crate::sorting_iter::SortingIterator;
12use crate::{Bucket, Credentials, Map};
13
14#[derive(Debug, Clone)]
20pub struct CompleteMultipartUpload<'a, I> {
21 bucket: &'a Bucket,
22 credentials: Option<&'a Credentials>,
23 object: &'a str,
24 upload_id: &'a str,
25
26 etags: I,
27
28 query: Map<'a>,
29 headers: Map<'a>,
30}
31
32impl<'a, I> CompleteMultipartUpload<'a, I> {
33 #[inline]
34 pub fn new(
35 bucket: &'a Bucket,
36 credentials: Option<&'a Credentials>,
37 object: &'a str,
38 upload_id: &'a str,
39 etags: I,
40 ) -> Self {
41 Self {
42 bucket,
43 credentials,
44 object,
45
46 upload_id,
47 etags,
48
49 query: Map::new(),
50 headers: Map::new(),
51 }
52 }
53}
54
55impl<'a, I> CompleteMultipartUpload<'a, I>
56where
57 I: Iterator<Item = &'a str>,
58{
59 pub fn body(self) -> String {
60 #[derive(Serialize)]
61 #[serde(rename = "CompleteMultipartUpload")]
62 struct CompleteMultipartUploadSerde<'a> {
63 #[serde(rename = "Part")]
64 parts: Vec<Part<'a>>,
65 }
66
67 #[derive(Serialize)]
68 struct Part<'a> {
69 #[serde(rename = "$value")]
70 nodes: Vec<Node<'a>>,
71 }
72
73 #[derive(Serialize)]
74 enum Node<'a> {
75 ETag(&'a str),
76 PartNumber(u16),
77 }
78
79 let parts = self
80 .etags
81 .enumerate()
82 .map(|(i, etag)| Part {
83 nodes: vec![Node::ETag(etag), Node::PartNumber(i as u16 + 1)],
84 })
85 .collect::<Vec<_>>();
86
87 let req = CompleteMultipartUploadSerde { parts };
88
89 quick_xml::se::to_string(&req).unwrap()
90 }
91}
92
93impl<'a, I> OSSAction<'a> for CompleteMultipartUpload<'a, I>
94where
95 I: Iterator<Item = &'a str>,
96{
97 const METHOD: Method = Method::Post;
98
99 fn query_mut(&mut self) -> &mut Map<'a> {
100 &mut self.query
101 }
102
103 fn headers_mut(&mut self) -> &mut Map<'a> {
104 &mut self.headers
105 }
106
107 fn sign_with_time(&self, expires_in: Duration, time: &OffsetDateTime) -> Url {
108 let url = self.bucket.object_url(self.object).unwrap();
109 let query = iter::once(("uploadId", self.upload_id));
110
111 match self.credentials {
112 Some(credentials) => sign(
113 time,
114 Method::Post,
115 url,
116 credentials.key(),
117 credentials.secret(),
118 credentials.token(),
119 self.bucket.region(),
120 expires_in.as_secs(),
121 SortingIterator::new(query, self.query.iter()),
122 self.headers.iter(),
123 ),
124 None => crate::signing::util::add_query_params(url, query),
125 }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use time::OffsetDateTime;
132
133 use pretty_assertions::assert_eq;
134
135 use super::*;
136 use crate::{Bucket, Credentials, UrlStyle};
137
138 #[test]
139 fn oss_example() {
140 let date = OffsetDateTime::from_unix_timestamp(1369353600).unwrap();
142 let expires_in = Duration::from_secs(86400);
143
144 let endpoint = "https://oss-cn-hangzhou.aliyuncs.com".parse().unwrap();
145 let bucket = Bucket::new(
146 endpoint,
147 UrlStyle::VirtualHost,
148 "examplebucket",
149 "cn-hangzhou",
150 )
151 .unwrap();
152 let credentials = Credentials::new(
153 "access_key_id",
154 "access_key_secret",
155 );
156
157 let etags = ["123456789", "abcdef"];
158 let action = CompleteMultipartUpload::new(
159 &bucket,
160 Some(&credentials),
161 "test.txt",
162 "abcd",
163 etags.iter().copied(),
164 );
165
166 let url = action.sign_with_time(expires_in, &date);
167 let expected = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/test.txt?uploadId=abcd&x-oss-additional-headers=host&x-oss-credential=access_key_id%2F20130524%2Fcn-hangzhou%2Foss%2Faliyun_v4_request&x-oss-date=20130524T000000Z&x-oss-expires=86400&x-oss-signature-version=OSS4-HMAC-SHA256&x-oss-signature=3bc39a1ea4e721fd3dd10d79eca7df7fcb7e15fcb010198322fa471114237558";
168
169 assert_eq!(expected, url.as_str());
170
171 let expected = "<CompleteMultipartUpload><Part><ETag>123456789</ETag><PartNumber>1</PartNumber></Part><Part><ETag>abcdef</ETag><PartNumber>2</PartNumber></Part></CompleteMultipartUpload>";
172 assert_eq!(action.body(), expected);
173 }
174
175 #[test]
176 fn anonymous_custom_query() {
177 let expires_in = Duration::from_secs(86400);
178
179 let endpoint = "https://oss-cn-hangzhou.aliyuncs.com".parse().unwrap();
180 let bucket = Bucket::new(
181 endpoint,
182 UrlStyle::VirtualHost,
183 "examplebucket",
184 "cn-hangzhou",
185 )
186 .unwrap();
187
188 let etags = ["123456789", "abcdef"];
189 let action =
190 CompleteMultipartUpload::new(&bucket, None, "test.txt", "abcd", etags.iter().copied());
191 let url = action.sign(expires_in);
192 let expected = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/test.txt?uploadId=abcd";
193
194 assert_eq!(expected, url.as_str());
195 }
196}