oss_rust_sdk/
oss.rs

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