1use 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}