oss_sdk_rs/
oss.rs

1//! Copyright The NoXF/oss-rust-sdk Authors
2use chrono::prelude::*;
3use reqwest::Client;
4use reqwest::{
5    header::{HeaderMap, DATE},
6    StatusCode,
7};
8use std::borrow::Cow;
9use std::collections::HashMap;
10use std::str;
11use std::time::{Duration, SystemTime};
12use super::auth::*;
13use super::errors::OSSError;
14use super::utils::*;
15
16const RESOURCES: [&str; 50] = [
17    "acl",
18    "uploads",
19    "location",
20    "cors",
21    "logging",
22    "website",
23    "referer",
24    "lifecycle",
25    "delete",
26    "append",
27    "tagging",
28    "objectMeta",
29    "uploadId",
30    "partNumber",
31    "security-token",
32    "position",
33    "img",
34    "style",
35    "styleName",
36    "replication",
37    "replicationProgress",
38    "replicationLocation",
39    "cname",
40    "bucketInfo",
41    "comp",
42    "qos",
43    "live",
44    "status",
45    "vod",
46    "startTime",
47    "endTime",
48    "symlink",
49    "x-oss-process",
50    "response-content-type",
51    "response-content-language",
52    "response-expires",
53    "response-cache-control",
54    "response-content-disposition",
55    "response-content-encoding",
56    "udf",
57    "udfName",
58    "udfImage",
59    "udfId",
60    "udfImageDesc",
61    "udfApplication",
62    "comp",
63    "udfApplicationLog",
64    "restore",
65    "callback",
66    "callback-var",
67];
68
69#[derive(Clone, Debug)]
70pub struct OSS<'a> {
71    key_id: Cow<'a, str>,
72    key_secret: Cow<'a, str>,
73    endpoint: Cow<'a, str>,
74    bucket: Cow<'a, str>,
75
76    pub(crate) http_client: Client,
77}
78
79#[derive(Default)]
80pub struct Options {
81    pub pool_max_idle_per_host: Option<usize>,
82    pub timeout: Option<Duration>,
83}
84
85impl<'a> OSS<'a> {
86    pub fn new<S>(key_id: S, key_secret: S, endpoint: S, bucket: S) -> Self
87    where
88        S: Into<Cow<'a, str>>,
89    {
90        Self::new_with_opts(key_id, key_secret, endpoint, bucket, Default::default())
91    }
92
93    pub fn new_with_opts<S>(key_id: S, key_secret: S, endpoint: S, bucket: S, opts: Options) -> Self
94    where
95        S: Into<Cow<'a, str>>,
96    {
97        let mut builder = Client::builder();
98        if let Some(timeout) = opts.timeout {
99            builder = builder.timeout(timeout);
100        }
101        if let Some(max_per_host) = opts.pool_max_idle_per_host {
102            builder = builder.pool_max_idle_per_host(max_per_host);
103        }
104
105        let http_client = builder.build().expect("Build http client failed");
106        OSS {
107            key_id: key_id.into(),
108            key_secret: key_secret.into(),
109            endpoint: endpoint.into(),
110            bucket: bucket.into(),
111            http_client,
112        }
113    }
114
115    pub fn bucket(&self) -> &str {
116        &self.bucket
117    }
118
119    pub fn endpoint(&self) -> &str {
120        &self.endpoint
121    }
122
123    pub fn key_id(&self) -> &str {
124        &self.key_id
125    }
126
127    pub fn key_secret(&self) -> &str {
128        &self.key_secret
129    }
130
131    pub fn set_bucket(&mut self, bucket: &'a str) {
132        self.bucket = bucket.into()
133    }
134
135    pub fn host(&self, bucket: &str, object: &str, resources_str: &str) -> String {
136        if self.endpoint.starts_with("https") {
137            format!(
138                "https://{}.{}/{}?{}",
139                bucket,
140                self.endpoint.replacen("https://", "", 1),
141                object,
142                resources_str
143            )
144        } else {
145            format!(
146                "http://{}.{}/{}?{}",
147                bucket,
148                self.endpoint.replacen("http://", "", 1),
149                object,
150                resources_str
151            )
152        }
153    }
154
155    pub fn date(&self) -> String {
156        let now: DateTime<Utc> = Utc::now();
157        now.format("%a, %d %b %Y %T GMT").to_string()
158    }
159
160    pub fn get_resources_str<S>(&self, params: &HashMap<S, Option<S>>) -> String
161    where
162        S: AsRef<str>,
163    {
164        let mut resources: Vec<(&S, &Option<S>)> = params
165            .iter()
166            .filter(|(k, _)| RESOURCES.contains(&k.as_ref()))
167            .collect();
168        resources.sort_by(|a, b| a.0.as_ref().to_string().cmp(&b.0.as_ref().to_string()));
169        let mut result = String::new();
170        for (k, v) in resources {
171            if !result.is_empty() {
172                result += "&";
173            }
174            if let Some(vv) = v {
175                result += &format!("{}={}", k.as_ref().to_owned(), vv.as_ref());
176            } else {
177                result += k.as_ref();
178            }
179        }
180        result
181    }
182
183    pub fn get_params_str<S>(&self, params: &HashMap<S, Option<S>>) -> String
184    where
185        S: AsRef<str>,
186    {
187        let mut resources: Vec<(&S, &Option<S>)> = params.iter().collect();
188        resources.sort_by(|a, b| a.0.as_ref().to_string().cmp(&b.0.as_ref().to_string()));
189        let mut result = String::new();
190        for (k, v) in resources {
191            if !result.is_empty() {
192                result += "&";
193            }
194            if let Some(vv) = v {
195                result += &format!("{}={}", k.as_ref().to_owned(), vv.as_ref());
196            } else {
197                result += k.as_ref();
198            }
199        }
200        result
201    }
202
203    /// Build a request. Return url and header for reqwest client builder.
204    pub fn build_request<S1, S2, H, R>(
205        &self,
206        req_type: RequestType,
207        object_name: S1,
208        headers: H,
209        resources: R,
210    ) -> Result<(String, HeaderMap), OSSError>
211    where
212        S1: AsRef<str>,
213        S2: AsRef<str>,
214        H: Into<Option<HashMap<S2, S2>>>,
215        R: Into<Option<HashMap<S2, Option<S2>>>>,
216    {
217        let object_name = object_name.as_ref();
218        let (resources_str, params_str) = if let Some(r) = resources.into() {
219            (self.get_resources_str(&r), self.get_params_str(&r))
220        } else {
221            (String::new(), String::new())
222        };
223
224        let host = self.host(self.bucket(), object_name, &params_str);
225        let date = self.date();
226        let mut headers = if let Some(h) = headers.into() {
227            to_headers(h)?
228        } else {
229            HeaderMap::new()
230        };
231        headers.insert(DATE, date.parse()?);
232        let authorization = self.oss_sign(
233            req_type.as_str(),
234            self.bucket(),
235            object_name,
236            &resources_str,
237            &headers,
238        )?;
239        headers.insert("Authorization", authorization.parse()?);
240
241        Ok((host, headers))
242    }
243}
244
245pub enum RequestType {
246    Get,
247    Put,
248    Delete,
249    Head,
250    Post,
251}
252
253impl RequestType {
254    fn as_str(&self) -> &str {
255        match self {
256            RequestType::Get => "GET",
257            RequestType::Put => "PUT",
258            RequestType::Delete => "DELETE",
259            RequestType::Head => "HEAD",
260            RequestType::Post => "POST",
261        }
262    }
263}
264
265#[derive(Debug)]
266pub struct ObjectMeta {
267    /// The last modified time
268    pub last_modified: SystemTime,
269    /// The size in bytes of the object
270    pub size: usize,
271    /// 128-bits RFC 1864 MD5. This field only presents in normal file. Multipart and append-able file will have empty md5.
272    pub md5: String,
273
274    pub mime_type:String,
275}
276
277impl ObjectMeta {
278    pub fn from_header_map(header: &HeaderMap) -> Result<Self, OSSError> {
279        let getter = |key: &str| -> Result<&str, OSSError> {
280            let value = header
281                .get(key)
282                .ok_or_else(|| OSSError::Object {
283                    status_code: StatusCode::BAD_REQUEST,
284                    message: format!(
285                        "can not find {} in head response, response header: {:?}",
286                        key, header
287                    ),
288                })?
289                .to_str()
290                .map_err(|_| OSSError::Object {
291                    status_code: StatusCode::BAD_REQUEST,
292                    message: format!("header entry {} contains invalid ASCII code", key),
293                })?;
294            Ok(value)
295        };
296
297        let last_modified = httpdate::parse_http_date(getter("Last-Modified")?).map_err(|e| {
298            OSSError::Object{
299                status_code: StatusCode::BAD_REQUEST,
300                message: format!("cannot parse to system time: {}", e)
301            }
302        })?;
303        let size = getter("Content-Length")?.parse().map_err(|e| {
304            OSSError::Object{
305                status_code: StatusCode::BAD_REQUEST,
306                message: format!("cannot parse to number: {}", e)
307            }
308        })?;
309        let md5 = getter("Content-Md5")?.to_string();
310
311        let mime_type = getter("Content-Type")?.to_string();
312        Ok(Self {
313            last_modified,
314            size,
315            md5,
316            mime_type,
317        })
318    }
319}