rustfs_rsc/client/
client.rs

1use std::str::FromStr;
2use std::sync::Arc;
3
4use crate::data::Data;
5use crate::error::{Error, Result, ValueError};
6use crate::provider::Provider;
7use crate::signer::sign_request_v4;
8use crate::utils::{check_bucket_name, urlencode, _VALID_ENDPOINT};
9use crate::Credentials;
10use hyper::{header, header::HeaderValue, HeaderMap};
11use hyper::{Method, Uri};
12use reqwest::{Body, Response};
13
14use super::{Bucket, BucketArgs};
15
16/// A `MinioBuilder` can be used to create a [`Minio`] with custom configuration.
17pub struct MinioBuilder {
18    endpoint: Option<String>,
19    // access_key: Option<String>,
20    // secret_key: Option<String>,
21    // session_token: Option<String>,
22    region: String,
23    agent: String,
24    secure: bool,
25    virtual_hosted: bool,
26    multi_chunked_encoding: bool,
27    provider: Option<Box<dyn Provider>>,
28    client: Option<reqwest::Client>,
29}
30
31impl MinioBuilder {
32    pub fn new() -> Self {
33        MinioBuilder {
34            endpoint: None,
35            secure: true,
36            virtual_hosted: false,
37            multi_chunked_encoding: true,
38            region: "us-east-1".to_string(),
39            agent: "MinIO (Linux; x86_64) minio-rs".to_string(),
40            provider: None,
41            client: None,
42        }
43    }
44
45    /// Set hostname of a S3 service.
46    #[deprecated(note = "Please use the `endpoint` instead")]
47    pub fn host<T: Into<String>>(mut self, host: T) -> Self {
48        let host: String = host.into();
49        if host.starts_with("http://") {
50            self.secure = false;
51            self.endpoint = Some(host[7..].into());
52        } else if host.starts_with("https://") {
53            self.secure = true;
54            self.endpoint = Some(host[8..].into());
55        } else {
56            self.endpoint = Some(host);
57        }
58        self
59    }
60
61    /// Set endpoint of a S3 service. `hostname`
62    pub fn endpoint<T: Into<String>>(mut self, endpoint: T) -> Self {
63        self.endpoint = Some(endpoint.into());
64        self
65    }
66
67    /// Set region name of buckets in S3 service.
68    ///
69    /// Default: `us-east-1`
70    pub fn region<T: Into<String>>(mut self, region: T) -> Self {
71        self.region = region.into();
72        self
73    }
74
75    /// Set agent header for minio client.
76    ///
77    /// Default: `MinIO (Linux; x86_64) minio-rs`
78    pub fn agent<T: Into<String>>(mut self, agent: T) -> Self {
79        self.agent = agent.into();
80        self
81    }
82
83    /// Set flag to indicate to use secure (TLS) connection to S3 service or not.
84    ///
85    /// Default: `true`.
86    pub fn secure(mut self, secure: bool) -> Self {
87        self.secure = secure;
88        self
89    }
90
91    /// Set custom http [reqwest::Client].
92    pub fn client(mut self, client: reqwest::Client) -> Self {
93        self.client = Some(client);
94        self
95    }
96
97    /// Set flag to indicate to use Virtual-hosted–style or not.
98    ///
99    /// In a virtual-hosted–style URI, the bucket name is part of the domain name in the URL.
100    /// like `https://bucket-name.s3.region-code.amazonaws.com`
101    ///
102    /// Default: `false`.
103    ///
104    /// **Note**: If the endpoint is an IP address, setting Virtual-hosted–style true will cause an error.
105    pub fn virtual_hosted_style(mut self, virtual_hosted_style: bool) -> Self {
106        self.virtual_hosted = virtual_hosted_style;
107        self
108    }
109
110    /// Set flag to indicate to use multi_chunked_encoding or not.
111    ///
112    /// Default: `true`.
113    pub fn multi_chunked_encoding(mut self, multi_chunked_encoding: bool) -> Self {
114        self.multi_chunked_encoding = multi_chunked_encoding;
115        self
116    }
117
118    /// Set credentials provider of your account in S3 service.
119    ///
120    /// **Required**.
121    pub fn provider<P>(mut self, provider: P) -> Self
122    where
123        P: Provider + 'static,
124    {
125        self.provider = Some(Box::new(provider));
126        self
127    }
128
129    pub fn build(self) -> std::result::Result<Minio, ValueError> {
130        let endpoint = self.endpoint.ok_or("Miss endpoint")?;
131        if !_VALID_ENDPOINT.is_match(&endpoint) {
132            return Err("Invalid endpoint".into());
133        }
134        let provider = self.provider.ok_or("Miss provide")?;
135
136        let agent: HeaderValue = self
137            .agent
138            .parse()
139            .map_err(|_| ValueError::from("Invalid agent"))?;
140
141        let client2 = self.client.unwrap_or_else(|| {
142            let mut headers = header::HeaderMap::new();
143            headers.insert(header::USER_AGENT, agent.clone());
144            reqwest::Client::builder()
145                .default_headers(headers)
146                .https_only(self.secure)
147                .max_tls_version(reqwest::tls::Version::TLS_1_2)
148                .build()
149                .unwrap()
150        });
151        Ok(Minio {
152            inner: Arc::new(MinioRef {
153                endpoint,
154                secure: self.secure,
155                client2,
156                virtual_hosted: self.virtual_hosted,
157                multi_chunked: self.multi_chunked_encoding,
158                region: self.region,
159                agent,
160                provider,
161            }),
162        })
163    }
164}
165
166/// Simple Storage Service (aka S3) client to perform bucket and object operations.
167///
168/// You do **not** have to wrap the `Minio` in an [`Rc`] or [`Arc`] to **reuse** it,
169/// because it already uses an [`Arc`] internally.
170///
171/// ## Create Minio client
172/// ```rust
173/// use minio_rsc::{provider::StaticProvider,Minio};
174/// let provider = StaticProvider::new("minio-access-key-test", "minio-secret-key-test", None);
175/// let minio = Minio::builder()
176///     .host("localhost:9022")
177///     .provider(provider)
178///     .secure(false)
179///     .build()
180///     .unwrap();
181///
182/// ```
183#[derive(Clone)]
184pub struct Minio {
185    inner: Arc<MinioRef>,
186}
187
188struct MinioRef {
189    endpoint: String,
190    virtual_hosted: bool,
191    multi_chunked: bool,
192    secure: bool,
193    client2: reqwest::Client,
194    region: String,
195    agent: HeaderValue,
196    provider: Box<dyn Provider>,
197}
198
199impl Minio {
200    /// get a minio [`MinioBuilder`]
201    pub fn builder() -> MinioBuilder {
202        MinioBuilder::new()
203    }
204
205    /// return whether the minio uses mutli chunked encoding.
206    pub(crate) fn multi_chunked(&self) -> bool {
207        self.inner.multi_chunked
208    }
209
210    pub fn region(&self) -> &str {
211        self.inner.region.as_ref()
212    }
213
214    fn _get_region<T: Into<String>>(&self, bucket_name: Option<T>) -> String {
215        self.inner.region.clone()
216    }
217
218    #[inline]
219    pub(super) async fn fetch_credentials(&self) -> Credentials {
220        self.inner.provider.fetch().await
221    }
222
223    /// Execute HTTP request.
224    async fn _url_open(
225        &self,
226        method: Method,
227        uri: String,
228        headers: HeaderMap,
229        body: Body,
230    ) -> Result<Response> {
231        let request = self
232            .inner
233            .client2
234            .request(method, uri)
235            .headers(headers)
236            .body(body)
237            .send()
238            .await?;
239        Ok(request)
240    }
241
242    #[inline]
243    pub(super) fn scheme(&self) -> &str {
244        if self.inner.secure {
245            "https"
246        } else {
247            "http"
248        }
249    }
250
251    /// build uri for bucket/key
252    ///
253    /// uriencode(key)
254    pub(super) fn _build_uri(&self, bucket: Option<String>, key: Option<String>) -> String {
255        let scheme = self.scheme();
256        let endpoint = self.inner.endpoint.as_str();
257        match bucket {
258            Some(b) => {
259                let mut uri = if self.inner.virtual_hosted {
260                    format!("{scheme}://{b}.{endpoint}")
261                } else {
262                    format!("{scheme}://{endpoint}/{b}",)
263                };
264                if let Some(key) = key {
265                    uri.push('/');
266                    uri.push_str(&urlencode(&key, true));
267                }
268                uri
269            }
270            None => format!("{scheme}://{endpoint}"),
271        }
272    }
273
274    pub async fn _execute<B: Into<Data<crate::error::Error>>>(
275        &self,
276        method: Method,
277        region: &str,
278        bucket_name: Option<String>,
279        object_name: Option<String>,
280        data: B,
281        headers: Option<HeaderMap>,
282        query_params: Option<String>,
283    ) -> Result<Response> {
284        // check bucket_name
285        if let Some(bucket_name) = &bucket_name {
286            check_bucket_name(bucket_name)?;
287        }
288        // check object_name
289        if let Some(object_name) = &object_name {
290            if object_name.is_empty() {
291                Err(ValueError::from("Object name cannot be empty."))?
292            }
293            if bucket_name.is_none() {
294                Err(ValueError::from("Miss bucket name."))?
295            }
296        }
297        // build uri
298        let uri = self._build_uri(bucket_name, object_name);
299
300        // add query to uri
301        let uri = if let Some(query) = query_params {
302            format!("{}?{}", uri, query)
303        } else {
304            uri
305        };
306        let mut data = data.into();
307        if !self.inner.multi_chunked {
308            data = data.convert().await?;
309        }
310        let mut headers = headers.unwrap_or(HeaderMap::new());
311        headers.insert(header::USER_AGENT, self.inner.agent.clone());
312        let credentials = self.fetch_credentials().await;
313        let uri = Uri::from_str(&uri).map_err(|e| Error::ValueError(e.to_string()))?;
314        let (uri, body) = sign_request_v4(
315            &method,
316            &uri,
317            &mut headers,
318            region,
319            data,
320            credentials.access_key(),
321            credentials.secret_key(),
322        )?;
323        self._url_open(method, uri, headers, body).await
324    }
325
326    #[inline]
327    pub fn executor(&self, method: Method) -> super::BaseExecutor {
328        super::BaseExecutor::new(method, self)
329    }
330
331    /// Instantiate an [Bucket]
332    pub fn bucket<B>(&self, bucket: B) -> Bucket
333    where
334        B: Into<BucketArgs>,
335    {
336        Bucket {
337            client: self.clone(),
338            bucket: bucket.into(),
339        }
340    }
341}