1pub mod body;
2mod credential;
3pub mod credentials;
4mod error;
5pub mod ops;
6mod query_auth_option;
7pub mod response;
8mod ser;
9mod utils;
10
11use std::borrow::Cow;
12use std::collections::HashSet;
13use std::time::Duration;
14
15use http::HeaderMap;
16use http::header::HOST;
17use serde::Serialize;
18use tracing::trace;
19use url::Url;
20
21pub use self::body::MakeBody;
22use self::credential::SignContext;
23use self::credentials::{
24 CredentialsProvider,
25 DefaultCredentialsChain,
26 DynCredentialsProvider,
27 StaticCredentialsProvider,
28};
29pub use self::error::{Error, Result};
30pub use self::query_auth_option::{QueryAuthOptions, QueryAuthOptionsBuilder};
31pub use self::response::ResponseProcessor;
32pub use self::utils::escape_path;
33
34pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
36
37#[derive(Debug, Clone)]
38pub struct Prepared<Q = (), B = ()> {
39 pub method: http::Method,
41 pub key: Option<String>,
43 pub additional_headers: HashSet<String>,
45 pub headers: Option<HeaderMap>,
47 pub query: Option<Q>,
49 pub body: Option<B>,
51}
52
53impl<Q, B> Default for Prepared<Q, B> {
54 fn default() -> Self {
55 Self {
56 method: http::Method::GET,
57 key: None,
58 additional_headers: HashSet::new(),
59 headers: None,
60 query: None,
61 body: None,
62 }
63 }
64}
65
66pub trait Ops: Sized {
67 const PRODUCT: &'static str = "oss";
68 const USE_BUCKET: bool = true;
69
70 type Query;
71 type Body: MakeBody;
72 type Response;
73
74 fn prepare(self) -> Result<Prepared<Self::Query, <Self::Body as MakeBody>::Body>>;
75}
76
77pub(crate) trait Request<P> {
78 type Response;
79
80 fn request(&self, ops: P) -> impl Future<Output = Result<Self::Response>>;
81
82 fn presign(
83 &self,
84 ops: P,
85 public: bool,
86 query_auth_options: Option<QueryAuthOptions>,
87 ) -> impl Future<Output = Result<String>>;
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
91pub enum UrlStyle {
92 #[default]
93 VirtualHosted,
94 Path,
95 CName,
96}
97
98pub struct ClientConfig {
101 pub http_timeout: Duration,
103 pub default_headers: http::HeaderMap,
105 pub url_style: UrlStyle,
107 pub public_url_style: UrlStyle,
109}
110
111impl Default for ClientConfig {
112 fn default() -> Self {
113 ClientConfig {
114 http_timeout: Duration::from_secs(30),
115 default_headers: http::HeaderMap::default(),
116 url_style: UrlStyle::default(),
117 public_url_style: UrlStyle::default(),
118 }
119 }
120}
121
122#[derive(Debug, Clone)]
123pub struct Client {
124 http_client: reqwest::Client,
125
126 raw_internal_host: String,
127 raw_internal_scheme: String,
128 raw_public_host: String,
129 raw_public_scheme: String,
130 region: String,
131 bucket: String,
132 url_style: UrlStyle,
133 public_url_style: UrlStyle,
134 credentials_provider: DynCredentialsProvider,
135}
136
137impl Client {
138 pub fn builder() -> ClientBuilder {
139 ClientBuilder::new()
140 }
141
142 fn build_url<'a>(
143 &'a self,
144 bucket: Option<Cow<'a, str>>,
145 key: Option<Cow<'a, str>>,
146 public: bool,
147 ) -> (Cow<'a, str>, Cow<'a, str>) {
148 let host = if public {
149 self.raw_public_host.as_str()
150 } else {
151 self.raw_internal_host.as_str()
152 };
153
154 let url_style = if public {
155 self.public_url_style
156 } else {
157 self.url_style
158 };
159
160 let (host, paths) = match (bucket, url_style) {
161 (Some(bucket_name), UrlStyle::VirtualHosted) => {
162 (Cow::Owned(format!("{bucket_name}.{host}")), None)
163 },
164 (Some(bucket_name), UrlStyle::Path) => {
165 let mut paths = Vec::with_capacity(2);
166 paths.push(bucket_name);
167 if key.is_none() {
168 paths.push(Cow::Borrowed(""));
169 }
170 (Cow::Borrowed(host), Some(paths))
171 },
172 (None, _) | (Some(_), UrlStyle::CName) => (Cow::Borrowed(host), None),
173 };
174
175 let path = match (paths, key.as_ref().map(|k| k.trim().trim_start_matches('/'))) {
176 (Some(paths), Some(key_str)) => {
177 let mut paths = paths.clone();
178 paths.push(escape_path(key_str).into());
179 Cow::Owned(format!("/{}", paths.join("/")))
180 },
181 (Some(paths), None) => Cow::Owned(format!("/{}", paths.join("/"))),
182 (None, Some(key_str)) => Cow::Owned(format!("/{}", escape_path(key_str))),
183 (None, None) => Cow::Borrowed("/"),
184 };
185
186 (host, path)
187 }
188
189 async fn prepare_request<P>(
190 &self,
191 ops: P,
192 public: bool,
193 query_auth_options: Option<QueryAuthOptions>,
194 ) -> Result<reqwest::Request>
195 where
196 P: Ops + Send + 'static,
197 P::Query: Serialize + Clone + Send,
198 P::Response: ResponseProcessor + Send,
199 P::Body: MakeBody + Send,
200 {
201 let Prepared {
202 method,
203 key,
204 additional_headers,
205 headers: extra_headers,
206 query,
207 body,
208 } = ops.prepare()?;
209
210 let bucket = P::USE_BUCKET.then_some(Cow::Borrowed(self.bucket.as_str()));
211
212 let (host, path) = self.build_url(bucket.clone(), key.as_deref().map(Cow::Borrowed), public);
214 let scheme = if public {
215 &self.raw_public_scheme
216 } else {
217 &self.raw_internal_scheme
218 };
219 let request_url = format!("{scheme}://{host}{path}");
220 let mut request = self.http_client.request(method.clone(), request_url).build()?;
221
222 let headers = request.headers_mut();
223 if let Some(extra_headers) = extra_headers {
225 headers.extend(extra_headers);
226 }
227
228 headers.insert(HOST, host.parse()?);
229
230 let additional_headers = additional_headers
232 .into_iter()
233 .map(Cow::Owned)
234 .collect::<HashSet<Cow<str>>>();
235
236 if let Some(body) = body {
238 P::Body::make_body(body, &mut request)?;
239 }
240
241 let sign_context = SignContext {
243 region: Cow::Borrowed(&self.region),
244 product: Cow::Borrowed(P::PRODUCT),
245 bucket,
246 key: key.as_deref().map(Cow::Borrowed),
247 query: query.as_ref(),
248 additional_headers,
249 };
250
251 let credentials = self.credentials_provider.get_credentials().await?;
253
254 credential::auth_to(&credentials, &mut request, sign_context, query_auth_options)?;
256
257 Ok(request)
258 }
259}
260
261impl<P> Request<P> for Client
262where
263 P: Ops + Send + 'static,
264 P::Query: Clone + Serialize + Send,
265 P::Response: ResponseProcessor + Send,
266 P::Body: MakeBody + Send,
267{
268 type Response = <P::Response as ResponseProcessor>::Output;
269
270 async fn request(&self, ops: P) -> Result<Self::Response> {
271 let request = self.prepare_request(ops, false, None).await?;
272
273 trace!("Sending request: {request:?}");
275 let resp = self.http_client.execute(request).await?;
276
277 P::Response::from_response(resp).await
279 }
280
281 async fn presign(
282 &self,
283 ops: P,
284 public: bool,
285 query_auth_options: Option<QueryAuthOptions>,
286 ) -> Result<String> {
287 let request = self.prepare_request(ops, public, query_auth_options).await?;
288
289 let sign_url = request.url().to_string();
290 Ok(sign_url)
291 }
292}
293
294pub struct ClientBuilder {
295 config: ClientConfig,
296 endpoint: Option<String>,
297 public_endpoint: Option<String>,
298 region: Option<String>,
299 bucket: Option<String>,
300 access_key_id: Option<String>,
301 access_key_secret: Option<String>,
302 security_token: Option<String>,
303 credentials_provider: Option<DynCredentialsProvider>,
304}
305
306impl ClientBuilder {
307 pub fn new() -> Self {
308 Self {
309 config: ClientConfig::default(),
310 endpoint: None,
311 public_endpoint: None,
312 region: None,
313 bucket: None,
314 access_key_id: None,
315 access_key_secret: None,
316 security_token: None,
317 credentials_provider: None,
318 }
319 }
320
321 pub fn endpoint<T: AsRef<str>>(mut self, endpoint: T) -> Self {
323 self.endpoint = Some(endpoint.as_ref().to_string());
324 self
325 }
326
327 pub fn public_endpoint<T: AsRef<str>>(mut self, public_endpoint: T) -> Self {
329 self.public_endpoint = Some(public_endpoint.as_ref().to_string());
330 self
331 }
332
333 pub fn region<T: AsRef<str>>(mut self, region: T) -> Self {
335 self.region = Some(region.as_ref().to_string());
336 self
337 }
338
339 pub fn bucket<T: AsRef<str>>(mut self, bucket: T) -> Self {
341 self.bucket = Some(bucket.as_ref().to_string());
342 self
343 }
344
345 pub fn access_key_id<T: AsRef<str>>(mut self, access_key_id: T) -> Self {
352 self.access_key_id = Some(access_key_id.as_ref().to_string());
353 self
354 }
355
356 pub fn access_key_secret<T: AsRef<str>>(mut self, access_key_secret: T) -> Self {
359 self.access_key_secret = Some(access_key_secret.as_ref().to_string());
360 self
361 }
362
363 pub fn security_token<T: AsRef<str>>(mut self, security_token: T) -> Self {
366 self.security_token = Some(security_token.as_ref().to_string());
367 self
368 }
369
370 pub fn credentials_provider<P>(mut self, provider: P) -> Self
379 where
380 P: CredentialsProvider + 'static,
381 {
382 self.credentials_provider = Some(DynCredentialsProvider::new(provider));
383 self
384 }
385
386 pub fn http_timeout(mut self, timeout: Duration) -> Self {
388 self.config.http_timeout = timeout;
389 self
390 }
391
392 pub fn default_headers(mut self, headers: http::HeaderMap) -> Self {
394 self.config.default_headers = headers;
395 self
396 }
397
398 pub fn url_style(mut self, style: UrlStyle) -> Self {
400 self.config.url_style = style;
401 self
402 }
403
404 pub fn public_url_style(mut self, style: UrlStyle) -> Self {
406 self.config.public_url_style = style;
407 self
408 }
409
410 pub fn build(self) -> Result<Client> {
412 let endpoint = self
414 .endpoint
415 .ok_or_else(|| Error::InvalidArgument("endpoint is required".to_string()))?;
416 let region = self
417 .region
418 .ok_or_else(|| Error::InvalidArgument("region is required".to_string()))?;
419 let bucket = self
420 .bucket
421 .ok_or_else(|| Error::InvalidArgument("bucket is required".to_string()))?;
422
423 let http_client = reqwest::Client::builder()
425 .default_headers(self.config.default_headers)
426 .timeout(self.config.http_timeout)
427 .build()?;
428
429 let endpoint_url = Url::parse(&endpoint)?;
431 let raw_internal_host = endpoint_url.host_str().ok_or(Error::MissingHost)?.to_owned();
432 let raw_internal_scheme = endpoint_url.scheme().to_owned();
433
434 let public_endpoint_str = self.public_endpoint.as_ref().unwrap_or(&endpoint);
436 let public_endpoint_url = Url::parse(public_endpoint_str)?;
437 let raw_public_host = public_endpoint_url
438 .host_str()
439 .ok_or(Error::MissingHost)?
440 .to_owned();
441 let raw_public_scheme = public_endpoint_url.scheme().to_owned();
442
443 let credentials_provider = if let Some(provider) = self.credentials_provider {
448 provider
449 } else {
450 match (self.access_key_id, self.access_key_secret) {
451 (Some(ak), Some(sk)) => {
452 let provider = if let Some(token) = self.security_token {
453 StaticCredentialsProvider::with_security_token(ak, sk, token)
454 } else {
455 StaticCredentialsProvider::new(ak, sk)
456 };
457 DynCredentialsProvider::new(provider)
458 },
459 _ => DynCredentialsProvider::new(DefaultCredentialsChain::with_http_client(
460 http_client.clone(),
461 )),
462 }
463 };
464
465 Ok(Client {
466 region,
467 bucket,
468 raw_internal_host,
469 raw_internal_scheme,
470 raw_public_host,
471 raw_public_scheme,
472 url_style: self.config.url_style,
473 public_url_style: self.config.public_url_style,
474 credentials_provider,
475 http_client,
476 })
477 }
478}
479
480impl Default for ClientBuilder {
481 fn default() -> Self {
482 Self::new()
483 }
484}