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;
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}