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#[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}