rusty_oss/actions/multipart_upload/
complete.rs

1use 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/// Complete a multipart upload.
15///
16/// Find out more about `CompleteMultipartUpload` from the [OSS API Reference][api]
17///
18/// [api]: https://help.aliyun.com/zh/oss/developer-reference/completemultipartupload
19#[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        // Fri, 24 May 2013 00:00:00 GMT
141        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}