1use crate::client::{BucketOperations, OSSClientInner};
4use crate::error::{ErrorContext, OssError, OssErrorKind, Result};
5use crate::http::client::{HttpRequest, HttpResponse};
6use crate::types::bucket::BucketName;
7use serde::{Deserialize, Serialize};
8use std::sync::Arc;
9
10#[derive(Debug, Clone, Serialize)]
11#[serde(rename = "CnameConfiguration")]
12struct CnameConfig {
13 #[serde(rename = "Domain")]
14 domain: String,
15}
16
17#[derive(Debug, Clone, Deserialize)]
18#[serde(rename = "ListCnameResult")]
19struct ListCnameResult {
20 #[serde(rename = "Cname", default)]
21 cnames: Vec<CnameInfo>,
22}
23#[derive(Debug, Clone, Deserialize)]
24struct CnameInfo {
25 #[serde(rename = "Domain")]
26 domain: String,
27 #[serde(rename = "Status", default)]
28 status: String,
29}
30
31pub struct PutBucketCnameBuilder {
32 client: Arc<OSSClientInner>,
33 bucket: BucketName,
34 domain: String,
35}
36impl PutBucketCnameBuilder {
37 pub(crate) fn new(c: Arc<OSSClientInner>, b: BucketName, d: impl Into<String>) -> Self {
38 Self {
39 client: c,
40 bucket: b,
41 domain: d.into(),
42 }
43 }
44 pub async fn send(self) -> Result<PutBucketCnameOutput> {
45 put_cname_impl(self.client, self.bucket, self.domain).await
46 }
47}
48#[derive(Debug, Clone)]
49pub struct PutBucketCnameOutput {
50 pub request_id: String,
51}
52
53pub struct ListBucketCnameBuilder {
54 client: Arc<OSSClientInner>,
55 bucket: BucketName,
56}
57impl ListBucketCnameBuilder {
58 pub(crate) fn new(c: Arc<OSSClientInner>, b: BucketName) -> Self {
59 Self {
60 client: c,
61 bucket: b,
62 }
63 }
64 pub async fn send(self) -> Result<ListBucketCnameOutput> {
65 list_cname_impl(self.client, self.bucket).await
66 }
67}
68#[derive(Debug, Clone)]
69pub struct ListBucketCnameOutput {
70 pub cnames: Vec<CnameEntry>,
71}
72#[derive(Debug, Clone)]
73pub struct CnameEntry {
74 pub domain: String,
75 pub status: String,
76}
77
78pub struct DeleteBucketCnameBuilder {
79 client: Arc<OSSClientInner>,
80 bucket: BucketName,
81 domain: String,
82}
83impl DeleteBucketCnameBuilder {
84 pub(crate) fn new(c: Arc<OSSClientInner>, b: BucketName, d: impl Into<String>) -> Self {
85 Self {
86 client: c,
87 bucket: b,
88 domain: d.into(),
89 }
90 }
91 pub async fn send(self) -> Result<DeleteBucketCnameOutput> {
92 delete_cname_impl(self.client, self.bucket, self.domain).await
93 }
94}
95#[derive(Debug, Clone)]
96pub struct DeleteBucketCnameOutput {
97 pub request_id: String,
98}
99
100async fn put_cname_impl(
101 client: Arc<OSSClientInner>,
102 bucket: BucketName,
103 domain: String,
104) -> Result<PutBucketCnameOutput> {
105 let ep = client.endpoint.clone();
106 let uri = format!("https://{}.{}?cname", bucket.as_str(), ep);
107 let qp = vec![("cname".into(), String::new())];
108 let xml = crate::util::xml::to_xml(&CnameConfig { domain })?;
109 let req = HttpRequest::builder()
110 .method(http::Method::PUT)
111 .uri(&uri)
112 .body(bytes::Bytes::from(xml))
113 .build();
114 let r = client
115 .send_signed(req, Some(&bucket), qp)
116 .await
117 .map_err(|e| err("PutBucketCname", &bucket, &ep, e))?;
118 if r.status().is_success() {
119 Ok(PutBucketCnameOutput {
120 request_id: rid(&r),
121 })
122 } else {
123 Err(service_err("PutBucketCname", &bucket, r))
124 }
125}
126
127async fn list_cname_impl(
128 client: Arc<OSSClientInner>,
129 bucket: BucketName,
130) -> Result<ListBucketCnameOutput> {
131 let ep = client.endpoint.clone();
132 let uri = format!("https://{}.{}?cname", bucket.as_str(), ep);
133 let qp = vec![("cname".into(), String::new())];
134 let req = HttpRequest::builder()
135 .method(http::Method::GET)
136 .uri(&uri)
137 .build();
138 let r = client
139 .send_signed(req, Some(&bucket), qp)
140 .await
141 .map_err(|e| err("ListBucketCname", &bucket, &ep, e))?;
142 if r.is_success() {
143 let result: ListCnameResult = crate::util::xml::from_xml(r.body_as_str().unwrap_or(""))
144 .map_err(|e| OssError {
145 kind: OssErrorKind::DeserializationError,
146 context: Box::new(ErrorContext {
147 operation: Some("parse XML".into()),
148 bucket: Some(bucket.to_string()),
149 ..Default::default()
150 }),
151 source: Some(Box::new(e)),
152 })?;
153 Ok(ListBucketCnameOutput {
154 cnames: result
155 .cnames
156 .into_iter()
157 .map(|c| CnameEntry {
158 domain: c.domain,
159 status: c.status,
160 })
161 .collect(),
162 })
163 } else {
164 Err(service_err("ListBucketCname", &bucket, r))
165 }
166}
167
168async fn delete_cname_impl(
169 client: Arc<OSSClientInner>,
170 bucket: BucketName,
171 domain: String,
172) -> Result<DeleteBucketCnameOutput> {
173 let ep = client.endpoint.clone();
174 let uri = format!("https://{}.{}?cname&comp=delete", bucket.as_str(), ep);
175 let qp = vec![
176 ("cname".into(), String::new()),
177 ("comp".into(), "delete".into()),
178 ];
179 let xml = crate::util::xml::to_xml(&CnameConfig { domain })?;
180 let req = HttpRequest::builder()
181 .method(http::Method::POST)
182 .uri(&uri)
183 .body(bytes::Bytes::from(xml))
184 .build();
185 let r = client
186 .send_signed(req, Some(&bucket), qp)
187 .await
188 .map_err(|e| err("DeleteBucketCname", &bucket, &ep, e))?;
189 if r.status().is_success() {
190 Ok(DeleteBucketCnameOutput {
191 request_id: rid(&r),
192 })
193 } else {
194 Err(service_err("DeleteBucketCname", &bucket, r))
195 }
196}
197
198fn rid(r: &HttpResponse) -> String {
199 r.headers
200 .get("x-oss-request-id")
201 .and_then(|v| v.to_str().ok())
202 .unwrap_or("")
203 .to_string()
204}
205fn err(op: &str, b: &BucketName, ep: &str, e: OssError) -> OssError {
206 OssError {
207 kind: OssErrorKind::TransportError,
208 context: Box::new(ErrorContext {
209 operation: Some(op.into()),
210 bucket: Some(b.to_string()),
211 endpoint: Some(ep.into()),
212 ..Default::default()
213 }),
214 source: Some(Box::new(e)),
215 }
216}
217fn service_err(op: &str, b: &BucketName, r: HttpResponse) -> OssError {
218 OssError {
219 kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
220 status_code: r.status().as_u16(),
221 code: String::new(),
222 message: String::new(),
223 request_id: String::new(),
224 host_id: String::new(),
225 resource: Some(b.to_string()),
226 string_to_sign: None,
227 })),
228 context: Box::new(ErrorContext {
229 operation: Some(op.into()),
230 bucket: Some(b.to_string()),
231 ..Default::default()
232 }),
233 source: None,
234 }
235}
236
237impl BucketOperations {
238 pub fn put_cname(&self, domain: impl Into<String>) -> PutBucketCnameBuilder {
239 PutBucketCnameBuilder::new(
240 self.client_inner().clone(),
241 self.bucket_name().clone(),
242 domain,
243 )
244 }
245 pub fn list_cname(&self) -> ListBucketCnameBuilder {
246 ListBucketCnameBuilder::new(self.client_inner().clone(), self.bucket_name().clone())
247 }
248 pub fn delete_cname(&self, domain: impl Into<String>) -> DeleteBucketCnameBuilder {
249 DeleteBucketCnameBuilder::new(
250 self.client_inner().clone(),
251 self.bucket_name().clone(),
252 domain,
253 )
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use crate::client::OSSClientInner;
261 use crate::config::credentials::Credentials;
262 use crate::http::client::{HttpClient, HttpRequest, HttpResponse};
263 use crate::types::region::Region;
264 use std::sync::Mutex;
265 struct Rc {
266 r: Arc<Mutex<Vec<HttpRequest>>>,
267 }
268 #[async_trait::async_trait]
269 impl HttpClient for Rc {
270 async fn send(&self, req: HttpRequest) -> crate::error::Result<HttpResponse> {
271 self.r.lock().unwrap().push(req);
272 let mut h = http::HeaderMap::new();
273 h.insert("x-oss-request-id", http::HeaderValue::from_static("rid"));
274 Ok(HttpResponse {
275 status: http::StatusCode::OK,
276 headers: h,
277 body: bytes::Bytes::new(),
278 })
279 }
280 }
281 fn ci() -> (Arc<OSSClientInner>, Arc<Mutex<Vec<HttpRequest>>>) {
282 let rq = Arc::new(Mutex::new(Vec::new()));
283 let h = Arc::new(Rc { r: rq.clone() });
284 let cr = Arc::new(crate::config::credentials::StaticCredentialsProvider::new(
285 Credentials::builder()
286 .access_key_id("ak")
287 .access_key_secret("sk")
288 .build()
289 .unwrap(),
290 ));
291 (
292 Arc::new(OSSClientInner {
293 http: h,
294 credentials: cr,
295 signer: Arc::from(crate::signer::create_signer(crate::signer::SignVersion::V4)),
296 region: Region::CnHangzhou,
297 endpoint: "oss-cn-hangzhou.aliyuncs.com".into(),
298 }),
299 rq,
300 )
301 }
302 #[test]
303 fn cname_xml() {
304 let c = CnameConfig {
305 domain: "cdn.example.com".into(),
306 };
307 let x = crate::util::xml::to_xml(&c).unwrap();
308 assert!(x.contains("<Domain>cdn.example.com</Domain>"));
309 }
310 #[tokio::test]
311 async fn put_cname_sends_request() {
312 let (i, r) = ci();
313 PutBucketCnameBuilder::new(
314 i,
315 BucketName::new("test-bucket").unwrap(),
316 "cdn.example.com",
317 )
318 .send()
319 .await
320 .unwrap();
321 assert_eq!(r.lock().unwrap()[0].method, http::Method::PUT);
322 }
323}