1mod 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
114pub struct ObsClient {
117 pub endpoint: String,
118 pub ak: String,
119 pub sk: String,
120 pub bucket: String,
121}
122
123impl ObsClient {
124
125 pub async fn list(&self, prefix: &str) -> Result<Vec<ObjectMeta>, Box<dyn std::error::Error>> {
134 let url = format!("https://{}.{}/?prefix={}", self.bucket, self.endpoint, prefix);
136
137 let date = utils::now_str_gmt();
139
140 let hmacsha1 = HmacSha1();
142
143 let string_to_sign = hmacsha1.string_to_sign("GET", "", "", &date, "", &format!("/{}/", self.bucket));
145
146 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
148
149 let authorization = format!("OBS {}:{}", self.ak, signature);
151 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 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 pub async fn upload_object(&self, obj_key: &str, data: Vec<u8>) -> Result<(), Box<dyn std::error::Error>> {
186 let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
188
189 let md5_string = utils::base64_md5_str(&data);
190
191 let date = utils::now_str_gmt();
193
194 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 let string_to_sign = hmacsha1.string_to_sign("PUT", &md5_string, file_type, &date, "", &format!("/{}/{}", self.bucket, obj_key));
202
203 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
205
206 let authorization = format!("OBS {}:{}", self.ak, signature);
208
209 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 pub async fn download_object(&self, obj_key: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
243
244 let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
246
247 let date = utils::now_str_gmt();
249
250 let hmacsha1 = HmacSha1();
252
253 let string_to_sign = hmacsha1.string_to_sign("GET", "", "", &date, "", &format!("/{}/{}", self.bucket, obj_key));
255
256 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
258
259 let authorization = format!("OBS {}:{}", self.ak, signature);
261
262 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 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 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 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 if file_path.exists() && !overwrite {
296 return Err(Box::new(Error::new(ErrorKind::AlreadyExists, "文件已存在,请删除文件或设置覆盖参数")));
297 }
298
299 let parent = file_path.parent().unwrap();
301 if !parent.exists() {
302 fs::create_dir_all(&parent)?;
303 }
304
305 let data = self.download_object(obj_key).await?;
307
308 fs::write(file_path, data)?;
310 Ok(())
311 }
312
313}
314
315
316#[derive(Serialize, Deserialize, Debug)]
320pub struct ObjectMeta {
321
322 pub key: String,
326
327 pub last_modified: String,
331
332 pub etag: String,
336
337 pub size: u64,
341
342 pub storage_class: String,
346}
347
348struct XmlParser {
352 xml: String,
353}
354
355
356impl XmlParser {
357 fn new(xml: &str) -> Self {
358 XmlParser { xml: xml.to_string() }
359 }
360
361 fn parse(&self) -> Vec<ObjectMeta> {
365 let xml = &self.xml;
366
367 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 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}