rusty_s3/actions/multipart_upload/
list_parts.rs1use std::iter;
2use std::time::Duration;
3
4use instant_xml::FromXml;
5use jiff::Timestamp;
6use url::Url;
7
8use crate::actions::{Method, S3_XML_NS, S3Action};
9use crate::signing::sign;
10use crate::sorting_iter::SortingIterator;
11use crate::{Bucket, Credentials, Map};
12
13#[derive(Debug, Clone)]
24pub struct ListParts<'a> {
25 bucket: &'a Bucket,
26 credentials: Option<&'a Credentials>,
27 object: &'a str,
28 upload_id: &'a str,
29
30 query: Map<'a>,
31 headers: Map<'a>,
32}
33
34#[allow(clippy::module_name_repetitions)]
35#[derive(Debug, Clone, FromXml)]
36#[xml(rename = "ListPartsResult", ns(S3_XML_NS))]
37pub struct ListPartsResponse {
38 pub parts: Vec<PartsContent>,
39 #[xml(rename = "MaxParts")]
40 pub max_parts: u16,
41 #[xml(rename = "IsTruncated")]
42 is_truncated: bool,
43 #[xml(rename = "NextPartNumberMarker")]
44 pub next_part_number_marker: Option<u16>,
45}
46
47#[derive(Debug, Clone, FromXml)]
48#[xml(rename = "Part", ns(S3_XML_NS))]
49pub struct PartsContent {
50 #[xml(rename = "PartNumber")]
51 pub number: u16,
52 #[xml(rename = "ETag")]
53 pub etag: String,
54 #[xml(rename = "LastModified")]
55 pub last_modified: String,
56 #[xml(rename = "Size")]
57 pub size: u64,
58}
59
60impl<'a> ListParts<'a> {
61 #[must_use]
62 pub const fn new(
63 bucket: &'a Bucket,
64 credentials: Option<&'a Credentials>,
65 object: &'a str,
66 upload_id: &'a str,
67 ) -> Self {
68 Self {
69 bucket,
70 credentials,
71 object,
72 upload_id,
73
74 query: Map::new(),
75 headers: Map::new(),
76 }
77 }
78
79 pub fn set_max_parts(&mut self, max_parts: u16) {
80 self.query.insert("max-parts", max_parts.to_string());
81 }
82
83 pub fn set_part_number_marker(&mut self, part_number_marker: u16) {
84 self.query
85 .insert("part-number-marker", part_number_marker.to_string());
86 }
87
88 pub fn parse_response(s: &str) -> Result<ListPartsResponse, instant_xml::Error> {
94 let mut parts: ListPartsResponse = instant_xml::from_str(s)?;
95 if !parts.is_truncated {
96 parts.next_part_number_marker = None;
97 }
98 Ok(parts)
99 }
100}
101
102impl<'a> S3Action<'a> for ListParts<'a> {
103 const METHOD: Method = Method::Get;
104
105 fn query_mut(&mut self) -> &mut Map<'a> {
106 &mut self.query
107 }
108
109 fn headers_mut(&mut self) -> &mut Map<'a> {
110 &mut self.headers
111 }
112
113 fn sign_with_time(&self, expires_in: Duration, time: &Timestamp) -> Url {
114 let url = self.bucket.object_url(self.object).unwrap();
115 let query =
116 SortingIterator::new(iter::once(("uploadId", self.upload_id)), self.query.iter());
117
118 match self.credentials {
119 Some(credentials) => sign(
120 time,
121 Self::METHOD,
122 url,
123 credentials.key(),
124 credentials.secret(),
125 credentials.token(),
126 self.bucket.region(),
127 expires_in.as_secs(),
128 query,
129 self.headers.iter(),
130 ),
131 None => crate::signing::util::add_query_params(url, query),
132 }
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use pretty_assertions::assert_eq;
139
140 use crate::{Bucket, Credentials, UrlStyle};
141
142 use super::*;
143
144 #[test]
145 fn aws_example() {
146 let date = Timestamp::from_second(1369353600).unwrap();
148 let expires_in = Duration::from_secs(86400);
149
150 let endpoint = "https://s3.amazonaws.com".parse().unwrap();
151 let bucket = Bucket::new(
152 endpoint,
153 UrlStyle::VirtualHost,
154 "examplebucket",
155 "us-east-1",
156 )
157 .unwrap();
158 let credentials = Credentials::new(
159 "AKIAIOSFODNN7EXAMPLE",
160 "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
161 );
162
163 let mut action = ListParts::new(&bucket, Some(&credentials), "test.txt", "abcd");
164 action.set_max_parts(100);
165 let url = action.sign_with_time(expires_in, &date);
166 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&max-parts=100&uploadId=abcd&X-Amz-Signature=10a814258808a79054a80e2aff66e95faba686648eb50bd143fe7fe7d6d7b6ce";
167 assert_eq!(expected, url.as_str());
168
169 let mut action = ListParts::new(&bucket, Some(&credentials), "test.txt", "abcd");
170 action.set_max_parts(50);
171 action.set_part_number_marker(100);
172 let url = action.sign_with_time(expires_in, &date);
173 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&max-parts=50&part-number-marker=100&uploadId=abcd&X-Amz-Signature=ea8eecb4f2534d606474497e6088ceb262081bf7c5a289ff0598aafdd66055da";
174 assert_eq!(expected, url.as_str());
175 }
176
177 #[test]
178 fn anonymous_custom_query() {
179 let expires_in = Duration::from_secs(86400);
180
181 let endpoint = "https://s3.amazonaws.com".parse().unwrap();
182 let bucket = Bucket::new(
183 endpoint,
184 UrlStyle::VirtualHost,
185 "examplebucket",
186 "us-east-1",
187 )
188 .unwrap();
189
190 let mut action = ListParts::new(&bucket, None, "test.txt", "abcd");
191 action.set_max_parts(100);
192 let url = action.sign(expires_in);
193 let expected =
194 "https://examplebucket.s3.amazonaws.com/test.txt?max-parts=100&uploadId=abcd";
195 assert_eq!(expected, url.as_str());
196
197 let mut action = ListParts::new(&bucket, None, "test.txt", "abcd");
198 action.set_max_parts(50);
199 action.set_part_number_marker(100);
200 let url = action.sign(expires_in);
201 let expected = "https://examplebucket.s3.amazonaws.com/test.txt?max-parts=50&part-number-marker=100&uploadId=abcd";
202 assert_eq!(expected, url.as_str());
203 }
204
205 #[test]
206 fn parse() {
207 let input = r#"<?xml version="1.0" encoding="UTF-8"?>
208 <ListPartsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
209 <Bucket>example-bucket</Bucket>
210 <Key>example-object</Key>
211 <UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>
212 <Initiator>
213 <ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>
214 <DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>
215 </Initiator>
216 <Owner>
217 <ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>
218 <DisplayName>someName</DisplayName>
219 </Owner>
220 <StorageClass>STANDARD</StorageClass>
221 <PartNumberMarker>1</PartNumberMarker>
222 <NextPartNumberMarker>3</NextPartNumberMarker>
223 <MaxParts>2</MaxParts>
224 <IsTruncated>true</IsTruncated>
225 <Part>
226 <PartNumber>2</PartNumber>
227 <LastModified>2010-11-10T20:48:34.000Z</LastModified>
228 <ETag>"7778aef83f66abc1fa1e8477f296d394"</ETag>
229 <Size>10485760</Size>
230 </Part>
231 <Part>
232 <PartNumber>3</PartNumber>
233 <LastModified>2010-11-10T20:48:33.000Z</LastModified>
234 <ETag>"aaaa18db4cc2f85cedef654fccc4a4x8"</ETag>
235 <Size>10485760</Size>
236 </Part>
237 </ListPartsResult>
238 "#;
239
240 let parsed = ListParts::parse_response(input).unwrap();
241 assert_eq!(parsed.parts.len(), 2);
242
243 let part_1 = &parsed.parts[0];
244 assert_eq!(part_1.etag, "\"7778aef83f66abc1fa1e8477f296d394\"");
245 assert_eq!(part_1.number, 2);
246 assert_eq!(part_1.last_modified, "2010-11-10T20:48:34.000Z");
247 assert_eq!(part_1.size, 10485760);
248
249 let part_2 = &parsed.parts[1];
250 assert_eq!(part_2.etag, "\"aaaa18db4cc2f85cedef654fccc4a4x8\"");
251 assert_eq!(part_2.number, 3);
252 assert_eq!(part_2.last_modified, "2010-11-10T20:48:33.000Z");
253 assert_eq!(part_2.size, 10485760);
254
255 assert_eq!(parsed.max_parts, 2);
256 assert_eq!(parsed.next_part_number_marker, Some(3));
257 }
258
259 #[test]
260 fn parse_no_parts() {
261 let input = r#"<?xml version="1.0" encoding="UTF-8"?>
262 <ListPartsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
263 <Bucket>example-bucket</Bucket>
264 <Key>example-object</Key>
265 <UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>
266 <Initiator>
267 <ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>
268 <DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>
269 </Initiator>
270 <Owner>
271 <ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>
272 <DisplayName>someName</DisplayName>
273 </Owner>
274 <StorageClass>STANDARD</StorageClass>
275 <PartNumberMarker>1</PartNumberMarker>
276 <MaxParts>2</MaxParts>
277 <IsTruncated>false</IsTruncated>
278 </ListPartsResult>
279 "#;
280
281 let parsed = ListParts::parse_response(input).unwrap();
282 assert!(parsed.parts.is_empty());
283 assert_eq!(parsed.max_parts, 2);
284 assert!(parsed.next_part_number_marker.is_none());
285 }
286}