Skip to main content

obs_sdk/
lib.rs

1//! # 使用说明
2//! 
3//! ## 1. 列举桶内对象列表
4//! 
5//! ```rust
6//! use obs_sdk::{ObsClient, ObsError};
7//! 
8//! static ENDPOINT: &str = "obs.cn-north-4.myhuaweicloud.com";
9//! static AK: &str = "YOUR_AK";
10//! static SK: &str = "YOUR_SK";
11//! static BUCKET_NAME: &str = "bucket_name";
12//! 
13//! #[tokio::test]
14//! async fn test_list_prefix() -> Result<(), ObsError> {
15//!     let client = ObsClient {
16//!         endpoint: ENDPOINT.to_string(),
17//!         ak: AK.to_string(),
18//!         sk: SK.to_string(),
19//!         bucket: BUCKET_NAME.to_string(),
20//!     };
21//!     let res = client.list("tmp").await?;
22//!     println!("{:?}", res);
23//!     Ok(())
24//! }
25//! ```
26//! 
27//! ## 2. 上传对象到桶
28//! 
29//! ```rust
30//! use obs_sdk::{ObsClient, ObsError};
31//! 
32//! static ENDPOINT: &str = "obs.cn-north-4.myhuaweicloud.com";
33//! static AK: &str = "YOUR_AK";
34//! static SK: &str = "YOUR_SK";
35//! static BUCKET_NAME: &str = "bucket_name";
36//! 
37//! #[tokio::test]
38//! async fn test_upload_object() -> Result<(), ObsError> {
39//!     let client = ObsClient {
40//!         endpoint: ENDPOINT.to_string(),
41//!         ak: AK.to_string(),
42//!         sk: SK.to_string(),
43//!         bucket: BUCKET_NAME.to_string(),
44//!     };
45//!     let res = client.upload_file("tmp_cargo.txt", "Cargo.txt").await?;
46//!     println!("{:?}", res);
47//!     Ok(())
48//! }
49//! ```
50//! 
51//! ## 3. 下载对象到本地目录
52//! 
53//! ```rust
54//! use obs_sdk::{ObsClient, ObsError};
55//! 
56//! static ENDPOINT: &str = "obs.cn-north-4.myhuaweicloud.com";
57//! static AK: &str = "YOUR_AK";
58//! static SK: &str = "YOUR_SK";
59//! static BUCKET_NAME: &str = "bucket_name";
60//! 
61//! #[tokio::test]
62//! async fn test_download_file02() -> Result<(), ObsError> {
63//!     let client = ObsClient {
64//!         endpoint: ENDPOINT.to_string(),
65//!         ak: AK.to_string(),
66//!         sk: SK.to_string(),
67//!         bucket: BUCKET_NAME.to_string(),
68//!     };
69//!     let res = client.download_file("2hls_stutter-10.mp4", "video/2hls_stutter-10.mp4", false).await;
70//!     res
71//! }
72//! ```
73//! 
74//! ## 4. 下载对象为字节内容
75//! 
76//! ```rust
77//! use obs_sdk::{ObsClient, ObsError};
78//! 
79//! static ENDPOINT: &str = "obs.cn-north-4.myhuaweicloud.com";
80//! static AK: &str = "YOUR_AK";
81//! static SK: &str = "YOUR_SK";
82//! static BUCKET_NAME: &str = "bucket_name";
83//! 
84//! #[tokio::test]
85//! async fn test_download_file01() -> Result<(), ObsError> {
86//!     let client = ObsClient {
87//!         endpoint: ENDPOINT.to_string(),
88//!         ak: AK.to_string(),
89//!         sk: SK.to_string(),
90//!         bucket: BUCKET_NAME.to_string(),
91//!     };
92//!     let data = client.download_object("2hls_stutter-10.mp4").await?;
93//!     let file_path = Path::new("output.mp4");
94//!     match fs::write(file_path, data) {
95//!         Ok(_) => println!("文件保存成功{:?}", file_path),
96//!         Err(e) => eprintln!("文件保存失败:{}", e)
97//!     }
98//!     Ok(())
99//! }
100//! ```
101//! 
102//! ## 5. url鉴权
103//! 
104//! ```rust
105//! use obs_sdk::{ObsClient, ObsError};
106//!
107//! static ENDPOINT: &str = "obs.cn-north-4.myhuaweicloud.com";
108//! static AK: &str = "YOUR_AK";
109//! static SK: &str = "YOUR_SK";
110//! static BUCKET_NAME: &str = "bucket_name";
111//! 
112//! #[test]
113//! fn test_url_sign() -> Result<(), ObsError> {
114//!     let client = ObsClient {
115//!         endpoint: ENDPOINT.to_string(),
116//!         ak: AK.to_string(),
117//!         sk: SK.to_string(),
118//!         bucket: BUCKET_NAME.to_string(),
119//!     };
120//!     let sign_url = client.url_sign("https://ranfs.obs.cn-north-4.myhuaweicloud.com/tmp_cargo.txt")?;
121//!     println!("sign_url = {}", sign_url);
122//!     Ok(())
123//! }
124//! ```
125//! 
126mod utils;
127mod algorithm;
128
129use algorithm::HmacSha1;
130use std::{fmt, fs, io};
131use std::path::Path;
132use serde::{Serialize, Deserialize};
133use regex::Regex;
134use std::vec::Vec;
135use url::{Url, form_urlencoded};
136use chrono::Local;
137use reqwest::StatusCode;
138use urlencoding::encode;
139
140/// 华为云OBS客户端
141/// 
142pub struct ObsClient {
143    pub endpoint: String,
144    pub ak: String,
145    pub sk: String,
146    pub bucket: String,
147}
148
149pub enum ObsError {
150    Common(Box<dyn core::error::Error + Send + Sync + 'static>),
151}
152
153// 兼容世界上所有的标准错误
154impl <E> From<E> for ObsError
155where
156    E: core::error::Error + Send + Sync + 'static
157{
158    fn from(err: E) -> ObsError {
159        ObsError::Common(Box::new(err))
160    }
161}
162
163impl fmt::Debug for ObsError {
164    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
165        let mut builder = f.debug_struct("obs-sdk::Error");
166        builder.finish()
167    }
168}
169
170impl fmt::Display for ObsError {
171    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
172        match self {
173            ObsError::Common(e) => f.write_str("obs sdk error")?,
174        };
175        Ok(())
176    }
177}
178
179
180
181impl ObsClient {
182
183    /// 列举指定前缀开头的所有对象元数据,方法内部会构造http请求:
184    /// ```plain
185    /// GET / HTTP/1.1
186    /// Host: bucketname.obs.cn-north-4.myhuaweicloud.com
187    /// Date: date
188    /// Authorization: authorization
189    /// ```
190    /// 
191    pub async fn list(&self, prefix: &str) -> Result<Vec<ObjectMeta>, ObsError> {
192        // 构造完整的url地址
193        let url = format!("https://{}.{}/?prefix={}", self.bucket, self.endpoint, prefix);
194
195        //  获取GMT格式的时间字符串
196        let date = utils::now_str_gmt();
197
198        // 创建HmacSha1对象
199        let hmacsha1 = HmacSha1();
200
201        // 构造签名用的原始字符串
202        let string_to_sign = hmacsha1.header_string_to_sign("GET", "", "", &date, "",  &format!("/{}/", self.bucket));
203
204        // 根据原始字符串+ak,获取header签名
205        let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
206
207        // 构造请求头Authorization的值
208        let authorization = format!("OBS {}:{}", self.ak, signature);
209        // 构造http请求
210        let client = reqwest::Client::new();
211        let res = client.get(url)
212            .header("Date", &date)
213            .header("Authorization", &authorization)
214            .send()
215            .await?;
216
217        // 如果请求成功,则返回字节内容
218        if res.status().is_success() {
219            let xml_content_string = res.text().await?;
220            let results = XmlParser::new(&xml_content_string).parse();
221            return Ok(results);
222        }
223
224        Err(ObsError::Common(Box::new(io::Error::new(io::ErrorKind::Other, format!("请求失败,状态码={}", res.status())))))
225    }
226
227    /// 上传对象
228    /// 
229    /// 方法内部构建请求
230    /// ```plain
231    /// PUT /object01 HTTP/1.1
232    /// User-Agent: curl/7.29.0
233    /// Host: examplebucket.obs.cn-north-4.myhuaweicloud.com
234    /// Accept: */*
235    /// Date: WED, 01 Jul 2015 04:11:15 GMT
236    /// Authorization: OBS H4IPJX0TQTHTHEBQQCEC:gYqplLq30dEX7GMi2qFWyjdFsyw=
237    /// Content-Length: 10240
238    /// Expect: 100-continue
239    /// 
240    /// [1024 Byte data content]
241    /// ```
242    /// 
243    pub async fn upload_object(&self, obj_key: &str, data: Vec<u8>) -> Result<(), ObsError> {
244        let obj_key = &Self::urlencode(obj_key);
245        // 构造完整的url地址
246        let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
247
248        let md5_string = utils::base64_md5_str(&data);
249
250        //  获取GMT格式的时间字符串
251        let date = utils::now_str_gmt();
252
253        // 创建HmacSha1对象
254        let hmacsha1 = HmacSha1();
255
256        let file_type = &utils::get_mime_type_from_extension(obj_key)
257            .unwrap_or(String::from("application/octet-stream"));
258
259        // 构造签名用的原始字符串
260        let string_to_sign = hmacsha1.header_string_to_sign("PUT", &md5_string, file_type, &date, "",  &format!("/{}/{}", self.bucket, obj_key));
261
262        // 根据原始字符串+ak,获取header签名
263        let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
264
265        // 构造请求头Authorization的值
266        let authorization = format!("OBS {}:{}", self.ak, signature);
267
268        // 构造http请求
269        let client = reqwest::Client::new();
270        let res = client.put(url)
271            .header("Content-MD5", &md5_string)
272            .header("Date", &date)
273            .header("Content-Type", file_type)
274            .header("Content-Length", data.len())
275            .header("Authorization", authorization)
276            .body(data)
277            .send()
278            .await?;
279        let _status: StatusCode = res.status();
280        Ok(())
281
282    }
283
284    /// 上传文件
285    pub async fn upload_file(&self, obj_key: &str, file_path: &str) -> Result<(), ObsError> {
286        let data = fs::read(file_path)?;
287        self.upload_object(obj_key, data).await
288    }
289
290    /// 下载对象,方法内部会构造http请求:
291    /// ```plain
292    /// GET /{obj_key} HTTP/1.1
293    /// Host: {bucket}.obs.cn-north-4.myhuaweicloud.com
294    /// Date: {date}
295    /// ```
296    /// 
297    pub async fn download_object(&self, obj_key: &str) -> Result<Vec<u8>, ObsError> {
298        let obj_key = &Self::urlencode(obj_key);
299        // 构造完整的url地址
300        let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
301
302        //  获取GMT格式的时间字符串
303        let date = utils::now_str_gmt();
304
305        // 创建HmacSha1对象
306        let hmacsha1 = HmacSha1();
307
308        // 构造签名用的原始字符串
309        let string_to_sign = hmacsha1.header_string_to_sign("GET", "", "", &date, "",  &format!("/{}/{}", self.bucket, obj_key));
310
311        // 根据原始字符串+ak,获取header签名
312        let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
313
314        // 构造请求头Authorization的值
315        let authorization = format!("OBS {}:{}", self.ak, signature);
316
317        // 构造http请求
318        let client = reqwest::Client::new();
319        let res = client.get(url)
320            .header("Authorization", &authorization)
321            .header("Date", &date)
322            .send()
323            .await?;
324
325        // 如果请求成功,则返回字节内容
326        if res.status().is_success() {
327            return Ok(res.bytes().await?.to_vec());
328        }
329        
330        Err(ObsError::Common(Box::new(io::Error::new(io::ErrorKind::Other, format!("请求失败,状态码={}", res.status())))))
331    }
332
333    /// 删除obs上的对象
334    pub async fn delete_object(&self, obj_key: &str) -> Result<(), ObsError> {
335        let obj_key = &Self::urlencode(obj_key);
336        // 构造完整的url地址
337        let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
338
339        let md5_string = "";
340
341        //  获取GMT格式的时间字符串
342        let date = utils::now_str_gmt();
343
344        // 创建HmacSha1对象
345        let hmacsha1 = HmacSha1();
346
347        // 构造签名用的原始字符串
348        let string_to_sign = hmacsha1.header_string_to_sign("DELETE", &md5_string, "", &date, "",  &format!("/{}/{}", self.bucket, obj_key));
349
350        // 根据原始字符串+ak,获取header签名
351        let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
352
353        // 构造请求头Authorization的值
354        let authorization = format!("OBS {}:{}", self.ak, signature);
355
356        // 构造http请求
357        let client = reqwest::Client::new();
358        let res = client.delete(url)
359            .header("Date", &date)
360            .header("Authorization", authorization)
361            .send()
362            .await;
363
364        let res = match res {
365            Ok(response) => response,
366            Err(e) => {
367                return Err(ObsError::Common(Box::new(io::Error::new(io::ErrorKind::Other, e))));
368            },
369        };
370        let status = res.status();
371        println!("status = {}, {}", status, res.text_with_charset("utf-8").await?);
372
373        Ok(())
374    }
375
376    /// 下载文件,并指定本地保存用的文件路径
377    /// 
378    /// # 参数
379    /// 
380    /// `overwrite` - 是否覆盖,true,当文件存在时,覆盖文件,false,当文件存在时,不覆盖文件
381    /// 
382    pub async fn download_file(&self, obj_key: &str, file_path: &str, overwrite: bool) -> Result<(), ObsError> {
383        let file_path = Path::new(file_path);
384
385        // 判断文件是否存在,如果存在,不做任何操作
386        if file_path.exists() && !overwrite {
387            return Err(ObsError::Common(Box::new(io::Error::new(io::ErrorKind::AlreadyExists, "文件已存在,请删除文件或设置覆盖参数"))));
388        }
389
390        // 根据父目录是否存在,选择性创建父目录
391        let parent = file_path.parent().unwrap();
392        if !parent.exists() {
393            fs::create_dir_all(&parent)?;
394        }
395
396        // 下载文件,得到原始文件字节内容
397        let data = self.download_object(obj_key).await?;
398        
399        // 保存文件
400        fs::write(file_path, data)?;
401        Ok(())
402    }
403
404    pub fn url_sign(&self, url_str: &str) -> Result<String, ObsError> {
405        let obs_object_url = Url::parse(url_str)?;
406        let resource_part = obs_object_url.path();
407        let host = obs_object_url.host().unwrap();
408        let domain = match host {
409            url::Host::Domain(domain) => domain.to_string(),
410            _ => format!("{}.{}", self.bucket, self.endpoint)
411        };
412        let parts: Vec<&str> = domain.split(".").collect();
413        let bucket_name = parts[0];
414
415        let timestamp = utils::timestamp(Local::now(), 3600*2);
416
417        //  获取GMT格式的时间字符串
418        let expires = format!("{}", timestamp);
419
420        // 创建HmacSha1对象
421        let hmacsha1 = HmacSha1();
422
423        // 构造签名用的原始字符串
424        let string_to_sign = hmacsha1.url_string_to_sign("GET", "", "", &expires, "",  &format!("/{}{}", bucket_name, resource_part));
425
426        // 根据原始字符串+ak,获取header签名
427        let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
428        let signature = form_urlencoded::byte_serialize(signature.as_bytes()).collect::<String>();
429
430        // 构造url
431        let sign_url = format!("{}?AccessKeyId={}&Expires={}&Signature={}", url_str, self.ak, expires, signature);
432        Ok(sign_url)
433    }
434
435    fn urlencode(s: &str) -> String {
436        let tokens: Vec<String> = s.split("/").map(|token| {
437            encode(token).to_string()
438        }).collect();
439        tokens.join("/")
440    }
441
442}
443
444
445/// obs对象的元数据信息
446/// 
447/// 这个结构体用于表示 OBS 对象的元数据,包含对象的各种属性,如名称、修改时间、内容标识、大小以及存储类型。
448#[derive(Serialize, Deserialize, Debug)]
449pub struct ObjectMeta {
450
451    /// 对象名
452    /// 
453    /// 唯一标识 OBS 存储中的对象
454    pub key: String,
455
456    /// 对象最近一次被修改的时间(UTC时间)
457    /// 
458    /// 该时间戳表示对象在 OBS 存储中最后一次被修改的时刻,采用 UTC 时间格式。
459    pub last_modified: String,
460
461    /// 对象的base64编码的128位MD5摘要
462    /// 
463    /// 这个 ETag 值是对象内容的唯一标识,可以通过该值识别对象内容是否有变化。
464    pub etag: String,
465
466    /// 对象的字节数
467    /// 
468    /// 表示对象在存储中占用的字节大小
469    pub size: u64,
470
471    /// 对象的存储类型:STANDARD,WARM,COLD,DEEP_ARCHIVE
472    /// 
473    /// 不同的存储类型对应不同的存储成本和访问性能,用户可以根据对象的访问频率等因素选择合适的存储类型
474    pub storage_class: String,
475}
476
477/// XML解析器
478/// 
479/// 用于解析XML格式的响应数据,目前这里面针对obs的接口“列举桶内对象”的响应结果进行解析,没有进行通用的xml解析,其不能作为通用工具使用
480struct XmlParser { 
481    xml: String,
482}
483
484
485impl XmlParser {
486    fn new(xml: &str) -> Self {
487        XmlParser { xml: xml.to_string() }
488    }
489
490    /// 解析obs接口“列举桶内对象”的响应结果
491    /// 
492    /// 该内部采用正则表达式进行解析,因此依赖外部的regex库
493    fn parse(&self) -> Vec<ObjectMeta> {
494        let xml = &self.xml;
495
496        // 定义解析需要使用的正则表达式
497        let contents_re = Regex::new(r#"<Contents>(.*?)</Contents>"#).unwrap();
498        let key_regex = Regex::new(r#"<Key>(.*?)</Key>"#).unwrap();
499        let last_modified_regex = Regex::new(r#"<LastModified>(.*?)</LastModified>"#).unwrap();
500        let etag_regex = Regex::new(r#"<ETag>(.*?)</ETag>"#).unwrap();
501        let size_regex = Regex::new(r#"<Size>(.*?)</Size>"#).unwrap();
502        let storage_class_regex = Regex::new(r#"<StorageClass>(.*?)</StorageClass>"#).unwrap();
503
504
505        // 解析Contents标签内的数据
506        let mut contents_vec = Vec::new();
507        for captures in contents_re.captures_iter(xml) {
508            let inner_content = &captures[1];
509
510            let key = key_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
511            let last_modified = last_modified_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
512            let etag = etag_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
513            let size = size_regex.captures(inner_content).and_then(|cap| cap[1].parse().ok()).unwrap_or(0);
514            let storage_class = storage_class_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
515            let content = ObjectMeta {
516                key,
517                last_modified,
518                etag,
519                size,
520                storage_class,
521            };
522            contents_vec.push(content);
523        }
524
525        contents_vec
526    }
527}
528
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533    use chrono::{Duration, Local};
534    use std::time::{SystemTime, UNIX_EPOCH};
535
536    #[test]
537    fn test_parse_xml() {
538        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><ListBucketResult xmlns="http://obs.myhwclouds.com/doc/2015-06-30/"><Name>obs-products</Name><Prefix>tmp</Prefix><Marker></Marker><MaxKeys>1000</MaxKeys><IsTruncated>false</IsTruncated><Contents><Key>tmp/</Key><LastModified>2024-12-03T12:01:48.020Z</LastModified><ETag>"d41d8cd98f00b204e9800998ecf8427e"</ETag><Size>0</Size><Owner><ID>74df55bf376f41d48959d2aa9deaaf38</ID></Owner><StorageClass>STANDARD</StorageClass></Contents><Contents><Key>tmp/index001.png</Key><LastModified>2025-08-20T07:42:59.813Z</LastModified><ETag>"de317c0b7b6e02b42ef2b9e29bb5906a"</ETag><Size>12082</Size><Owner><ID>74df55bf376f41d48959d2aa9deaaf38</ID></Owner><StorageClass>STANDARD</StorageClass></Contents><Contents><Key>tmp/index002.png</Key><LastModified>2025-08-20T07:52:10.204Z</LastModified><ETag>"de317c0b7b6e02b42ef2b9e29bb5906a"</ETag><Size>12082</Size><Owner><ID>74df55bf376f41d48959d2aa9deaaf38</ID></Owner><StorageClass>STANDARD</StorageClass></Contents></ListBucketResult>"#;
539        let parser = XmlParser::new(xml);
540        let contents = parser.parse();
541        let json_data = serde_json::to_string_pretty(&contents).unwrap();
542        println!("{}", json_data);
543    }
544
545    #[test]
546    fn test_timestamp() {
547        let now = Local::now();
548        let two_hours = Duration::hours(2);
549        let future_time = now + two_hours;
550
551        let system_time: SystemTime = future_time.into();
552        let duration = system_time.duration_since(UNIX_EPOCH).unwrap();
553        let timestamp = duration.as_secs();
554        println!("timestamp = {}", timestamp);
555    }
556
557}