1use std::borrow::Cow;
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::{Bucket, Credentials, Map};
12
13#[derive(Debug, Clone)]
24pub struct ListObjectsV2<'a> {
25 bucket: &'a Bucket,
26 credentials: Option<&'a Credentials>,
27
28 query: Map<'a>,
29 headers: Map<'a>,
30}
31
32#[derive(Debug, Clone, Deserialize)]
33pub struct ListObjectsV2Response {
34 #[serde(rename = "Contents")]
37 #[serde(default)]
38 pub contents: Vec<ListObjectsContent>,
39
40 #[serde(rename = "MaxKeys")]
47 pub max_keys: Option<u16>,
48 #[serde(rename = "CommonPrefixes", default)]
49 pub common_prefixes: Vec<CommonPrefixes>,
50 #[serde(rename = "NextContinuationToken")]
57 pub next_continuation_token: Option<String>,
58 #[serde(rename = "StartAfter")]
59 pub start_after: Option<String>,
60}
61
62#[derive(Debug, Clone, Deserialize)]
63pub struct ListObjectsContent {
64 #[serde(rename = "ETag")]
65 pub etag: String,
66 #[serde(rename = "Key")]
67 pub key: String,
68 #[serde(rename = "LastModified")]
69 pub last_modified: String,
70 #[serde(rename = "Owner")]
71 pub owner: Option<ListObjectsOwner>,
72 #[serde(rename = "Size")]
73 pub size: u64,
74 #[serde(rename = "StorageClass")]
75 pub storage_class: Option<String>,
76}
77
78#[derive(Debug, Clone, Deserialize)]
79pub struct ListObjectsOwner {
80 #[serde(rename = "ID")]
81 pub id: String,
82 #[serde(rename = "DisplayName")]
83 pub display_name: String,
84}
85
86#[derive(Debug, Clone, Deserialize)]
87pub struct CommonPrefixes {
88 #[serde(rename = "Prefix")]
89 pub prefix: String,
90}
91
92impl<'a> ListObjectsV2<'a> {
93 pub fn new(bucket: &'a Bucket, credentials: Option<&'a Credentials>) -> Self {
94 let mut query = Map::new();
95 query.insert("encoding-type", "url");
96 query.insert("list-type", "2");
97
98 Self {
99 bucket,
100 credentials,
101
102 query,
103 headers: Map::new(),
104 }
105 }
106
107 pub fn with_prefix(&mut self, prefix: impl Into<Cow<'a, str>>) {
111 self.query_mut().insert("prefix", prefix);
112 }
113
114 pub fn with_start_after(&mut self, start_after: impl Into<Cow<'a, str>>) {
120 self.query_mut().insert("start-after", start_after);
121 }
122
123 pub fn with_continuation_token(&mut self, continuation_token: impl Into<Cow<'a, str>>) {
128 self.query_mut()
129 .insert("continuation-token", continuation_token);
130 }
131
132 pub fn with_max_keys(&mut self, max_keys: usize) {
138 self.query_mut().insert("max-keys", max_keys.to_string());
139 }
140
141 pub fn parse_response(s: &str) -> Result<ListObjectsV2Response, quick_xml::DeError> {
142 let mut parsed: ListObjectsV2Response = quick_xml::de::from_str(s)?;
143
144 for content in parsed.contents.iter_mut() {
146 if let Some(owner) = &content.owner {
147 if owner.id.is_empty() && owner.display_name.is_empty() {
148 content.owner = None;
149 }
150 }
151 }
152
153 Ok(parsed)
154 }
155}
156
157impl<'a> OSSAction<'a> for ListObjectsV2<'a> {
158 const METHOD: Method = Method::Get;
159
160 fn query_mut(&mut self) -> &mut Map<'a> {
161 &mut self.query
162 }
163
164 fn headers_mut(&mut self) -> &mut Map<'a> {
165 &mut self.headers
166 }
167
168 fn sign_with_time(&self, expires_in: Duration, time: &OffsetDateTime) -> Url {
169 let url = self.bucket.base_url().clone();
170
171 match self.credentials {
172 Some(credentials) => sign(
173 time,
174 Method::Get,
175 url,
176 credentials.key(),
177 credentials.secret(),
178 credentials.token(),
179 self.bucket.region(),
180 expires_in.as_secs(),
181 self.query.iter(),
182 self.headers.iter(),
183 ),
184 None => crate::signing::util::add_query_params(url, self.query.iter()),
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use time::OffsetDateTime;
192
193 use pretty_assertions::assert_eq;
194
195 use super::*;
196 use crate::{Bucket, Credentials, UrlStyle};
197
198 #[test]
199 fn oss_example() {
200 let date = OffsetDateTime::from_unix_timestamp(1369353600).unwrap();
202 let expires_in = Duration::from_secs(86400);
203
204 let endpoint = "https://oss-cn-hangzhou.aliyuncs.com".parse().unwrap();
205 let bucket = Bucket::new(
206 endpoint,
207 UrlStyle::VirtualHost,
208 "examplebucket",
209 "cn-hangzhou",
210 )
211 .unwrap();
212 let credentials = Credentials::new(
213 "access_key_id",
214 "access_key_secret",
215 );
216
217 let action = ListObjectsV2::new(&bucket, Some(&credentials));
218
219 let url = action.sign_with_time(expires_in, &date);
220 let expected = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/?encoding-type=url&list-type=2&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=b3f1c590a5375a45e9ad593f2b55adc660686d92c5ad8807fe54e40b7cc3fff5";
221
222 assert_eq!(expected, url.as_str());
223 }
224
225 #[test]
226 fn anonymous_custom_query() {
227 let expires_in = Duration::from_secs(86400);
228
229 let endpoint = "https://oss-cn-hangzhou.aliyuncs.com".parse().unwrap();
230 let bucket = Bucket::new(
231 endpoint,
232 UrlStyle::VirtualHost,
233 "examplebucket",
234 "cn-hangzhou",
235 )
236 .unwrap();
237
238 let mut action = ListObjectsV2::new(&bucket, None);
239 action.query_mut().insert("continuation-token", "duck");
240
241 let url = action.sign(expires_in);
242 let expected = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com/?continuation-token=duck&encoding-type=url&list-type=2";
243
244 assert_eq!(expected, url.as_str());
245 }
246
247 #[test]
248 fn parse() {
249 let input = r#"
250 <?xml version="1.0" encoding="UTF-8"?>
251 <ListBucketResult xmlns="http://doc.oss-cn-hangzhou.aliyuncs.com">
252 <Name>test</Name>
253 <Prefix></Prefix>
254 <KeyCount>3</KeyCount>
255 <MaxKeys>4500</MaxKeys>
256 <Delimiter></Delimiter>
257 <IsTruncated>false</IsTruncated>
258 <Contents>
259 <Key>duck.jpg</Key>
260 <LastModified>2020-12-01T20:43:11.794Z</LastModified>
261 <ETag>"bfd537a51d15208163231b0711e0b1f3"</ETag>
262 <Size>4274</Size>
263 <Owner>
264 <ID></ID>
265 <DisplayName></DisplayName>
266 </Owner>
267 <StorageClass>STANDARD</StorageClass>
268 </Contents>
269 <Contents>
270 <Key>idk.txt</Key>
271 <LastModified>2020-12-05T08:23:52.215Z</LastModified>
272 <ETag>"5927c5d64d94a5786f90003aa26d0159-1"</ETag>
273 <Size>9</Size>
274 <Owner>
275 <ID></ID>
276 <DisplayName></DisplayName>
277 </Owner>
278 <StorageClass>STANDARD</StorageClass>
279 </Contents>
280 <Contents>
281 <Key>img.jpg</Key>
282 <LastModified>2020-11-26T20:21:35.858Z</LastModified>
283 <ETag>"f7dbec93a0932ccb4d0f4e512eb1a443"</ETag>
284 <Size>41259</Size>
285 <Owner>
286 <ID></ID>
287 <DisplayName></DisplayName>
288 </Owner>
289 <StorageClass>STANDARD</StorageClass>
290 </Contents>
291 <EncodingType>url</EncodingType>
292 </ListBucketResult>
293 "#;
294
295 let parsed = ListObjectsV2::parse_response(input).unwrap();
296 assert_eq!(parsed.contents.len(), 3);
297
298 let item_1 = &parsed.contents[0];
299 assert_eq!(item_1.etag, "\"bfd537a51d15208163231b0711e0b1f3\"");
300 assert_eq!(item_1.key, "duck.jpg");
301 assert_eq!(item_1.last_modified, "2020-12-01T20:43:11.794Z");
302 assert!(item_1.owner.is_none());
303 assert_eq!(item_1.size, 4274);
304 assert_eq!(item_1.storage_class, Some("STANDARD".to_string()));
305
306 let item_2 = &parsed.contents[1];
307 assert_eq!(item_2.etag, "\"5927c5d64d94a5786f90003aa26d0159-1\"");
308 assert_eq!(item_2.key, "idk.txt");
309 assert_eq!(item_2.last_modified, "2020-12-05T08:23:52.215Z");
310 assert!(item_2.owner.is_none());
311 assert_eq!(item_2.size, 9);
312 assert_eq!(item_2.storage_class, Some("STANDARD".to_string()));
313
314 let item_3 = &parsed.contents[2];
315 assert_eq!(item_3.etag, "\"f7dbec93a0932ccb4d0f4e512eb1a443\"");
316 assert_eq!(item_3.key, "img.jpg");
317 assert_eq!(item_3.last_modified, "2020-11-26T20:21:35.858Z");
318 assert!(item_3.owner.is_none());
319 assert_eq!(item_3.size, 41259);
320 assert_eq!(item_3.storage_class, Some("STANDARD".to_string()));
321
322 assert_eq!(parsed.max_keys, Some(4500));
323 assert!(parsed.common_prefixes.is_empty());
324 assert!(parsed.next_continuation_token.is_none());
325 assert!(parsed.start_after.is_none());
326 }
327
328 #[test]
329 fn parse_no_contents() {
330 let input = r#"
331 <?xml version="1.0" encoding="UTF-8"?>
332 <ListBucketResult xmlns="http://doc.oss-cn-hangzhou.aliyuncs.com">
333 <Name>test</Name>
334 <Prefix></Prefix>
335 <KeyCount>0</KeyCount>
336 <MaxKeys>4500</MaxKeys>
337 <Delimiter></Delimiter>
338 <IsTruncated>false</IsTruncated>
339 <EncodingType>url</EncodingType>
340 </ListBucketResult>
341 "#;
342
343 let parsed = ListObjectsV2::parse_response(input).unwrap();
344 assert_eq!(parsed.contents.is_empty(), true);
345
346 assert_eq!(parsed.max_keys, Some(4500));
347 assert!(parsed.common_prefixes.is_empty());
348 assert!(parsed.next_continuation_token.is_none());
349 assert!(parsed.start_after.is_none());
350 }
351}