Skip to main content

aliyun_oss/operations/
object_acl.rs

1//! Object ACL operations.
2
3use std::sync::Arc;
4
5use crate::client::{BucketOperations, OSSClientInner};
6use crate::error::{ErrorContext, OssError, OssErrorKind, Result};
7use crate::http::client::HttpRequest;
8use crate::types::acl::ObjectAcl;
9use crate::types::bucket::BucketName;
10use crate::types::object::ObjectKey;
11use crate::util::uri::oss_endpoint_url;
12
13pub struct GetObjectAclBuilder {
14    client: Arc<OSSClientInner>,
15    bucket: BucketName,
16    key: ObjectKey,
17    version_id: Option<String>,
18}
19
20impl GetObjectAclBuilder {
21    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName, key: ObjectKey) -> Self {
22        Self {
23            client,
24            bucket,
25            key,
26            version_id: None,
27        }
28    }
29
30    pub fn version_id(mut self, id: impl Into<String>) -> Self {
31        self.version_id = Some(id.into());
32        self
33    }
34
35    pub async fn send(self) -> Result<crate::types::response::GetObjectAclOutput> {
36        let endpoint = self.client.endpoint.clone();
37        let uri = oss_endpoint_url(
38            &endpoint,
39            Some(self.bucket.as_str()),
40            Some(self.key.as_str()),
41        );
42
43        let mut query_pairs: Vec<(String, String)> = vec![("acl".into(), String::new())];
44        if let Some(ref vid) = self.version_id {
45            query_pairs.push(("versionId".into(), vid.clone()));
46        }
47
48        let parts: Vec<String> = query_pairs
49            .iter()
50            .filter(|(_, v)| !v.is_empty())
51            .map(|(k, v)| format!("{}={}", k, v))
52            .chain(
53                query_pairs
54                    .iter()
55                    .filter(|(_, v)| v.is_empty())
56                    .map(|(k, _)| k.clone()),
57            )
58            .collect();
59        let query_string = if parts.is_empty() {
60            String::new()
61        } else {
62            format!("?{}", parts.join("&"))
63        };
64        let full_uri = format!("{}{}", uri, query_string);
65
66        let request = HttpRequest::builder()
67            .method(http::Method::GET)
68            .uri(&full_uri)
69            .build();
70
71        let response = self
72            .client
73            .send_signed(request, Some(&self.bucket), query_pairs)
74            .await
75            .map_err(|e| OssError {
76                kind: OssErrorKind::TransportError,
77                context: Box::new(ErrorContext {
78                    operation: Some("GetObjectAcl".into()),
79                    bucket: Some(self.bucket.to_string()),
80                    object_key: Some(self.key.to_string()),
81                    ..Default::default()
82                }),
83                source: Some(Box::new(e)),
84            })?;
85
86        if response.is_success() {
87            let body_str = response.body_as_str().unwrap_or("");
88            Ok(crate::util::xml::from_xml(body_str)?)
89        } else {
90            Err(OssError {
91                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
92                    status_code: response.status().as_u16(),
93                    code: String::new(),
94                    message: String::new(),
95                    request_id: String::new(),
96                    host_id: String::new(),
97                    resource: Some(self.key.to_string()),
98                    string_to_sign: None,
99                })),
100                context: Box::new(ErrorContext {
101                    operation: Some("GetObjectAcl".into()),
102                    bucket: Some(self.bucket.to_string()),
103                    object_key: Some(self.key.to_string()),
104                    ..Default::default()
105                }),
106                source: None,
107            })
108        }
109    }
110}
111
112pub struct PutObjectAclBuilder {
113    client: Arc<OSSClientInner>,
114    bucket: BucketName,
115    key: ObjectKey,
116    acl: ObjectAcl,
117    version_id: Option<String>,
118}
119
120impl PutObjectAclBuilder {
121    pub(crate) fn new(
122        client: Arc<OSSClientInner>,
123        bucket: BucketName,
124        key: ObjectKey,
125        acl: ObjectAcl,
126    ) -> Self {
127        Self {
128            client,
129            bucket,
130            key,
131            acl,
132            version_id: None,
133        }
134    }
135
136    pub fn version_id(mut self, id: impl Into<String>) -> Self {
137        self.version_id = Some(id.into());
138        self
139    }
140
141    pub async fn send(self) -> Result<PutObjectAclOutput> {
142        let endpoint = self.client.endpoint.clone();
143        let uri = oss_endpoint_url(
144            &endpoint,
145            Some(self.bucket.as_str()),
146            Some(self.key.as_str()),
147        );
148
149        let mut query_pairs: Vec<(String, String)> = vec![("acl".into(), String::new())];
150        if let Some(ref vid) = self.version_id {
151            query_pairs.push(("versionId".into(), vid.clone()));
152        }
153
154        let parts: Vec<String> = query_pairs
155            .iter()
156            .filter(|(_, v)| !v.is_empty())
157            .map(|(k, v)| format!("{}={}", k, v))
158            .chain(
159                query_pairs
160                    .iter()
161                    .filter(|(_, v)| v.is_empty())
162                    .map(|(k, _)| k.clone()),
163            )
164            .collect();
165        let query_string = if parts.is_empty() {
166            String::new()
167        } else {
168            format!("?{}", parts.join("&"))
169        };
170        let full_uri = format!("{}{}", uri, query_string);
171
172        let body_xml = format!(
173            r#"<?xml version="1.0" encoding="UTF-8"?><AccessControlPolicy><Owner><ID>default</ID></Owner><AccessControlList><Grant>{}</Grant></AccessControlList></AccessControlPolicy>"#,
174            self.acl.as_str()
175        );
176
177        let request = HttpRequest::builder()
178            .method(http::Method::PUT)
179            .uri(&full_uri)
180            .body(bytes::Bytes::from(body_xml))
181            .build();
182
183        let response = self
184            .client
185            .send_signed(request, Some(&self.bucket), query_pairs)
186            .await
187            .map_err(|e| OssError {
188                kind: OssErrorKind::TransportError,
189                context: Box::new(ErrorContext {
190                    operation: Some("PutObjectAcl".into()),
191                    bucket: Some(self.bucket.to_string()),
192                    object_key: Some(self.key.to_string()),
193                    ..Default::default()
194                }),
195                source: Some(Box::new(e)),
196            })?;
197
198        if response.is_success() {
199            Ok(PutObjectAclOutput {
200                request_id: response
201                    .headers
202                    .get("x-oss-request-id")
203                    .and_then(|v| v.to_str().ok())
204                    .unwrap_or("")
205                    .to_string(),
206            })
207        } else {
208            Err(OssError {
209                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
210                    status_code: response.status().as_u16(),
211                    code: String::new(),
212                    message: String::new(),
213                    request_id: String::new(),
214                    host_id: String::new(),
215                    resource: Some(self.key.to_string()),
216                    string_to_sign: None,
217                })),
218                context: Box::new(ErrorContext {
219                    operation: Some("PutObjectAcl".into()),
220                    bucket: Some(self.bucket.to_string()),
221                    object_key: Some(self.key.to_string()),
222                    ..Default::default()
223                }),
224                source: None,
225            })
226        }
227    }
228}
229
230#[derive(Debug, Clone)]
231pub struct PutObjectAclOutput {
232    pub request_id: String,
233}
234
235impl BucketOperations {
236    pub fn get_object_acl(&self, key: impl Into<String>) -> Result<GetObjectAclBuilder> {
237        let object_key = ObjectKey::new(key.into())?;
238        Ok(GetObjectAclBuilder::new(
239            self.client_inner().clone(),
240            self.bucket_name().clone(),
241            object_key,
242        ))
243    }
244
245    pub fn put_object_acl(
246        &self,
247        key: impl Into<String>,
248        acl: ObjectAcl,
249    ) -> Result<PutObjectAclBuilder> {
250        let object_key = ObjectKey::new(key.into())?;
251        Ok(PutObjectAclBuilder::new(
252            self.client_inner().clone(),
253            self.bucket_name().clone(),
254            object_key,
255            acl,
256        ))
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use std::str::FromStr;
263    use std::sync::Mutex;
264
265    use crate::client::OSSClientInner;
266    use crate::config::credentials::Credentials;
267    use crate::http::client::{HttpClient, HttpRequest, HttpResponse};
268    use crate::types::region::Region;
269
270    use super::*;
271
272    struct RecordingHttpClient {
273        requests: Arc<Mutex<Vec<HttpRequest>>>,
274        response_body: bytes::Bytes,
275    }
276
277    #[async_trait::async_trait]
278    impl HttpClient for RecordingHttpClient {
279        async fn send(&self, request: HttpRequest) -> crate::error::Result<HttpResponse> {
280            self.requests.lock().unwrap().push(request);
281            let mut headers = http::HeaderMap::new();
282            headers.insert(
283                "x-oss-request-id",
284                http::HeaderValue::from_static("rid-acl"),
285            );
286            Ok(HttpResponse {
287                status: http::StatusCode::OK,
288                headers,
289                body: self.response_body.clone(),
290            })
291        }
292    }
293
294    fn create_test_inner(
295        body: bytes::Bytes,
296    ) -> (Arc<OSSClientInner>, Arc<Mutex<Vec<HttpRequest>>>) {
297        let requests = Arc::new(Mutex::new(Vec::new()));
298        let http = Arc::new(RecordingHttpClient {
299            requests: requests.clone(),
300            response_body: body,
301        });
302        let credentials = Arc::new(crate::config::credentials::StaticCredentialsProvider::new(
303            Credentials::builder()
304                .access_key_id("test-ak")
305                .access_key_secret("test-sk")
306                .build()
307                .unwrap(),
308        ));
309        let inner = Arc::new(OSSClientInner {
310            http,
311            credentials,
312            signer: Arc::from(crate::signer::create_signer(crate::signer::SignVersion::V4)),
313            region: Region::CnHangzhou,
314            endpoint: "oss-cn-hangzhou.aliyuncs.com".into(),
315        });
316        (inner, requests)
317    }
318
319    #[tokio::test]
320    async fn get_object_acl_sends_request() {
321        let acl_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
322<AccessControlPolicy>
323  <Owner><ID>owner-id</ID></Owner>
324  <AccessControlList><Grant>private</Grant></AccessControlList>
325</AccessControlPolicy>"#;
326
327        let (inner, requests) = create_test_inner(bytes::Bytes::from(acl_xml));
328        let builder = GetObjectAclBuilder::new(
329            inner,
330            BucketName::new("test-bucket").unwrap(),
331            ObjectKey::new("obj.txt").unwrap(),
332        );
333
334        let output = builder.send().await.unwrap();
335        assert_eq!(output.acl.grant, "private");
336
337        let captured = requests.lock().unwrap();
338        assert!(captured[0].uri.contains("acl"));
339    }
340
341    #[tokio::test]
342    async fn put_object_acl_sends_with_xml_body() {
343        let (inner, requests) = create_test_inner(bytes::Bytes::new());
344        let builder = PutObjectAclBuilder::new(
345            inner,
346            BucketName::new("test-bucket").unwrap(),
347            ObjectKey::new("obj.txt").unwrap(),
348            ObjectAcl::PublicRead,
349        );
350
351        builder.send().await.unwrap();
352
353        let captured = requests.lock().unwrap();
354        assert_eq!(captured[0].method, http::Method::PUT);
355        let body_str = captured[0]
356            .body
357            .as_ref()
358            .map(|b| String::from_utf8_lossy(b).to_string());
359        assert!(body_str.unwrap().contains("public-read"));
360    }
361
362    #[tokio::test]
363    #[ignore = "requires valid OSS credentials"]
364    async fn e2e_object_acl_round_trip() {
365        let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
366        let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
367        let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
368        let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
369
370        let region = Region::from_str(&region_str).unwrap_or_else(|_| Region::Custom {
371            endpoint: format!("oss-{}.aliyuncs.com", region_str),
372            region_id: region_str.clone(),
373        });
374
375        let client = crate::client::OSSClient::builder()
376            .region(region)
377            .credentials(ak, sk)
378            .build()
379            .unwrap();
380
381        let key = format!("test-acl-{}.txt", chrono::Utc::now().timestamp());
382
383        client
384            .bucket(&bucket_str)
385            .unwrap()
386            .put_object(&key)
387            .unwrap()
388            .body(bytes::Bytes::from("acl test"))
389            .send()
390            .await
391            .unwrap();
392
393        client
394            .bucket(&bucket_str)
395            .unwrap()
396            .put_object_acl(&key, ObjectAcl::PublicRead)
397            .unwrap()
398            .send()
399            .await
400            .unwrap();
401
402        let acl_output = client
403            .bucket(&bucket_str)
404            .unwrap()
405            .get_object_acl(&key)
406            .unwrap()
407            .send()
408            .await
409            .unwrap();
410
411        assert_eq!(acl_output.acl.grant, "public-read");
412        eprintln!(
413            "ACL round-trip '{}' succeeded: grant={}",
414            key, acl_output.acl.grant
415        );
416
417        client
418            .bucket(&bucket_str)
419            .unwrap()
420            .delete_object(&key)
421            .unwrap()
422            .send()
423            .await
424            .unwrap();
425    }
426}