1use 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(®ion_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}