Skip to main content

aliyun_oss/operations/
bucket_website.rs

1//! Static website hosting 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 = "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}