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