rusty_oss/actions/multipart_upload/
list_parts.rs

1use std::iter;
2use std::time::Duration;
3
4use serde::Deserialize;
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/// Lists the parts that have been uploaded for a specific multipart upload.
15///
16/// If `next_part_number_marker` is `Some` the response is truncated, and the
17/// rest of the list can be retrieved by reusing the `ListParts` action
18/// but with `part_number_marker` set to the value of `next_part_number_marker`
19/// received in the previous response.
20///
21/// Find out more about `ListParts` from the [OSS API Reference][api]
22///
23/// [api]: https://help.aliyun.com/zh/oss/developer-reference/listparts
24#[derive(Debug, Clone)]
25pub struct ListParts<'a> {
26    bucket: &'a Bucket,
27    credentials: Option<&'a Credentials>,
28    object: &'a str,
29    upload_id: &'a str,
30
31    query: Map<'a>,
32    headers: Map<'a>,
33}
34
35#[derive(Debug, Clone, Deserialize)]
36pub struct ListPartsResponse {
37    #[serde(rename = "Part")]
38    #[serde(default)]
39    pub parts: Vec<PartsContent>,
40    #[serde(rename = "MaxParts")]
41    pub max_parts: u16,
42    #[serde(rename = "IsTruncated")]
43    is_truncated: bool,
44    #[serde(rename = "NextPartNumberMarker")]
45    pub next_part_number_marker: Option<u16>,
46}
47
48#[derive(Debug, Clone, Deserialize)]
49pub struct PartsContent {
50    #[serde(rename = "PartNumber")]
51    pub number: u16,
52    #[serde(rename = "ETag")]
53    pub etag: String,
54    #[serde(rename = "LastModified")]
55    pub last_modified: String,
56    #[serde(rename = "Size")]
57    pub size: u64,
58}
59
60impl<'a> ListParts<'a> {
61    pub fn new(
62        bucket: &'a Bucket,
63        credentials: Option<&'a Credentials>,
64        object: &'a str,
65        upload_id: &'a str,
66    ) -> Self {
67        Self {
68            bucket,
69            credentials,
70            object,
71            upload_id,
72
73            query: Map::new(),
74            headers: Map::new(),
75        }
76    }
77
78    pub fn set_max_parts(&mut self, max_parts: u16) {
79        self.query.insert("max-parts", max_parts.to_string());
80    }
81
82    pub fn set_part_number_marker(&mut self, part_number_marker: u16) {
83        self.query
84            .insert("part-number-marker", part_number_marker.to_string());
85    }
86
87    pub fn parse_response(s: &str) -> Result<ListPartsResponse, quick_xml::DeError> {
88        let mut parts: ListPartsResponse = quick_xml::de::from_str(s)?;
89        if !parts.is_truncated {
90            parts.next_part_number_marker = None;
91        }
92        Ok(parts)
93    }
94}
95
96impl<'a> OSSAction<'a> for ListParts<'a> {
97    const METHOD: Method = Method::Get;
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 =
110            SortingIterator::new(iter::once(("uploadId", self.upload_id)), self.query.iter());
111
112        match self.credentials {
113            Some(credentials) => sign(
114                time,
115                Method::Get,
116                url,
117                credentials.key(),
118                credentials.secret(),
119                credentials.token(),
120                self.bucket.region(),
121                expires_in.as_secs(),
122                query,
123                self.headers.iter(),
124            ),
125            None => crate::signing::util::add_query_params(url, query),
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use pretty_assertions::assert_eq;
133    use time::OffsetDateTime;
134
135    use crate::{Bucket, Credentials, UrlStyle};
136
137    use super::*;
138
139    #[test]
140    fn oss_example() {
141        // Fri, 24 May 2013 00:00:00 GMT
142        let date = OffsetDateTime::from_unix_timestamp(1369353600).unwrap();
143        let expires_in = Duration::from_secs(86400);
144
145        let endpoint = "https://oss-cn-hangzhou.aliyuncs.com".parse().unwrap();
146        let bucket = Bucket::new(
147            endpoint,
148            UrlStyle::VirtualHost,
149            "examplebucket",
150            "cn-hangzhou",
151        )
152        .unwrap();
153        let credentials = Credentials::new(
154            "access_key_id",
155            "access_key_secret",
156        );
157
158        let mut action = ListParts::new(&bucket, Some(&credentials), "test.txt", "abcd");
159        action.set_max_parts(100);
160        let url = action.sign_with_time(expires_in, &date);
161        let expected = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/test.txt?max-parts=100&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=9481ae57574a62c0d21086c6df38d225a38f92666f088fcba53d22e7925d925b";
162        assert_eq!(expected, url.as_str());
163
164        let mut action = ListParts::new(&bucket, Some(&credentials), "test.txt", "abcd");
165        action.set_max_parts(50);
166        action.set_part_number_marker(100);
167        let url = action.sign_with_time(expires_in, &date);
168        let expected = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/test.txt?max-parts=50&part-number-marker=100&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=6d88224efc86a63436a4d484dd3cbd118f8851afbd3a9468b819506c58ffc86f";
169        assert_eq!(expected, url.as_str());
170    }
171
172    #[test]
173    fn anonymous_custom_query() {
174        let expires_in = Duration::from_secs(86400);
175
176        let endpoint = "https://oss-cn-hangzhou.aliyuncs.com".parse().unwrap();
177        let bucket = Bucket::new(
178            endpoint,
179            UrlStyle::VirtualHost,
180            "examplebucket",
181            "cn-hangzhou",
182        )
183        .unwrap();
184
185        let mut action = ListParts::new(&bucket, None, "test.txt", "abcd");
186        action.set_max_parts(100);
187        let url = action.sign(expires_in);
188        let expected =
189            "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/test.txt?max-parts=100&uploadId=abcd";
190        assert_eq!(expected, url.as_str());
191
192        let mut action = ListParts::new(&bucket, None, "test.txt", "abcd");
193        action.set_max_parts(50);
194        action.set_part_number_marker(100);
195        let url = action.sign(expires_in);
196        let expected =
197            "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/test.txt?max-parts=50&part-number-marker=100&uploadId=abcd";
198        assert_eq!(expected, url.as_str());
199    }
200
201    #[test]
202    fn parse() {
203        let input = r#"
204        <?xml version="1.0" encoding="UTF-8"?>
205        <ListPartsResult xmlns="http://doc.oss-cn-hangzhou.aliyuncs.com">
206          <Bucket>example-bucket</Bucket>
207          <Key>example-object</Key>
208          <UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>
209          <Initiator>
210              <ID>arn:aliyun:ram::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>
211              <DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>
212          </Initiator>
213          <Owner>
214            <ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>
215            <DisplayName>someName</DisplayName>
216          </Owner>
217          <StorageClass>STANDARD</StorageClass>
218          <PartNumberMarker>1</PartNumberMarker>
219          <NextPartNumberMarker>3</NextPartNumberMarker>
220          <MaxParts>2</MaxParts>
221          <IsTruncated>true</IsTruncated>
222          <Part>
223            <PartNumber>2</PartNumber>
224            <LastModified>2010-11-10T20:48:34.000Z</LastModified>
225            <ETag>"7778aef83f66abc1fa1e8477f296d394"</ETag>
226            <Size>10485760</Size>
227          </Part>
228          <Part>
229            <PartNumber>3</PartNumber>
230            <LastModified>2010-11-10T20:48:33.000Z</LastModified>
231            <ETag>"aaaa18db4cc2f85cedef654fccc4a4x8"</ETag>
232            <Size>10485760</Size>
233          </Part>
234        </ListPartsResult>
235        "#;
236
237        let parsed = ListParts::parse_response(input).unwrap();
238        assert_eq!(parsed.parts.len(), 2);
239
240        let part_1 = &parsed.parts[0];
241        assert_eq!(part_1.etag, "\"7778aef83f66abc1fa1e8477f296d394\"");
242        assert_eq!(part_1.number, 2);
243        assert_eq!(part_1.last_modified, "2010-11-10T20:48:34.000Z");
244        assert_eq!(part_1.size, 10485760);
245
246        let part_2 = &parsed.parts[1];
247        assert_eq!(part_2.etag, "\"aaaa18db4cc2f85cedef654fccc4a4x8\"");
248        assert_eq!(part_2.number, 3);
249        assert_eq!(part_2.last_modified, "2010-11-10T20:48:33.000Z");
250        assert_eq!(part_2.size, 10485760);
251
252        assert_eq!(parsed.max_parts, 2);
253        assert_eq!(parsed.next_part_number_marker, Some(3));
254    }
255
256    #[test]
257    fn parse_no_parts() {
258        let input = r#"
259        <?xml version="1.0" encoding="UTF-8"?>
260        <ListPartsResult xmlns="http://doc.oss-cn-hangzhou.aliyuncs.com">
261          <Bucket>example-bucket</Bucket>
262          <Key>example-object</Key>
263          <UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>
264          <Initiator>
265              <ID>arn:aliyun:ram::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>
266              <DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>
267          </Initiator>
268          <Owner>
269            <ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>
270            <DisplayName>someName</DisplayName>
271          </Owner>
272          <StorageClass>STANDARD</StorageClass>
273          <PartNumberMarker>1</PartNumberMarker>
274          <MaxParts>2</MaxParts>
275          <IsTruncated>false</IsTruncated>
276        </ListPartsResult>
277        "#;
278
279        let parsed = ListParts::parse_response(input).unwrap();
280        assert!(parsed.parts.is_empty());
281        assert_eq!(parsed.max_parts, 2);
282        assert!(parsed.next_part_number_marker.is_none());
283    }
284}