xt_oss/
oss.rs

1pub const BASE_URL: &str = "aliyuncs.com";
2pub const DEFAULT_REGION: &str = "oss-cn-hangzhou";
3pub const USER_AGENT: &str = "xt oss/0.1";
4pub const DEFAULT_CONTENT_TYPE: &str = "application/octet-stream";
5pub const DEFAULT_CONNECT_TIMEOUT: u64 = 180;
6pub const DEFAULT_TIMEOUT: u64 = 60;
7pub const GMT_DATE_FMT: &str = "%a, %d %b %Y %H:%M:%S GMT";
8pub const XML_CONTENT: &str = r#"<?xml version="1.0" encoding="UTF-8"?>"#;
9
10pub use bytes::{Bytes, BytesMut};
11use std::time::Duration;
12pub mod api;
13pub(super) mod auth;
14pub mod entities;
15pub mod http;
16
17use super::oss::{
18    self,
19    http::header::{AUTHORIZATION, CONTENT_TYPE, DATE},
20};
21use chrono::Utc;
22use reqwest::{header::HeaderMap, Response, Result};
23
24pub struct RequestTask<'a> {
25    request: &'a oss::Request<'a>,
26    url: &'a str,
27    resource: Option<&'a str>,
28    method: http::Method,
29    headers: http::HeaderMap,
30    body: Bytes,
31}
32
33impl<'a> RequestTask<'a> {
34    pub(crate) fn new(request: &'a oss::Request<'a>) -> Self {
35        Self {
36            request,
37            url: Default::default(),
38            resource: None,
39            method: http::Method::GET,
40            headers: http::HeaderMap::new(),
41            body: Bytes::new(),
42        }
43    }
44
45    pub fn with_url(mut self, value: &'a str) -> Self {
46        self.url = value;
47        self
48    }
49
50    pub fn with_resource(mut self, value: &'a str) -> Self {
51        self.resource = Some(value);
52        self
53    }
54
55    pub fn with_headers(mut self, value: http::HeaderMap) -> Self {
56        self.headers = value;
57        self
58    }
59
60    pub fn with_method(mut self, value: http::Method) -> Self {
61        self.method = value;
62        self
63    }
64
65    pub fn with_body(mut self, value: Bytes) -> Self {
66        self.body = value;
67        self
68    }
69
70    pub async fn execute(&self) -> oss::Result<Response> {
71        self.inner_execute(None).await
72    }
73
74    pub async fn execute_timeout(&self, value: u64) -> oss::Result<Response> {
75        self.inner_execute(Some(value)).await
76    }
77
78    fn authorization(&self, headers: &HeaderMap, date: &String) -> String {
79        let access_key_id = self.request.access_key_id.unwrap_or_default();
80        let access_key_secret = self.request.access_key_secret.unwrap_or_default();
81        let sts_token = self.request.sts_token;
82        let resourse = self.resource;
83        auth::SingerV1 {
84            access_key_id,
85            access_key_secret,
86            sts_token,
87            headers: &headers,
88            method: &self.method,
89            date: &date,
90            resourse,
91        }
92        .complute()
93    }
94
95    async fn inner_execute(&self, timeout: Option<u64>) -> oss::Result<Response> {
96        let date = Utc::now().format(oss::GMT_DATE_FMT).to_string();
97        let mut headers = http::HeaderMap::new();
98        headers.insert(DATE, date.parse().unwrap());
99        if let Some(sts_token) = self.request.sts_token {
100            headers.insert("x-oss-security-token", sts_token.parse().unwrap());
101        }
102        headers.extend(self.headers.to_owned());
103        let auth = self.authorization(&headers, &date);
104        headers.insert(AUTHORIZATION, auth.parse().unwrap());
105        // dbg!(&headers);
106        let timeout = Duration::from_secs(timeout.unwrap_or(oss::DEFAULT_TIMEOUT));
107        self.request
108            .client
109            .request(self.method.to_owned(), self.url)
110            .headers(headers)
111            .timeout(timeout)
112            .body(self.body.to_owned())
113            .send()
114            .await
115    }
116}
117
118#[derive(Debug, Default, Clone)]
119pub struct Request<'a> {
120    access_key_id: Option<&'a str>,
121    access_key_secret: Option<&'a str>,
122    sts_token: Option<&'a str>,
123    client: reqwest::Client,
124}
125
126impl<'a> Request<'a> {
127    pub fn new() -> Self {
128        let mut headers = http::HeaderMap::new();
129        headers.insert(
130            CONTENT_TYPE,
131            http::HeaderValue::from_static(DEFAULT_CONTENT_TYPE),
132        );
133        let client = reqwest::Client::builder()
134            .default_headers(headers)
135            .user_agent(oss::USER_AGENT)
136            .connect_timeout(Duration::from_secs(DEFAULT_CONNECT_TIMEOUT))
137            .build()
138            .unwrap();
139        Self {
140            client,
141            ..Self::default()
142        }
143    }
144
145    pub fn with_access_key_id(mut self, value: &'a str) -> Self {
146        self.access_key_id = Some(value);
147        self
148    }
149
150    pub fn with_access_key_secret(mut self, value: &'a str) -> Self {
151        self.access_key_secret = Some(value);
152        self
153    }
154
155    pub fn with_sts_token(mut self, value: Option<&'a str>) -> Self {
156        self.sts_token = value;
157        self
158    }
159
160    pub fn task(&self) -> RequestTask<'_> {
161        RequestTask::new(&self)
162    }
163}
164
165#[derive(Debug, Clone, Default, Copy)]
166pub struct Options<'a> {
167    /// 通过阿里云控制台创建的AccessKey ID
168    access_key_id: &'a str,
169    /// 通过阿里云控制台创建的AccessKey Secret
170    access_key_secret: &'a str,
171    /// 使用临时授权方式
172    sts_token: &'a str,
173    /// 通过控制台或PutBucket创建的Bucket
174    bucket: &'a str,
175    /// OSS访问域名。
176    endpoint: &'a str,
177    /// Bucket所在的区域,默认值为oss-cn-hangzhou
178    region: &'a str,
179    /// 是否使用阿里云内网访问,默认值为false
180    internal: bool,
181    /// 是否支持上传自定义域名,默认值为false
182    cname: bool,
183    // /// Bucket是否开启请求者付费模,默认值为false
184    // is_request_pay: bool,
185    /// 设置secure为true,则使用HTTPS;设置secure为false,则使用HTTP
186    secure: bool,
187    /// 超时时间,默认值为60秒
188    timeout: u64,
189}
190
191impl<'a> Options<'a> {
192    pub fn new() -> Self {
193        Self {
194            region: oss::DEFAULT_REGION,
195            internal: false,
196            cname: false,
197            // is_request_pay: false,
198            secure: false,
199            timeout: 60u64,
200            ..Self::default()
201        }
202    }
203
204    pub fn with_access_key_id(mut self, value: &'a str) -> Self {
205        self.access_key_id = value;
206        self
207    }
208
209    pub fn with_access_key_secret(mut self, value: &'a str) -> Self {
210        self.access_key_secret = value;
211        self
212    }
213
214    pub fn with_bucket(mut self, value: &'a str) -> Self {
215        self.bucket = value;
216        self
217    }
218
219    pub fn with_region(mut self, value: &'a str) -> Self {
220        self.region = value;
221        self
222    }
223
224    pub fn with_sts_token(mut self, value: &'a str) -> Self {
225        self.sts_token = value;
226        self
227    }
228
229    pub fn with_endpoint(mut self, value: &'a str) -> Self {
230        self.endpoint = if let Some(v) = value.strip_prefix("http://") {
231            v
232        } else if let Some(v) = value.strip_prefix("https://") {
233            v
234        } else {
235            value
236        };
237        self
238    }
239
240    pub fn with_internal(mut self, value: bool) -> Self {
241        self.internal = value;
242        self
243    }
244
245    pub fn with_cname(mut self, value: bool) -> Self {
246        self.cname = value;
247        self
248    }
249
250    // pub fn with_is_request_pay(mut self, value: bool) -> Self {
251    //     self.is_request_pay = value;
252    //     self
253    // }
254
255    pub fn with_secret(mut self, value: bool) -> Self {
256        self.secure = value;
257        self
258    }
259    pub fn with_timeout(mut self, value: u64) -> Self {
260        self.timeout = value;
261        self
262    }
263
264    pub fn root_url(&self) -> String {
265        format!(
266            "{}://{}{}.{}",
267            self.schema(),
268            oss::DEFAULT_REGION,
269            if self.internal == true {
270                "-internal"
271            } else {
272                ""
273            },
274            oss::BASE_URL
275        )
276    }
277
278    pub fn base_url(&self) -> String {
279        if self.internal == true {
280            format!("{}://{}.{}", self.schema(), self.bucket, self.host())
281        } else if self.cname == true {
282            format!("{}://{}", self.schema(), self.host())
283        } else {
284            if self.bucket.is_empty() {
285                panic!("Bucket parameter must be provided.");
286            }
287            format!("{}://{}.{}", self.schema(), self.bucket, self.host())
288        }
289    }
290
291    pub fn object_url(&self, object: &'a str) -> String {
292        format!("{}/{}", self.base_url(), object)
293    }
294
295    fn schema(&self) -> String {
296        match self.secure {
297            true => "https".to_string(),
298            false => "http".to_string(),
299        }
300    }
301
302    // 当`cname`为true时,`endpoint`,`bucket`为必填,否则产生panic错误.
303    // 当internal为true时,忽略cname与endpoint
304    // 无论是否使用cname正确的设置region(location)与bucket
305    fn host(&self) -> String {
306        if self.internal == true {
307            format!(
308                "{}{}.{}",
309                self.region,
310                if self.internal { "-internal" } else { "" },
311                oss::BASE_URL
312            )
313        } else if self.cname == true {
314            if self.endpoint.is_empty() {
315                panic!("Endpoint parameter must be provided.");
316            }
317            self.endpoint.to_string()
318        } else {
319            format!("{}.{}", self.region, oss::BASE_URL)
320        }
321    }
322
323    pub fn client(self) -> oss::Client<'a> {
324        oss::Client::new(self)
325    }
326}
327
328#[derive(Debug, Default, Clone)]
329pub struct Client<'a> {
330    options: Options<'a>,
331    request: Request<'a>,
332}
333
334impl<'a> Client<'a> {
335    pub fn new(options: Options<'a>) -> Self {
336        let request = self::Request::new()
337            .with_access_key_id(options.access_key_id)
338            .with_access_key_secret(options.access_key_secret)
339            .with_sts_token((!options.sts_token.is_empty()).then_some(options.sts_token));
340        Self { options, request }
341    }
342
343    pub fn options(&self) -> &Options {
344        &self.options
345    }
346
347    pub fn region(&self) -> &'a str {
348        self.options.region
349    }
350
351    pub fn bucket(&self) -> &'a str {
352        self.options.bucket
353    }
354
355    pub fn root_url(&self) -> String {
356        self.options.root_url()
357    }
358
359    pub fn base_url(&self) -> String {
360        self.options.base_url()
361    }
362
363    pub fn object_url(&self, object: &'a str) -> String {
364        self.options.object_url(object)
365    }
366
367    pub fn timeout(&self) -> u64 {
368        self.options.timeout
369    }
370}
371
372#[cfg(test)]
373pub mod tests {
374    use crate::oss;
375
376    #[test]
377    fn options_new_normal_1() {
378        let options = oss::Options::new()
379            .with_access_key_id("access_key_id")
380            .with_access_key_secret("access_key_secret")
381            .with_region("oss-cn-shanghai")
382            .with_endpoint("cdn.xuetube.com")
383            .with_bucket("xuetube")
384            .with_cname(true)
385            .with_internal(true)
386            .with_secret(true);
387        assert_eq!(
388            options.root_url(),
389            "https://oss-cn-hangzhou-internal.aliyuncs.com"
390        );
391        assert_eq!(
392            options.base_url(),
393            "https://xuetube.oss-cn-shanghai-internal.aliyuncs.com"
394        );
395    }
396
397    #[test]
398    fn options_new_normal_2() {
399        let options = oss::Options::new()
400            .with_access_key_id("access_key_id")
401            .with_access_key_secret("access_key_secret")
402            .with_region("oss-cn-shanghai")
403            .with_bucket("xtoss-ex")
404            .with_secret(true)
405            .with_internal(false);
406
407        let host = "oss-cn-shanghai.aliyuncs.com";
408        let root_url = "https://oss-cn-hangzhou.aliyuncs.com";
409        let base_url = "https://xtoss-ex.oss-cn-shanghai.aliyuncs.com";
410
411        assert_eq!(options.host(), host);
412        assert_eq!(options.root_url(), root_url);
413        assert_eq!(options.base_url(), base_url);
414    }
415
416    #[test]
417    fn options_new_endpoint() {
418        let options = oss::Options::new()
419            .with_access_key_id("access_key_id")
420            .with_access_key_secret("access_key_secret")
421            .with_bucket("xtoss-ex1")
422            .with_cname(true)
423            .with_endpoint("https://cdn.xuetube.com")
424            .with_internal(false)
425            .with_region("oss-cn-shanghai")
426            .with_secret(true)
427            // .with_sts_token("sts token")
428            .with_timeout(60);
429
430        let host = "cdn.xuetube.com";
431        let root_url = "https://oss-cn-hangzhou.aliyuncs.com";
432        let base_url = "https://cdn.xuetube.com";
433
434        assert_eq!(options.host(), host);
435        assert_eq!(options.root_url(), root_url);
436        assert_eq!(options.base_url(), base_url);
437    }
438}