Skip to main content

aliyun_oss/operations/
bucket_logging.rs

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