Skip to main content

aliyun_oss/operations/
object_tagging.rs

1//! Object tagging operations.
2
3use std::sync::Arc;
4
5use serde::{Deserialize, Serialize};
6
7use crate::client::{BucketOperations, OSSClientInner};
8use crate::error::{ErrorContext, OssError, OssErrorKind, Result};
9use crate::http::client::HttpRequest;
10use crate::types::bucket::BucketName;
11use crate::types::object::ObjectKey;
12use crate::util::uri::oss_endpoint_url;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(rename = "Tagging")]
16struct TaggingConfig {
17    #[serde(rename = "TagSet")]
18    tag_set: TagSetConfig,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22struct TagSetConfig {
23    #[serde(rename = "Tag")]
24    tags: Vec<TagPair>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28struct TagPair {
29    #[serde(rename = "Key")]
30    key: String,
31    #[serde(rename = "Value")]
32    value: String,
33}
34
35pub struct PutObjectTaggingBuilder {
36    client: Arc<OSSClientInner>,
37    bucket: BucketName,
38    key: ObjectKey,
39    tags: Vec<(String, String)>,
40}
41
42impl PutObjectTaggingBuilder {
43    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName, key: ObjectKey) -> Self {
44        Self {
45            client,
46            bucket,
47            key,
48            tags: Vec::new(),
49        }
50    }
51
52    pub fn tag(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
53        self.tags.push((k.into(), v.into()));
54        self
55    }
56
57    pub async fn send(self) -> Result<PutObjectTaggingOutput> {
58        let endpoint = self.client.endpoint.clone();
59        let uri = oss_endpoint_url(
60            &endpoint,
61            Some(self.bucket.as_str()),
62            Some(self.key.as_str()),
63        );
64        let full_uri = format!("{}?tagging", uri);
65        let qp: Vec<(String, String)> = vec![("tagging".into(), String::new())];
66
67        let config = TaggingConfig {
68            tag_set: TagSetConfig {
69                tags: self
70                    .tags
71                    .into_iter()
72                    .map(|(k, v)| TagPair { key: k, value: v })
73                    .collect(),
74            },
75        };
76        let body_xml = crate::util::xml::to_xml(&config)?;
77
78        let request = HttpRequest::builder()
79            .method(http::Method::PUT)
80            .uri(&full_uri)
81            .body(bytes::Bytes::from(body_xml))
82            .build();
83        let response = self
84            .client
85            .send_signed(request, Some(&self.bucket), qp)
86            .await
87            .map_err(|e| OssError {
88                kind: OssErrorKind::TransportError,
89                context: Box::new(ErrorContext {
90                    operation: Some("PutObjectTagging".into()),
91                    bucket: Some(self.bucket.to_string()),
92                    object_key: Some(self.key.to_string()),
93                    endpoint: Some(endpoint),
94                    ..Default::default()
95                }),
96                source: Some(Box::new(e)),
97            })?;
98
99        if response.is_success() {
100            Ok(PutObjectTaggingOutput {
101                request_id: response
102                    .headers
103                    .get("x-oss-request-id")
104                    .and_then(|v| v.to_str().ok())
105                    .unwrap_or("")
106                    .to_string(),
107            })
108        } else {
109            Err(OssError {
110                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
111                    status_code: response.status().as_u16(),
112                    code: String::new(),
113                    message: String::new(),
114                    request_id: String::new(),
115                    host_id: String::new(),
116                    resource: Some(self.key.to_string()),
117                    string_to_sign: None,
118                })),
119                context: Box::new(ErrorContext {
120                    operation: Some("PutObjectTagging".into()),
121                    bucket: Some(self.bucket.to_string()),
122                    object_key: Some(self.key.to_string()),
123                    ..Default::default()
124                }),
125                source: None,
126            })
127        }
128    }
129}
130
131#[derive(Debug, Clone)]
132pub struct PutObjectTaggingOutput {
133    pub request_id: String,
134}
135
136pub struct GetObjectTaggingBuilder {
137    client: Arc<OSSClientInner>,
138    bucket: BucketName,
139    key: ObjectKey,
140}
141
142impl GetObjectTaggingBuilder {
143    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName, key: ObjectKey) -> Self {
144        Self {
145            client,
146            bucket,
147            key,
148        }
149    }
150
151    pub async fn send(self) -> Result<GetObjectTaggingOutput> {
152        let endpoint = self.client.endpoint.clone();
153        let uri = oss_endpoint_url(
154            &endpoint,
155            Some(self.bucket.as_str()),
156            Some(self.key.as_str()),
157        );
158        let full_uri = format!("{}?tagging", uri);
159        let qp: Vec<(String, String)> = vec![("tagging".into(), String::new())];
160
161        let request = HttpRequest::builder()
162            .method(http::Method::GET)
163            .uri(&full_uri)
164            .build();
165        let response = self
166            .client
167            .send_signed(request, Some(&self.bucket), qp)
168            .await
169            .map_err(|e| OssError {
170                kind: OssErrorKind::TransportError,
171                context: Box::new(ErrorContext {
172                    operation: Some("GetObjectTagging".into()),
173                    bucket: Some(self.bucket.to_string()),
174                    object_key: Some(self.key.to_string()),
175                    endpoint: Some(endpoint),
176                    ..Default::default()
177                }),
178                source: Some(Box::new(e)),
179            })?;
180
181        if response.is_success() {
182            let body_str = response.body_as_str().unwrap_or("");
183            let config: TaggingConfig =
184                crate::util::xml::from_xml(body_str).map_err(|e| OssError {
185                    kind: OssErrorKind::DeserializationError,
186                    context: Box::new(ErrorContext {
187                        operation: Some("GetObjectTagging: parse XML".into()),
188                        bucket: Some(self.bucket.to_string()),
189                        object_key: Some(self.key.to_string()),
190                        ..Default::default()
191                    }),
192                    source: Some(Box::new(e)),
193                })?;
194            Ok(GetObjectTaggingOutput {
195                tags: config
196                    .tag_set
197                    .tags
198                    .into_iter()
199                    .map(|t| (t.key, t.value))
200                    .collect(),
201            })
202        } else {
203            Err(OssError {
204                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
205                    status_code: response.status().as_u16(),
206                    code: String::new(),
207                    message: String::new(),
208                    request_id: String::new(),
209                    host_id: String::new(),
210                    resource: Some(self.key.to_string()),
211                    string_to_sign: None,
212                })),
213                context: Box::new(ErrorContext {
214                    operation: Some("GetObjectTagging".into()),
215                    bucket: Some(self.bucket.to_string()),
216                    object_key: Some(self.key.to_string()),
217                    ..Default::default()
218                }),
219                source: None,
220            })
221        }
222    }
223}
224
225#[derive(Debug, Clone)]
226pub struct GetObjectTaggingOutput {
227    pub tags: Vec<(String, String)>,
228}
229
230pub struct DeleteObjectTaggingBuilder {
231    client: Arc<OSSClientInner>,
232    bucket: BucketName,
233    key: ObjectKey,
234}
235
236impl DeleteObjectTaggingBuilder {
237    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName, key: ObjectKey) -> Self {
238        Self {
239            client,
240            bucket,
241            key,
242        }
243    }
244    pub async fn send(self) -> Result<DeleteObjectTaggingOutput> {
245        let endpoint = self.client.endpoint.clone();
246        let uri = oss_endpoint_url(
247            &endpoint,
248            Some(self.bucket.as_str()),
249            Some(self.key.as_str()),
250        );
251        let full_uri = format!("{}?tagging", uri);
252        let qp: Vec<(String, String)> = vec![("tagging".into(), String::new())];
253
254        let request = HttpRequest::builder()
255            .method(http::Method::DELETE)
256            .uri(&full_uri)
257            .build();
258        let response = self
259            .client
260            .send_signed(request, Some(&self.bucket), qp)
261            .await
262            .map_err(|e| OssError {
263                kind: OssErrorKind::TransportError,
264                context: Box::new(ErrorContext {
265                    operation: Some("DeleteObjectTagging".into()),
266                    bucket: Some(self.bucket.to_string()),
267                    object_key: Some(self.key.to_string()),
268                    endpoint: Some(endpoint),
269                    ..Default::default()
270                }),
271                source: Some(Box::new(e)),
272            })?;
273
274        if response.status().is_success() {
275            Ok(DeleteObjectTaggingOutput {
276                request_id: response
277                    .headers
278                    .get("x-oss-request-id")
279                    .and_then(|v| v.to_str().ok())
280                    .unwrap_or("")
281                    .to_string(),
282            })
283        } else {
284            Err(OssError {
285                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
286                    status_code: response.status().as_u16(),
287                    code: String::new(),
288                    message: String::new(),
289                    request_id: String::new(),
290                    host_id: String::new(),
291                    resource: Some(self.key.to_string()),
292                    string_to_sign: None,
293                })),
294                context: Box::new(ErrorContext {
295                    operation: Some("DeleteObjectTagging".into()),
296                    bucket: Some(self.bucket.to_string()),
297                    object_key: Some(self.key.to_string()),
298                    ..Default::default()
299                }),
300                source: None,
301            })
302        }
303    }
304}
305
306#[derive(Debug, Clone)]
307pub struct DeleteObjectTaggingOutput {
308    pub request_id: String,
309}
310
311impl BucketOperations {
312    pub fn put_object_tagging(&self, key: impl Into<String>) -> Result<PutObjectTaggingBuilder> {
313        Ok(PutObjectTaggingBuilder::new(
314            self.client_inner().clone(),
315            self.bucket_name().clone(),
316            ObjectKey::new(key.into())?,
317        ))
318    }
319    pub fn get_object_tagging(&self, key: impl Into<String>) -> Result<GetObjectTaggingBuilder> {
320        Ok(GetObjectTaggingBuilder::new(
321            self.client_inner().clone(),
322            self.bucket_name().clone(),
323            ObjectKey::new(key.into())?,
324        ))
325    }
326    pub fn delete_object_tagging(
327        &self,
328        key: impl Into<String>,
329    ) -> Result<DeleteObjectTaggingBuilder> {
330        Ok(DeleteObjectTaggingBuilder::new(
331            self.client_inner().clone(),
332            self.bucket_name().clone(),
333            ObjectKey::new(key.into())?,
334        ))
335    }
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341    use crate::client::OSSClientInner;
342    use crate::config::credentials::Credentials;
343    use crate::http::client::{HttpClient, HttpRequest, HttpResponse};
344    use crate::types::region::Region;
345    use std::sync::Mutex;
346
347    struct Rc {
348        r: Arc<Mutex<Vec<HttpRequest>>>,
349    }
350    #[async_trait::async_trait]
351    impl HttpClient for Rc {
352        async fn send(&self, req: HttpRequest) -> crate::error::Result<HttpResponse> {
353            self.r.lock().unwrap().push(req);
354            let mut h = http::HeaderMap::new();
355            h.insert("x-oss-request-id", http::HeaderValue::from_static("rid"));
356            Ok(HttpResponse {
357                status: http::StatusCode::OK,
358                headers: h,
359                body: bytes::Bytes::new(),
360            })
361        }
362    }
363    fn ci() -> (Arc<OSSClientInner>, Arc<Mutex<Vec<HttpRequest>>>) {
364        let rq = Arc::new(Mutex::new(Vec::new()));
365        let h = Arc::new(Rc { r: rq.clone() });
366        let cr = Arc::new(crate::config::credentials::StaticCredentialsProvider::new(
367            Credentials::builder()
368                .access_key_id("ak")
369                .access_key_secret("sk")
370                .build()
371                .unwrap(),
372        ));
373        (
374            Arc::new(OSSClientInner {
375                http: h,
376                credentials: cr,
377                signer: Arc::from(crate::signer::create_signer(crate::signer::SignVersion::V4)),
378                region: Region::CnHangzhou,
379                endpoint: "oss-cn-hangzhou.aliyuncs.com".into(),
380            }),
381            rq,
382        )
383    }
384
385    #[test]
386    fn tagging_xml() {
387        let c = TaggingConfig {
388            tag_set: TagSetConfig {
389                tags: vec![TagPair {
390                    key: "env".into(),
391                    value: "prod".into(),
392                }],
393            },
394        };
395        let xml = crate::util::xml::to_xml(&c).unwrap();
396        assert!(xml.contains("<Key>env</Key>"));
397        assert!(xml.contains("<Value>prod</Value>"));
398    }
399
400    #[tokio::test]
401    async fn delete_tagging_sends_delete() {
402        let (i, r) = ci();
403        DeleteObjectTaggingBuilder::new(
404            i,
405            BucketName::new("test-bucket").unwrap(),
406            ObjectKey::new("k").unwrap(),
407        )
408        .send()
409        .await
410        .unwrap();
411        assert_eq!(r.lock().unwrap()[0].method, http::Method::DELETE);
412    }
413}