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