Skip to main content

aliyun_oss/
client.rs

1use std::sync::Arc;
2
3use crate::config::credentials::{CredentialsProvider, StaticCredentialsProvider};
4use crate::error::{ErrorContext, OssError, OssErrorKind, Result};
5use crate::http::client::{HttpClient, HttpRequest, HttpResponse, ReqwestHttpClient};
6use crate::http::middleware::extract_path;
7use crate::signer::{SignVersion, Signer, SigningRequest, create_signer};
8use crate::types::bucket::BucketName;
9use crate::types::region::Region;
10
11#[allow(dead_code)]
12pub(crate) struct OSSClientInner {
13    pub(crate) http: Arc<dyn HttpClient>,
14    pub(crate) credentials: Arc<dyn CredentialsProvider>,
15    pub(crate) signer: Arc<dyn Signer>,
16    pub(crate) region: Region,
17    pub(crate) endpoint: String,
18}
19
20impl OSSClientInner {
21    pub(crate) async fn send_signed(
22        &self,
23        request: HttpRequest,
24        bucket: Option<&BucketName>,
25        query_params: Vec<(String, String)>,
26    ) -> Result<HttpResponse> {
27        let creds = self.credentials.credentials().await?;
28
29        let timestamp = chrono::Utc::now().format("%Y%m%dT%H%M%SZ").to_string();
30
31        let mut headers: Vec<(String, String)> = request
32            .headers
33            .iter()
34            .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
35            .collect();
36
37        headers.push(("x-oss-date".to_string(), timestamp.clone()));
38
39        let path = extract_path(&request.uri);
40        let signing_uri = if let Some(bucket) = bucket {
41            format!("/{}{}", bucket.as_str(), path)
42        } else {
43            path
44        };
45
46        let mut signing_request = SigningRequest {
47            method: request.method.as_str().to_string(),
48            uri: signing_uri,
49            region: self.region.region_id().to_string(),
50            query_params,
51            headers,
52            timestamp,
53        };
54
55        self.signer.sign(&mut signing_request, &creds)?;
56
57        let mut signed_request = HttpRequest::builder()
58            .method(request.method.clone())
59            .uri(&request.uri);
60
61        for (key, value) in &signing_request.headers {
62            if let (Ok(name), Ok(val)) = (
63                http::HeaderName::from_bytes(key.as_bytes()),
64                http::HeaderValue::from_str(value),
65            ) {
66                signed_request = signed_request.header(name, val);
67            }
68        }
69
70        if let Some(body) = request.body {
71            signed_request = signed_request.body(body);
72        }
73
74        self.http.send(signed_request.build()).await
75    }
76}
77
78/// The main entry point for the Alibaba Cloud OSS SDK.
79///
80/// `OSSClient` is cheaply cloneable (wraps an `Arc` internally) and provides
81/// access to bucket-level and service-level operations.
82///
83/// # Examples
84///
85/// ```rust,no_run
86/// use aliyun_oss::client::OSSClient;
87/// use aliyun_oss::types::region::Region;
88///
89/// # async fn example() -> aliyun_oss::error::Result<()> {
90/// let client = OSSClient::builder()
91///     .region(Region::CnHangzhou)
92///     .credentials("your-ak", "your-sk")
93///     .build()?;
94///
95/// let bucket = client.bucket("my-bucket")?;
96/// # Ok(())
97/// # }
98/// ```
99#[derive(Clone)]
100pub struct OSSClient {
101    inner: Arc<OSSClientInner>,
102}
103
104impl OSSClient {
105    pub fn builder() -> OSSClientBuilder {
106        OSSClientBuilder::default()
107    }
108
109    pub fn region(&self) -> &Region {
110        &self.inner.region
111    }
112
113    pub fn endpoint(&self) -> &str {
114        &self.inner.endpoint
115    }
116
117    pub(crate) fn inner(&self) -> &Arc<OSSClientInner> {
118        &self.inner
119    }
120
121    pub fn presign(
122        &self,
123        bucket: impl Into<String>,
124        key: impl Into<String>,
125    ) -> crate::signer::presigned::PreSignedUrlBuilder {
126        let creds = tokio::runtime::Handle::current()
127            .block_on(self.inner.credentials.credentials())
128            .expect("credentials should be available for presign");
129        crate::signer::presigned::PreSignedUrlBuilder::new(
130            creds,
131            self.inner.region.region_id().to_string(),
132            self.inner.endpoint.clone(),
133            bucket.into(),
134            key.into(),
135        )
136    }
137
138    pub fn bucket(&self, name: impl Into<String>) -> Result<BucketOperations> {
139        let bucket = BucketName::new(name.into())?;
140        Ok(BucketOperations {
141            client: self.inner.clone(),
142            bucket,
143        })
144    }
145}
146
147#[derive(Default)]
148pub struct OSSClientBuilder {
149    region: Option<Region>,
150    credentials: Option<Arc<dyn CredentialsProvider>>,
151    http: Option<Arc<dyn HttpClient>>,
152    signer: Option<Arc<dyn Signer>>,
153    endpoint: Option<String>,
154}
155
156impl OSSClientBuilder {
157    pub fn region(mut self, region: Region) -> Self {
158        self.region = Some(region);
159        self
160    }
161
162    pub fn credentials(
163        mut self,
164        access_key_id: impl Into<String>,
165        access_key_secret: impl Into<String>,
166    ) -> Self {
167        let creds = crate::config::credentials::Credentials::builder()
168            .access_key_id(access_key_id.into())
169            .access_key_secret(access_key_secret.into())
170            .build()
171            .expect("credentials build should succeed");
172        self.credentials = Some(Arc::new(StaticCredentialsProvider::new(creds)));
173        self
174    }
175
176    pub fn credentials_provider(mut self, provider: impl CredentialsProvider + 'static) -> Self {
177        self.credentials = Some(Arc::new(provider));
178        self
179    }
180
181    pub fn http_client(mut self, client: impl HttpClient + 'static) -> Self {
182        self.http = Some(Arc::new(client));
183        self
184    }
185
186    pub fn signer(mut self, signer: impl Signer + 'static) -> Self {
187        self.signer = Some(Arc::new(signer));
188        self
189    }
190
191    pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
192        self.endpoint = Some(endpoint.into());
193        self
194    }
195
196    pub fn build(self) -> Result<OSSClient> {
197        let region = self.region.ok_or_else(|| OssError {
198            kind: OssErrorKind::ConfigError,
199            context: Box::new(ErrorContext {
200                operation: Some("OSSClientBuilder: region is required".into()),
201                ..Default::default()
202            }),
203            source: None,
204        })?;
205
206        let credentials = self.credentials.ok_or_else(|| OssError {
207            kind: OssErrorKind::ConfigError,
208            context: Box::new(ErrorContext {
209                operation: Some("OSSClientBuilder: credentials are required".into()),
210                ..Default::default()
211            }),
212            source: None,
213        })?;
214
215        let http = self
216            .http
217            .unwrap_or_else(|| Arc::new(ReqwestHttpClient::default()));
218
219        let signer = self
220            .signer
221            .unwrap_or_else(|| Arc::from(create_signer(SignVersion::V4)));
222
223        let endpoint = self
224            .endpoint
225            .unwrap_or_else(|| region.external_endpoint().to_string());
226
227        Ok(OSSClient {
228            inner: Arc::new(OSSClientInner {
229                http,
230                credentials,
231                signer,
232                region,
233                endpoint,
234            }),
235        })
236    }
237}
238
239pub struct BucketOperations {
240    pub(crate) client: Arc<OSSClientInner>,
241    pub(crate) bucket: BucketName,
242}
243
244impl BucketOperations {
245    pub fn bucket_name(&self) -> &BucketName {
246        &self.bucket
247    }
248
249    pub(crate) fn client_inner(&self) -> &Arc<OSSClientInner> {
250        &self.client
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    use crate::config::credentials::Credentials;
258    use crate::http::client::{HttpRequest, HttpResponse};
259    use crate::types::region::Region;
260
261    struct MockHttpClient;
262
263    #[async_trait::async_trait]
264    impl HttpClient for MockHttpClient {
265        async fn send(&self, _request: HttpRequest) -> Result<HttpResponse> {
266            Ok(HttpResponse::new(http::StatusCode::OK))
267        }
268    }
269
270    #[test]
271    fn oss_client_builder_requires_region_and_credentials() {
272        let client = OSSClient::builder()
273            .region(Region::CnHangzhou)
274            .credentials("test-ak", "test-sk")
275            .http_client(MockHttpClient)
276            .build();
277
278        assert!(client.is_ok());
279    }
280
281    #[test]
282    fn oss_client_builder_missing_region_returns_error() {
283        let client = OSSClient::builder()
284            .credentials("test-ak", "test-sk")
285            .http_client(MockHttpClient)
286            .build();
287
288        assert!(client.is_err());
289    }
290
291    #[test]
292    fn oss_client_builder_missing_credentials_returns_error() {
293        let client = OSSClient::builder()
294            .region(Region::CnHangzhou)
295            .http_client(MockHttpClient)
296            .build();
297
298        assert!(client.is_err());
299    }
300
301    #[test]
302    fn oss_client_builder_default_endpoint_from_region() {
303        let client = OSSClient::builder()
304            .region(Region::CnHangzhou)
305            .credentials("ak", "sk")
306            .http_client(MockHttpClient)
307            .build()
308            .unwrap();
309
310        assert_eq!(client.endpoint(), "oss-cn-hangzhou.aliyuncs.com");
311    }
312
313    #[test]
314    fn oss_client_builder_custom_endpoint() {
315        let client = OSSClient::builder()
316            .region(Region::CnHangzhou)
317            .credentials("ak", "sk")
318            .http_client(MockHttpClient)
319            .endpoint("custom.oss.example.com")
320            .build()
321            .unwrap();
322
323        assert_eq!(client.endpoint(), "custom.oss.example.com");
324    }
325
326    #[test]
327    fn oss_client_bucket_accessor_returns_bucket_operations() {
328        let client = OSSClient::builder()
329            .region(Region::CnHangzhou)
330            .credentials("ak", "sk")
331            .http_client(MockHttpClient)
332            .build()
333            .unwrap();
334
335        let bucket_ops = client.bucket("my-bucket").unwrap();
336        assert_eq!(bucket_ops.bucket_name().as_str(), "my-bucket");
337    }
338
339    #[test]
340    fn oss_client_bucket_accessor_rejects_invalid_bucket() {
341        let client = OSSClient::builder()
342            .region(Region::CnHangzhou)
343            .credentials("ak", "sk")
344            .http_client(MockHttpClient)
345            .build()
346            .unwrap();
347
348        let result = client.bucket("AB");
349        assert!(result.is_err());
350    }
351
352    #[test]
353    fn oss_client_clone() {
354        let client = OSSClient::builder()
355            .region(Region::CnHangzhou)
356            .credentials("ak", "sk")
357            .http_client(MockHttpClient)
358            .build()
359            .unwrap();
360
361        let cloned = client.clone();
362        assert_eq!(cloned.region(), &Region::CnHangzhou);
363        assert_eq!(cloned.endpoint(), client.endpoint());
364    }
365
366    #[test]
367    fn oss_client_with_credentials_provider() {
368        let credentials = Credentials::builder()
369            .access_key_id("ak")
370            .access_key_secret("sk")
371            .build()
372            .unwrap();
373        let provider = StaticCredentialsProvider::new(credentials);
374
375        let client = OSSClient::builder()
376            .region(Region::CnShanghai)
377            .credentials_provider(provider)
378            .http_client(MockHttpClient)
379            .build()
380            .unwrap();
381
382        assert_eq!(client.region(), &Region::CnShanghai);
383    }
384
385    #[test]
386    fn oss_client_send_sync() {
387        fn assert_send_sync<T: Send + Sync>() {}
388        assert_send_sync::<OSSClient>();
389        assert_send_sync::<BucketOperations>();
390    }
391}