1mod utils;
127mod algorithm;
128
129use algorithm::HmacSha1;
130use std::fs;
131use std::path::Path;
132use std::io::{Error, ErrorKind};
133use serde::{Serialize, Deserialize};
134use regex::Regex;
135use std::vec::Vec;
136use url::{Url, form_urlencoded};
137use chrono::Local;
138
139pub struct ObsClient {
142 pub endpoint: String,
143 pub ak: String,
144 pub sk: String,
145 pub bucket: String,
146}
147
148impl ObsClient {
149
150 pub async fn list(&self, prefix: &str) -> Result<Vec<ObjectMeta>, Box<dyn std::error::Error>> {
159 let url = format!("https://{}.{}/?prefix={}", self.bucket, self.endpoint, prefix);
161
162 let date = utils::now_str_gmt();
164
165 let hmacsha1 = HmacSha1();
167
168 let string_to_sign = hmacsha1.header_string_to_sign("GET", "", "", &date, "", &format!("/{}/", self.bucket));
170
171 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
173
174 let authorization = format!("OBS {}:{}", self.ak, signature);
176 let client = reqwest::Client::new();
178 let res = client.get(url)
179 .header("Date", &date)
180 .header("Authorization", &authorization)
181 .send()
182 .await?;
183
184 if res.status().is_success() {
186 let xml_content_string = res.text().await?;
187 let results = XmlParser::new(&xml_content_string).parse();
188 return Ok(results);
189 }
190
191 Err(Box::new(Error::new(ErrorKind::Other, format!("请求失败,状态码={}", res.status()))))
192 }
193
194 pub async fn upload_object(&self, obj_key: &str, data: Vec<u8>) -> Result<(), Box<dyn std::error::Error>> {
211 let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
213
214 let md5_string = utils::base64_md5_str(&data);
215
216 let date = utils::now_str_gmt();
218
219 let hmacsha1 = HmacSha1();
221
222 let file_type = utils::get_mime_type_from_extension(obj_key)
223 .expect("资源对应类型暂不支持上传,请在方法get_mime_type_from_extension中添加文件类型");
224
225 let string_to_sign = hmacsha1.header_string_to_sign("PUT", &md5_string, file_type, &date, "", &format!("/{}/{}", self.bucket, obj_key));
227
228 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
230
231 let authorization = format!("OBS {}:{}", self.ak, signature);
233
234 let client = reqwest::Client::new();
236 println!("url = {}", url);
237 let res = client.put(url)
238 .header("Content-MD5", &md5_string)
239 .header("Date", &date)
240 .header("Content-Type", file_type)
241 .header("Content-Length", data.len())
242 .header("Authorization", authorization)
243 .body(data)
244 .send()
245 .await;
246
247 let res = match res {
248 Ok(response) => response,
249 Err(e) => {
250 return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e)));
251 },
252 };
253 let status = res.status();
254 println!("status = {}, {}", status, res.text_with_charset("utf-8").await?);
255
256 Ok(())
257
258 }
259
260 pub async fn upload_file(&self, obj_key: &str, file_path: &str) -> Result<(), Box<dyn std::error::Error>> {
262 let data = fs::read(file_path)?;
263 self.upload_object(obj_key, data).await
264 }
265
266 pub async fn download_object(&self, obj_key: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
274
275 let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
277
278 let date = utils::now_str_gmt();
280
281 let hmacsha1 = HmacSha1();
283
284 let string_to_sign = hmacsha1.header_string_to_sign("GET", "", "", &date, "", &format!("/{}/{}", self.bucket, obj_key));
286
287 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
289
290 let authorization = format!("OBS {}:{}", self.ak, signature);
292
293 let client = reqwest::Client::new();
295 let res = client.get(url)
296 .header("Authorization", &authorization)
297 .header("Date", &date)
298 .send()
299 .await?;
300
301 if res.status().is_success() {
303 return Ok(res.bytes().await?.to_vec());
304 }
305
306 Err(Box::new(Error::new(ErrorKind::Other, format!("请求失败,状态码={}", res.status()))))
307 }
308
309 pub async fn delete_object(&self, obj_key: &str) -> Result<(), Box<dyn std::error::Error>> {
311 let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
313
314 let md5_string = "";
315
316 let date = utils::now_str_gmt();
318
319 let hmacsha1 = HmacSha1();
321
322 let string_to_sign = hmacsha1.header_string_to_sign("DELETE", &md5_string, "", &date, "", &format!("/{}/{}", self.bucket, obj_key));
324
325 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
327
328 let authorization = format!("OBS {}:{}", self.ak, signature);
330
331 let client = reqwest::Client::new();
333 println!("url = {}", url);
334 let res = client.delete(url)
335 .header("Date", &date)
336 .header("Authorization", authorization)
337 .send()
338 .await;
339
340 let res = match res {
341 Ok(response) => response,
342 Err(e) => {
343 return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e)));
344 },
345 };
346 let status = res.status();
347 println!("status = {}, {}", status, res.text_with_charset("utf-8").await?);
348
349 Ok(())
350 }
351
352 pub async fn download_file(&self, obj_key: &str, file_path: &str, overwrite: bool) -> Result<(), Box<dyn std::error::Error>> {
359 let file_path = Path::new(file_path);
360
361 if file_path.exists() && !overwrite {
363 return Err(Box::new(Error::new(ErrorKind::AlreadyExists, "文件已存在,请删除文件或设置覆盖参数")));
364 }
365
366 let parent = file_path.parent().unwrap();
368 if !parent.exists() {
369 fs::create_dir_all(&parent)?;
370 }
371
372 let data = self.download_object(obj_key).await?;
374
375 fs::write(file_path, data)?;
377 Ok(())
378 }
379
380 pub fn url_sign(&self, url_str: &str) -> Result<String, Box<dyn std::error::Error>> {
381 let obs_object_url = Url::parse(url_str)?;
382 let resource_part = obs_object_url.path();
383 let host = obs_object_url.host().unwrap();
384 let domain = match host {
385 url::Host::Domain(domain) => domain.to_string(),
386 _ => format!("{}.{}", self.bucket, self.endpoint)
387 };
388 let parts: Vec<&str> = domain.split(".").collect();
389 let bucket_name = parts[0];
390
391 let timestamp = utils::timestamp(Local::now(), 3600*2);
392
393 let expires = format!("{}", timestamp);
395
396 let hmacsha1 = HmacSha1();
398
399 let string_to_sign = hmacsha1.url_string_to_sign("GET", "", "", &expires, "", &format!("/{}{}", bucket_name, resource_part));
401
402 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
404 let signature = form_urlencoded::byte_serialize(signature.as_bytes()).collect::<String>();
405
406 let sign_url = format!("{}?AccessKeyId={}&Expires={}&Signature={}", url_str, self.ak, expires, signature);
408 Ok(sign_url)
409 }
410
411}
412
413
414#[derive(Serialize, Deserialize, Debug)]
418pub struct ObjectMeta {
419
420 pub key: String,
424
425 pub last_modified: String,
429
430 pub etag: String,
434
435 pub size: u64,
439
440 pub storage_class: String,
444}
445
446struct XmlParser {
450 xml: String,
451}
452
453
454impl XmlParser {
455 fn new(xml: &str) -> Self {
456 XmlParser { xml: xml.to_string() }
457 }
458
459 fn parse(&self) -> Vec<ObjectMeta> {
463 let xml = &self.xml;
464
465 let contents_re = Regex::new(r#"<Contents>(.*?)</Contents>"#).unwrap();
467 let key_regex = Regex::new(r#"<Key>(.*?)</Key>"#).unwrap();
468 let last_modified_regex = Regex::new(r#"<LastModified>(.*?)</LastModified>"#).unwrap();
469 let etag_regex = Regex::new(r#"<ETag>(.*?)</ETag>"#).unwrap();
470 let size_regex = Regex::new(r#"<Size>(.*?)</Size>"#).unwrap();
471 let storage_class_regex = Regex::new(r#"<StorageClass>(.*?)</StorageClass>"#).unwrap();
472
473
474 let mut contents_vec = Vec::new();
476 for captures in contents_re.captures_iter(xml) {
477 let inner_content = &captures[1];
478
479 let key = key_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
480 let last_modified = last_modified_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
481 let etag = etag_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
482 let size = size_regex.captures(inner_content).and_then(|cap| cap[1].parse().ok()).unwrap_or(0);
483 let storage_class = storage_class_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
484 let content = ObjectMeta {
485 key,
486 last_modified,
487 etag,
488 size,
489 storage_class,
490 };
491 contents_vec.push(content);
492 }
493
494 contents_vec
495 }
496}
497
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502 use chrono::{Duration, Local};
503 use std::time::{SystemTime, UNIX_EPOCH};
504
505 #[test]
506 fn test_parse_xml() {
507 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>"#;
508 let parser = XmlParser::new(xml);
509 let contents = parser.parse();
510 let json_data = serde_json::to_string_pretty(&contents).unwrap();
511 println!("{}", json_data);
512 }
513
514 #[test]
515 fn test_timestamp() {
516 let now = Local::now();
517 let two_hours = Duration::hours(2);
518 let future_time = now + two_hours;
519
520 let system_time: SystemTime = future_time.into();
521 let duration = system_time.duration_since(UNIX_EPOCH).unwrap();
522 let timestamp = duration.as_secs();
523 println!("timestamp = {}", timestamp);
524 }
525
526}