obs_sdk/
lib.rs

1//! # 使用说明
2//! 
3//! ## 1. 列举桶内对象列表
4//! 
5//! ```rust
6//! use obs_sdk::ObsClient;
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<(), Box<dyn std::error::Error>> {
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;
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<(), Box<dyn std::error::Error>> {
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;
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<(), Box<dyn std::error::Error>> {
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;
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<(), Box<dyn std::error::Error>> {
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//! 
102mod utils;
103mod algorithm;
104
105use algorithm::HmacSha1;
106use std::fs;
107use std::path::Path;
108use std::io::{Error, ErrorKind};
109use serde::{Serialize, Deserialize};
110use regex::Regex;
111use std::vec::Vec;
112
113
114/// 华为云OBS客户端
115/// 
116pub struct ObsClient {
117    pub endpoint: String,
118    pub ak: String,
119    pub sk: String,
120    pub bucket: String,
121}
122
123impl ObsClient {
124
125    /// 列举指定前缀开头的所有对象元数据,方法内部会构造http请求:
126    /// ```plain
127    /// GET / HTTP/1.1
128    /// Host: bucketname.obs.cn-north-4.myhuaweicloud.com
129    /// Date: date
130    /// Authorization: authorization
131    /// ```
132    /// 
133    pub async fn list(&self, prefix: &str) -> Result<Vec<ObjectMeta>, Box<dyn std::error::Error>> {
134        // 构造完整的url地址
135        let url = format!("https://{}.{}/?prefix={}", self.bucket, self.endpoint, prefix);
136
137        //  获取GMT格式的时间字符串
138        let date = utils::now_str_gmt();
139
140        // 创建HmacSha1对象
141        let hmacsha1 = HmacSha1();
142
143        // 构造签名用的原始字符串
144        let string_to_sign = hmacsha1.string_to_sign("GET", "", "", &date, "",  &format!("/{}/", self.bucket));
145
146        // 根据原始字符串+ak,获取header签名
147        let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
148
149        // 构造请求头Authorization的值
150        let authorization = format!("OBS {}:{}", self.ak, signature);
151        // 构造http请求
152        let client = reqwest::Client::new();
153        let res = client.get(url)
154            .header("Date", &date)
155            .header("Authorization", &authorization)
156            .send()
157            .await?;
158
159        // 如果请求成功,则返回字节内容
160        if res.status().is_success() {
161            let xml_content_string = res.text().await?;
162            let results = XmlParser::new(&xml_content_string).parse();
163            return Ok(results);
164        }
165
166        Err(Box::new(Error::new(ErrorKind::Other, format!("请求失败,状态码={}", res.status()))))
167    }
168
169    /// 上传对象
170    /// 
171    /// 方法内部构建请求
172    /// ```plain
173    /// PUT /object01 HTTP/1.1
174    /// User-Agent: curl/7.29.0
175    /// Host: examplebucket.obs.cn-north-4.myhuaweicloud.com
176    /// Accept: */*
177    /// Date: WED, 01 Jul 2015 04:11:15 GMT
178    /// Authorization: OBS H4IPJX0TQTHTHEBQQCEC:gYqplLq30dEX7GMi2qFWyjdFsyw=
179    /// Content-Length: 10240
180    /// Expect: 100-continue
181    /// 
182    /// [1024 Byte data content]
183    /// ```
184    /// 
185    pub async fn upload_object(&self, obj_key: &str, data: Vec<u8>) -> Result<(), Box<dyn std::error::Error>> {
186        // 构造完整的url地址
187        let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
188
189        let md5_string = utils::base64_md5_str(&data);
190
191        //  获取GMT格式的时间字符串
192        let date = utils::now_str_gmt();
193
194        // 创建HmacSha1对象
195        let hmacsha1 = HmacSha1();
196
197        let file_type = utils::get_mime_type_from_extension(obj_key)
198            .expect("资源对应类型暂不支持上传,请在方法get_mime_type_from_extension中添加文件类型");
199
200        // 构造签名用的原始字符串
201        let string_to_sign = hmacsha1.string_to_sign("PUT", &md5_string, file_type, &date, "",  &format!("/{}/{}", self.bucket, obj_key));
202
203        // 根据原始字符串+ak,获取header签名
204        let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
205
206        // 构造请求头Authorization的值
207        let authorization = format!("OBS {}:{}", self.ak, signature);
208
209        // 构造http请求
210        let client = reqwest::Client::new();
211        println!("url = {}", url);
212        let res = client.put(url)
213            .header("Content-MD5", &md5_string)
214            .header("Date", &date)
215            .header("Content-Type", file_type)
216            .header("Content-Length", data.len())
217            .header("Authorization", authorization)
218            .body(data)
219            .send()
220            .await;
221
222        let res = match res {
223            Ok(response) => response,
224            Err(e) => {
225                return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e)));
226            },
227        };
228        let status = res.status();
229        println!("status = {}, {}", status, res.text_with_charset("utf-8").await?);
230
231        Ok(())
232
233    }
234
235    /// 下载对象,方法内部会构造http请求:
236    /// ```plain
237    /// GET /{obj_key} HTTP/1.1
238    /// Host: {bucket}.obs.cn-north-4.myhuaweicloud.com
239    /// Date: {date}
240    /// ```
241    /// 
242    pub async fn download_object(&self, obj_key: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
243
244        // 构造完整的url地址
245        let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
246
247        //  获取GMT格式的时间字符串
248        let date = utils::now_str_gmt();
249
250        // 创建HmacSha1对象
251        let hmacsha1 = HmacSha1();
252
253        // 构造签名用的原始字符串
254        let string_to_sign = hmacsha1.string_to_sign("GET", "", "", &date, "",  &format!("/{}/{}", self.bucket, obj_key));
255
256        // 根据原始字符串+ak,获取header签名
257        let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
258
259        // 构造请求头Authorization的值
260        let authorization = format!("OBS {}:{}", self.ak, signature);
261
262        // 构造http请求
263        let client = reqwest::Client::new();
264        let res = client.get(url)
265            .header("Authorization", &authorization)
266            .header("Date", &date)
267            .send()
268            .await?;
269
270        // 如果请求成功,则返回字节内容
271        if res.status().is_success() {
272            return Ok(res.bytes().await?.to_vec());
273        }
274        
275        Err(Box::new(Error::new(ErrorKind::Other, format!("请求失败,状态码={}", res.status()))))
276    }
277
278    /// 上传文件
279    pub async fn upload_file(&self, obj_key: &str, file_path: &str) -> Result<(), Box<dyn std::error::Error>> {
280        let data = fs::read(file_path)?;
281        let file_type = utils::get_mime_type_from_extension(file_path).unwrap();
282        self.upload_object(obj_key, data).await
283    }
284
285    /// 下载文件,并指定本地保存用的文件路径
286    /// 
287    /// # 参数
288    /// 
289    /// `overwrite` - 是否覆盖,true,当文件存在时,覆盖文件,false,当文件存在时,不覆盖文件
290    /// 
291    pub async fn download_file(&self, obj_key: &str, file_path: &str, overwrite: bool) -> Result<(), Box<dyn std::error::Error>> {
292        let file_path = Path::new(file_path);
293
294        // 判断文件是否存在,如果存在,不做任何操作
295        if file_path.exists() && !overwrite {
296            return Err(Box::new(Error::new(ErrorKind::AlreadyExists, "文件已存在,请删除文件或设置覆盖参数")));
297        }
298
299        // 根据父目录是否存在,选择性创建父目录
300        let parent = file_path.parent().unwrap();
301        if !parent.exists() {
302            fs::create_dir_all(&parent)?;
303        }
304
305        // 下载文件,得到原始文件字节内容
306        let data = self.download_object(obj_key).await?;
307        
308        // 保存文件
309        fs::write(file_path, data)?;
310        Ok(())
311    }
312
313}
314
315
316/// obs对象的元数据信息
317/// 
318/// 这个结构体用于表示 OBS 对象的元数据,包含对象的各种属性,如名称、修改时间、内容标识、大小以及存储类型。
319#[derive(Serialize, Deserialize, Debug)]
320pub struct ObjectMeta {
321
322    /// 对象名
323    /// 
324    /// 唯一标识 OBS 存储中的对象
325    pub key: String,
326
327    /// 对象最近一次被修改的时间(UTC时间)
328    /// 
329    /// 该时间戳表示对象在 OBS 存储中最后一次被修改的时刻,采用 UTC 时间格式。
330    pub last_modified: String,
331
332    /// 对象的base64编码的128位MD5摘要
333    /// 
334    /// 这个 ETag 值是对象内容的唯一标识,可以通过该值识别对象内容是否有变化。
335    pub etag: String,
336
337    /// 对象的字节数
338    /// 
339    /// 表示对象在存储中占用的字节大小
340    pub size: u64,
341
342    /// 对象的存储类型:STANDARD,WARM,COLD,DEEP_ARCHIVE
343    /// 
344    /// 不同的存储类型对应不同的存储成本和访问性能,用户可以根据对象的访问频率等因素选择合适的存储类型
345    pub storage_class: String,
346}
347
348/// XML解析器
349/// 
350/// 用于解析XML格式的响应数据,目前这里面针对obs的接口“列举桶内对象”的响应结果进行解析,没有进行通用的xml解析,其不能作为通用工具使用
351struct XmlParser { 
352    xml: String,
353}
354
355
356impl XmlParser {
357    fn new(xml: &str) -> Self {
358        XmlParser { xml: xml.to_string() }
359    }
360
361    /// 解析obs接口“列举桶内对象”的响应结果
362    /// 
363    /// 该内部采用正则表达式进行解析,因此依赖外部的regex库
364    fn parse(&self) -> Vec<ObjectMeta> {
365        let xml = &self.xml;
366
367        // 定义解析需要使用的正则表达式
368        let contents_re = Regex::new(r#"<Contents>(.*?)</Contents>"#).unwrap();
369        let key_regex = Regex::new(r#"<Key>(.*?)</Key>"#).unwrap();
370        let last_modified_regex = Regex::new(r#"<LastModified>(.*?)</LastModified>"#).unwrap();
371        let etag_regex = Regex::new(r#"<ETag>(.*?)</ETag>"#).unwrap();
372        let size_regex = Regex::new(r#"<Size>(.*?)</Size>"#).unwrap();
373        let storage_class_regex = Regex::new(r#"<StorageClass>(.*?)</StorageClass>"#).unwrap();
374
375
376        // 解析Contents标签内的数据
377        let mut contents_vec = Vec::new();
378        for captures in contents_re.captures_iter(xml) {
379            let inner_content = &captures[1];
380
381            let key = key_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
382            let last_modified = last_modified_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
383            let etag = etag_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
384            let size = size_regex.captures(inner_content).and_then(|cap| cap[1].parse().ok()).unwrap_or(0);
385            let storage_class = storage_class_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
386            let content = ObjectMeta {
387                key,
388                last_modified,
389                etag,
390                size,
391                storage_class,
392            };
393            contents_vec.push(content);
394        }
395
396        contents_vec
397    }
398}
399
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    #[test]
406    fn test_parse_xml() {
407        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>"#;
408        let parser = XmlParser::new(xml);
409        let contents = parser.parse();
410        let json_data = serde_json::to_string_pretty(&contents).unwrap();
411        println!("{}", json_data);
412    }
413
414}