Skip to main content

aliyun_oss/types/
response.rs

1//! XML response deserialization types for OSS API responses.
2
3use serde::Deserialize;
4
5use super::bucket::BucketName;
6use super::object::{ETag, ObjectKey};
7
8/// Response for ListObjects (V1) API.
9#[derive(Debug, Clone, Deserialize, PartialEq)]
10#[serde(rename = "ListBucketResult")]
11pub struct ListObjectsOutput {
12    #[serde(rename = "Name")]
13    pub name: String,
14
15    #[serde(rename = "Prefix", default)]
16    pub prefix: String,
17
18    #[serde(rename = "Marker", default)]
19    pub marker: String,
20
21    #[serde(rename = "MaxKeys")]
22    pub max_keys: i32,
23
24    #[serde(rename = "IsTruncated")]
25    pub is_truncated: bool,
26
27    #[serde(rename = "NextMarker", default)]
28    pub next_marker: String,
29
30    #[serde(rename = "Delimiter", default)]
31    pub delimiter: String,
32
33    #[serde(rename = "EncodingType", default)]
34    pub encoding_type: String,
35
36    #[serde(rename = "CommonPrefixes", default)]
37    pub common_prefixes: Vec<CommonPrefix>,
38
39    #[serde(rename = "Contents", default)]
40    pub objects: Vec<ObjectSummary>,
41}
42
43impl ListObjectsOutput {
44    /// Returns the bucket name parsed from the response.
45    pub fn bucket_name(&self) -> Option<BucketName> {
46        BucketName::new(&self.name).ok()
47    }
48
49    /// Returns the parsed object keys from the response.
50    pub fn object_keys(&self) -> Vec<ObjectKey> {
51        self.objects
52            .iter()
53            .filter_map(|o| ObjectKey::new(&o.key).ok())
54            .collect()
55    }
56}
57
58/// A common prefix (directory-like entry) returned in list results.
59#[derive(Debug, Clone, Deserialize, PartialEq)]
60pub struct CommonPrefix {
61    #[serde(rename = "Prefix")]
62    pub prefix: String,
63}
64
65/// Summary information for a single object in a list results response.
66#[derive(Debug, Clone, Deserialize, PartialEq)]
67pub struct ObjectSummary {
68    #[serde(rename = "Key")]
69    pub key: String,
70
71    #[serde(rename = "LastModified")]
72    pub last_modified: String,
73
74    #[serde(rename = "ETag")]
75    pub etag: String,
76
77    #[serde(rename = "Size")]
78    pub size: u64,
79
80    #[serde(rename = "StorageClass")]
81    pub storage_class: String,
82
83    #[serde(rename = "Owner", default)]
84    pub owner: Option<OwnerInfo>,
85}
86
87impl ObjectSummary {
88    /// Returns the parsed ETag from the response.
89    pub fn etag_parsed(&self) -> Option<ETag> {
90        ETag::from_header(&self.etag)
91    }
92
93    /// Returns the parsed object key.
94    pub fn object_key(&self) -> Option<ObjectKey> {
95        ObjectKey::new(&self.key).ok()
96    }
97}
98
99/// Owner information from OSS API responses.
100#[derive(Debug, Clone, Deserialize, PartialEq)]
101pub struct OwnerInfo {
102    #[serde(rename = "ID")]
103    pub id: String,
104
105    #[serde(rename = "DisplayName", default)]
106    pub display_name: String,
107}
108
109/// Response for ListObjectsV2 API.
110#[derive(Debug, Clone, Deserialize, PartialEq)]
111#[serde(rename = "ListBucketResult")]
112pub struct ListObjectsV2Output {
113    #[serde(rename = "Name")]
114    pub name: String,
115
116    #[serde(rename = "Prefix", default)]
117    pub prefix: String,
118
119    #[serde(rename = "StartAfter", default)]
120    pub start_after: String,
121
122    #[serde(rename = "MaxKeys")]
123    pub max_keys: i32,
124
125    #[serde(rename = "IsTruncated")]
126    pub is_truncated: bool,
127
128    #[serde(rename = "NextContinuationToken", default)]
129    pub next_continuation_token: String,
130
131    #[serde(rename = "ContinuationToken", default)]
132    pub continuation_token: String,
133
134    #[serde(rename = "KeyCount", default)]
135    pub key_count: i32,
136
137    #[serde(rename = "Delimiter", default)]
138    pub delimiter: String,
139
140    #[serde(rename = "EncodingType", default)]
141    pub encoding_type: String,
142
143    #[serde(rename = "CommonPrefixes", default)]
144    pub common_prefixes: Vec<CommonPrefix>,
145
146    #[serde(rename = "Contents", default)]
147    pub objects: Vec<ObjectSummary>,
148}
149
150/// Response for the ListBuckets (GetService) API.
151#[derive(Debug, Clone, Deserialize, PartialEq)]
152#[serde(rename = "ListAllMyBucketsResult")]
153pub struct ListBucketsOutput {
154    #[serde(rename = "Owner")]
155    pub owner: OwnerInfo,
156
157    #[serde(rename = "Buckets", default)]
158    pub buckets: BucketsContainer,
159}
160
161/// Container for bucket summaries in a ListBuckets response.
162#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
163pub struct BucketsContainer {
164    #[serde(rename = "Bucket", default)]
165    pub bucket: Vec<BucketSummary>,
166}
167
168/// Summary of a single bucket in the ListBuckets response.
169#[derive(Debug, Clone, Deserialize, PartialEq)]
170pub struct BucketSummary {
171    #[serde(rename = "Name")]
172    pub name: String,
173
174    #[serde(rename = "CreationDate")]
175    pub creation_date: String,
176
177    #[serde(rename = "Location")]
178    pub location: String,
179
180    #[serde(rename = "ExtranetEndpoint")]
181    pub extranet_endpoint: String,
182
183    #[serde(rename = "IntranetEndpoint")]
184    pub intranet_endpoint: String,
185
186    #[serde(rename = "StorageClass")]
187    pub storage_class: String,
188
189    #[serde(rename = "Region", default)]
190    pub region: String,
191}
192
193/// Response for the GetBucketInfo API.
194#[derive(Debug, Clone, Deserialize, PartialEq)]
195#[serde(rename = "BucketInfo")]
196pub struct GetBucketInfoOutput {
197    #[serde(rename = "Bucket")]
198    pub bucket: BucketInfoDetail,
199}
200
201/// Detailed bucket information from GetBucketInfo.
202#[derive(Debug, Clone, Deserialize, PartialEq)]
203pub struct BucketInfoDetail {
204    #[serde(rename = "Name")]
205    pub name: String,
206
207    #[serde(rename = "CreationDate")]
208    pub creation_date: String,
209
210    #[serde(rename = "Location")]
211    pub location: String,
212
213    #[serde(rename = "ExtranetEndpoint")]
214    pub extranet_endpoint: String,
215
216    #[serde(rename = "IntranetEndpoint")]
217    pub intranet_endpoint: String,
218
219    #[serde(rename = "StorageClass")]
220    pub storage_class: String,
221
222    #[serde(rename = "Owner")]
223    pub owner: OwnerInfo,
224
225    #[serde(rename = "AccessControlList", default)]
226    pub acl: Option<AccessControlList>,
227
228    #[serde(rename = "DataRedundancyType", default)]
229    pub data_redundancy_type: String,
230
231    #[serde(rename = "Versioning", default)]
232    pub versioning: String,
233}
234
235/// ACL policy detail from bucket/object info responses.
236#[derive(Debug, Clone, Deserialize, PartialEq)]
237pub struct AccessControlList {
238    #[serde(rename = "Grant")]
239    pub grant: String,
240}
241
242/// Response for the GetBucketStat API.
243#[derive(Debug, Clone, Deserialize, PartialEq)]
244#[serde(rename = "BucketStat")]
245pub struct GetBucketStatOutput {
246    #[serde(rename = "Storage")]
247    pub storage: u64,
248
249    #[serde(rename = "ObjectCount")]
250    pub object_count: u64,
251
252    #[serde(rename = "MultipartUploadCount")]
253    pub multipart_upload_count: u64,
254
255    #[serde(rename = "LiveChannelCount")]
256    pub live_channel_count: u64,
257
258    #[serde(rename = "LastModifiedTime")]
259    pub last_modified_time: u64,
260
261    #[serde(rename = "StandardStorage")]
262    pub standard_storage: u64,
263
264    #[serde(rename = "StandardObjectCount")]
265    pub standard_object_count: u64,
266
267    #[serde(rename = "InfrequentAccessStorage")]
268    pub ia_storage: u64,
269
270    #[serde(rename = "InfrequentAccessObjectCount")]
271    pub ia_object_count: u64,
272
273    #[serde(rename = "ArchiveStorage")]
274    pub archive_storage: u64,
275
276    #[serde(rename = "ArchiveObjectCount")]
277    pub archive_object_count: u64,
278
279    #[serde(rename = "ColdArchiveStorage")]
280    pub cold_archive_storage: u64,
281
282    #[serde(rename = "ColdArchiveObjectCount")]
283    pub cold_archive_object_count: u64,
284
285    #[serde(rename = "DeepColdArchiveStorage")]
286    pub deep_cold_archive_storage: u64,
287
288    #[serde(rename = "DeepColdArchiveObjectCount")]
289    pub deep_cold_archive_object_count: u64,
290}
291
292/// Response for the GetObjectAcl API.
293#[derive(Debug, Clone, Deserialize, PartialEq)]
294#[serde(rename = "AccessControlPolicy")]
295pub struct GetObjectAclOutput {
296    #[serde(rename = "Owner")]
297    pub owner: OwnerInfo,
298    #[serde(rename = "AccessControlList")]
299    pub acl: AccessControlPolicyDetail,
300}
301
302/// ACL grant detail from access policy responses.
303#[derive(Debug, Clone, Deserialize, PartialEq)]
304pub struct AccessControlPolicyDetail {
305    #[serde(rename = "Grant")]
306    pub grant: String,
307}
308
309/// Response for the DeleteMultipleObjects API.
310#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
311#[serde(rename = "DeleteResult")]
312pub struct DeleteResult {
313    #[serde(rename = "Deleted", default)]
314    pub deleted: Vec<DeletedObject>,
315}
316
317/// An entry for a successfully deleted object in a DeleteMultipleObjects response.
318#[derive(Debug, Clone, Deserialize, PartialEq)]
319pub struct DeletedObject {
320    #[serde(rename = "Key")]
321    pub key: String,
322    #[serde(rename = "DeleteMarker", default)]
323    pub delete_marker: bool,
324    #[serde(rename = "DeleteMarkerVersionId", default)]
325    pub delete_marker_version_id: String,
326    #[serde(rename = "VersionId", default)]
327    pub version_id: String,
328}
329
330/// Response for the ListObjectVersions API.
331#[derive(Debug, Clone, Deserialize, PartialEq)]
332#[serde(rename = "ListVersionsResult")]
333pub struct ListVersionsOutput {
334    #[serde(rename = "Name")]
335    pub name: String,
336    #[serde(rename = "Prefix", default)]
337    pub prefix: String,
338    #[serde(rename = "KeyMarker", default)]
339    pub key_marker: String,
340    #[serde(rename = "VersionIdMarker", default)]
341    pub version_id_marker: String,
342    #[serde(rename = "MaxKeys")]
343    pub max_keys: i32,
344    #[serde(rename = "IsTruncated")]
345    pub is_truncated: bool,
346    #[serde(rename = "NextKeyMarker", default)]
347    pub next_key_marker: String,
348    #[serde(rename = "NextVersionIdMarker", default)]
349    pub next_version_id_marker: String,
350    #[serde(rename = "Version", default)]
351    pub versions: Vec<ObjectVersion>,
352    #[serde(rename = "DeleteMarker", default)]
353    pub delete_markers: Vec<DeleteMarkerSummary>,
354    #[serde(rename = "CommonPrefixes", default)]
355    pub common_prefixes: Vec<CommonPrefix>,
356}
357
358/// An object version entry in a ListObjectVersions response.
359#[derive(Debug, Clone, Deserialize, PartialEq)]
360pub struct ObjectVersion {
361    #[serde(rename = "Key")]
362    pub key: String,
363    #[serde(rename = "VersionId")]
364    pub version_id: String,
365    #[serde(rename = "IsLatest")]
366    pub is_latest: bool,
367    #[serde(rename = "LastModified")]
368    pub last_modified: String,
369    #[serde(rename = "ETag")]
370    pub etag: String,
371    #[serde(rename = "Size")]
372    pub size: u64,
373    #[serde(rename = "StorageClass")]
374    pub storage_class: String,
375    #[serde(rename = "Owner", default)]
376    pub owner: Option<OwnerInfo>,
377}
378
379/// A delete marker entry in a ListObjectVersions response.
380#[derive(Debug, Clone, Deserialize, PartialEq)]
381pub struct DeleteMarkerSummary {
382    #[serde(rename = "Key")]
383    pub key: String,
384    #[serde(rename = "VersionId")]
385    pub version_id: String,
386    #[serde(rename = "IsLatest")]
387    pub is_latest: bool,
388    #[serde(rename = "LastModified")]
389    pub last_modified: String,
390    #[serde(rename = "Owner", default)]
391    pub owner: Option<OwnerInfo>,
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397    use crate::util::xml::from_xml;
398
399    #[test]
400    fn list_objects_output_parsed_from_xml() {
401        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
402        <ListBucketResult>
403          <Name>examplebucket</Name>
404          <Prefix>test</Prefix>
405          <Marker></Marker>
406          <MaxKeys>100</MaxKeys>
407          <IsTruncated>false</IsTruncated>
408          <Contents>
409            <Key>test/file1.txt</Key>
410            <LastModified>2024-01-01T00:00:00.000Z</LastModified>
411            <ETag>"abc123"</ETag>
412            <Size>1024</Size>
413            <StorageClass>Standard</StorageClass>
414            <Owner>
415              <ID>owner-id</ID>
416              <DisplayName>owner</DisplayName>
417            </Owner>
418          </Contents>
419        </ListBucketResult>"#;
420
421        let result: ListObjectsOutput = from_xml(xml).unwrap();
422        assert_eq!(result.name, "examplebucket");
423        assert_eq!(result.objects.len(), 1);
424        assert_eq!(result.objects[0].key, "test/file1.txt");
425        assert_eq!(result.objects[0].size, 1024);
426        assert_eq!(result.objects[0].etag_parsed().unwrap().as_str(), "abc123");
427    }
428
429    #[test]
430    fn list_objects_truncated_with_next_marker() {
431        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
432        <ListBucketResult>
433          <Name>bucket</Name>
434          <MaxKeys>2</MaxKeys>
435          <IsTruncated>true</IsTruncated>
436          <NextMarker>next-token</NextMarker>
437          <Contents>
438            <Key>obj1</Key>
439            <LastModified>2024-01-01T00:00:00.000Z</LastModified>
440            <ETag>"etag1"</ETag>
441            <Size>100</Size>
442            <StorageClass>Standard</StorageClass>
443          </Contents>
444        </ListBucketResult>"#;
445
446        let result: ListObjectsOutput = from_xml(xml).unwrap();
447        assert!(result.is_truncated);
448        assert_eq!(result.next_marker, "next-token");
449    }
450
451    #[test]
452    fn common_prefixes_parsed_correctly() {
453        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
454        <ListBucketResult>
455          <Name>bucket</Name>
456          <MaxKeys>100</MaxKeys>
457          <IsTruncated>false</IsTruncated>
458          <Prefix>dir/</Prefix>
459          <Delimiter>/</Delimiter>
460          <CommonPrefixes>
461            <Prefix>dir/subdir1/</Prefix>
462          </CommonPrefixes>
463          <CommonPrefixes>
464            <Prefix>dir/subdir2/</Prefix>
465          </CommonPrefixes>
466        </ListBucketResult>"#;
467
468        let result: ListObjectsOutput = from_xml(xml).unwrap();
469        assert_eq!(result.common_prefixes.len(), 2);
470        assert_eq!(result.common_prefixes[0].prefix, "dir/subdir1/");
471        assert_eq!(result.common_prefixes[1].prefix, "dir/subdir2/");
472    }
473
474    #[test]
475    fn list_buckets_output_parsed_from_xml() {
476        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
477        <ListAllMyBucketsResult>
478          <Owner>
479            <ID>owner-id</ID>
480            <DisplayName>owner-name</DisplayName>
481          </Owner>
482          <Buckets>
483            <Bucket>
484              <Name>bucket1</Name>
485              <CreationDate>2024-01-01T00:00:00.000Z</CreationDate>
486              <Location>oss-cn-hangzhou</Location>
487              <ExtranetEndpoint>oss-cn-hangzhou.aliyuncs.com</ExtranetEndpoint>
488              <IntranetEndpoint>oss-cn-hangzhou-internal.aliyuncs.com</IntranetEndpoint>
489              <StorageClass>Standard</StorageClass>
490            </Bucket>
491          </Buckets>
492        </ListAllMyBucketsResult>"#;
493
494        let result: ListBucketsOutput = from_xml(xml).unwrap();
495        assert_eq!(result.owner.id, "owner-id");
496        assert_eq!(result.buckets.bucket.len(), 1);
497        assert_eq!(result.buckets.bucket[0].name, "bucket1");
498    }
499
500    #[test]
501    fn list_objects_v2_output_parsed_from_xml() {
502        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
503        <ListBucketResult>
504          <Name>bucket</Name>
505          <MaxKeys>100</MaxKeys>
506          <IsTruncated>false</IsTruncated>
507          <KeyCount>2</KeyCount>
508          <ContinuationToken>token1</ContinuationToken>
509          <Contents>
510            <Key>obj1</Key>
511            <LastModified>2024-01-01T00:00:00.000Z</LastModified>
512            <ETag>"etag1"</ETag>
513            <Size>100</Size>
514            <StorageClass>Standard</StorageClass>
515          </Contents>
516        </ListBucketResult>"#;
517
518        let result: ListObjectsV2Output = from_xml(xml).unwrap();
519        assert_eq!(result.key_count, 2);
520        assert_eq!(result.continuation_token, "token1");
521    }
522
523    #[test]
524    fn get_bucket_info_output_parsed_from_xml() {
525        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
526        <BucketInfo>
527          <Bucket>
528            <Name>my-bucket</Name>
529            <CreationDate>2024-01-01T00:00:00.000Z</CreationDate>
530            <Location>oss-cn-hangzhou</Location>
531            <StorageClass>Standard</StorageClass>
532            <ExtranetEndpoint>oss-cn-hangzhou.aliyuncs.com</ExtranetEndpoint>
533            <IntranetEndpoint>oss-cn-hangzhou-internal.aliyuncs.com</IntranetEndpoint>
534            <Owner>
535              <ID>owner-id</ID>
536            </Owner>
537            <AccessControlList>
538              <Grant>private</Grant>
539            </AccessControlList>
540          </Bucket>
541        </BucketInfo>"#;
542
543        let result: GetBucketInfoOutput = from_xml(xml).unwrap();
544        assert_eq!(result.bucket.name, "my-bucket");
545        assert!(result.bucket.acl.is_some());
546    }
547
548    #[test]
549    fn get_bucket_stat_output_parsed_from_xml() {
550        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
551        <BucketStat>
552          <Storage>1024</Storage>
553          <ObjectCount>10</ObjectCount>
554          <MultipartUploadCount>2</MultipartUploadCount>
555          <LiveChannelCount>0</LiveChannelCount>
556          <LastModifiedTime>1700000000</LastModifiedTime>
557          <StandardStorage>512</StandardStorage>
558          <StandardObjectCount>5</StandardObjectCount>
559          <InfrequentAccessStorage>256</InfrequentAccessStorage>
560          <InfrequentAccessObjectCount>3</InfrequentAccessObjectCount>
561          <ArchiveStorage>256</ArchiveStorage>
562          <ArchiveObjectCount>2</ArchiveObjectCount>
563          <ColdArchiveStorage>0</ColdArchiveStorage>
564          <ColdArchiveObjectCount>0</ColdArchiveObjectCount>
565          <DeepColdArchiveStorage>0</DeepColdArchiveStorage>
566          <DeepColdArchiveObjectCount>0</DeepColdArchiveObjectCount>
567        </BucketStat>"#;
568
569        let result: GetBucketStatOutput = from_xml(xml).unwrap();
570        assert_eq!(result.storage, 1024);
571        assert_eq!(result.object_count, 10);
572        assert_eq!(result.standard_storage, 512);
573    }
574}