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;
138use reqwest::StatusCode;
139
140pub struct ObsClient {
143 pub endpoint: String,
144 pub ak: String,
145 pub sk: String,
146 pub bucket: String,
147}
148
149impl ObsClient {
150
151 pub async fn list(&self, prefix: &str) -> Result<Vec<ObjectMeta>, Box<dyn std::error::Error>> {
160 let url = format!("https://{}.{}/?prefix={}", self.bucket, self.endpoint, prefix);
162
163 let date = utils::now_str_gmt();
165
166 let hmacsha1 = HmacSha1();
168
169 let string_to_sign = hmacsha1.header_string_to_sign("GET", "", "", &date, "", &format!("/{}/", self.bucket));
171
172 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
174
175 let authorization = format!("OBS {}:{}", self.ak, signature);
177 let client = reqwest::Client::new();
179 let res = client.get(url)
180 .header("Date", &date)
181 .header("Authorization", &authorization)
182 .send()
183 .await?;
184
185 if res.status().is_success() {
187 let xml_content_string = res.text().await?;
188 let results = XmlParser::new(&xml_content_string).parse();
189 return Ok(results);
190 }
191
192 Err(Box::new(Error::new(ErrorKind::Other, format!("请求失败,状态码={}", res.status()))))
193 }
194
195 pub async fn upload_object(&self, obj_key: &str, data: Vec<u8>) -> Result<(), Box<dyn std::error::Error>> {
212 let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
214
215 let md5_string = utils::base64_md5_str(&data);
216
217 let date = utils::now_str_gmt();
219
220 let hmacsha1 = HmacSha1();
222
223 let file_type = &utils::get_mime_type_from_extension(obj_key)
224 .expect("资源对应类型暂不支持上传,请在方法get_mime_type_from_extension中添加文件类型");
225
226 let string_to_sign = hmacsha1.header_string_to_sign("PUT", &md5_string, file_type, &date, "", &format!("/{}/{}", self.bucket, obj_key));
228
229 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
231
232 let authorization = format!("OBS {}:{}", self.ak, signature);
234
235 let client = reqwest::Client::new();
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: StatusCode = res.status();
254
255 Ok(())
256
257 }
258
259 pub async fn upload_file(&self, obj_key: &str, file_path: &str) -> Result<(), Box<dyn std::error::Error>> {
261 let data = fs::read(file_path)?;
262 self.upload_object(obj_key, data).await
263 }
264
265 pub async fn download_object(&self, obj_key: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
273
274 let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
276
277 let date = utils::now_str_gmt();
279
280 let hmacsha1 = HmacSha1();
282
283 let string_to_sign = hmacsha1.header_string_to_sign("GET", "", "", &date, "", &format!("/{}/{}", self.bucket, obj_key));
285
286 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
288
289 let authorization = format!("OBS {}:{}", self.ak, signature);
291
292 let client = reqwest::Client::new();
294 let res = client.get(url)
295 .header("Authorization", &authorization)
296 .header("Date", &date)
297 .send()
298 .await?;
299
300 if res.status().is_success() {
302 return Ok(res.bytes().await?.to_vec());
303 }
304
305 Err(Box::new(Error::new(ErrorKind::Other, format!("请求失败,状态码={}", res.status()))))
306 }
307
308 pub async fn delete_object(&self, obj_key: &str) -> Result<(), Box<dyn std::error::Error>> {
310 let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
312
313 let md5_string = "";
314
315 let date = utils::now_str_gmt();
317
318 let hmacsha1 = HmacSha1();
320
321 let string_to_sign = hmacsha1.header_string_to_sign("DELETE", &md5_string, "", &date, "", &format!("/{}/{}", self.bucket, obj_key));
323
324 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
326
327 let authorization = format!("OBS {}:{}", self.ak, signature);
329
330 let client = reqwest::Client::new();
332 let res = client.delete(url)
333 .header("Date", &date)
334 .header("Authorization", authorization)
335 .send()
336 .await;
337
338 let res = match res {
339 Ok(response) => response,
340 Err(e) => {
341 return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e)));
342 },
343 };
344 let status = res.status();
345 println!("status = {}, {}", status, res.text_with_charset("utf-8").await?);
346
347 Ok(())
348 }
349
350 pub async fn download_file(&self, obj_key: &str, file_path: &str, overwrite: bool) -> Result<(), Box<dyn std::error::Error>> {
357 let file_path = Path::new(file_path);
358
359 if file_path.exists() && !overwrite {
361 return Err(Box::new(Error::new(ErrorKind::AlreadyExists, "文件已存在,请删除文件或设置覆盖参数")));
362 }
363
364 let parent = file_path.parent().unwrap();
366 if !parent.exists() {
367 fs::create_dir_all(&parent)?;
368 }
369
370 let data = self.download_object(obj_key).await?;
372
373 fs::write(file_path, data)?;
375 Ok(())
376 }
377
378 pub fn url_sign(&self, url_str: &str) -> Result<String, Box<dyn std::error::Error>> {
379 let obs_object_url = Url::parse(url_str)?;
380 let resource_part = obs_object_url.path();
381 let host = obs_object_url.host().unwrap();
382 let domain = match host {
383 url::Host::Domain(domain) => domain.to_string(),
384 _ => format!("{}.{}", self.bucket, self.endpoint)
385 };
386 let parts: Vec<&str> = domain.split(".").collect();
387 let bucket_name = parts[0];
388
389 let timestamp = utils::timestamp(Local::now(), 3600*2);
390
391 let expires = format!("{}", timestamp);
393
394 let hmacsha1 = HmacSha1();
396
397 let string_to_sign = hmacsha1.url_string_to_sign("GET", "", "", &expires, "", &format!("/{}{}", bucket_name, resource_part));
399
400 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
402 let signature = form_urlencoded::byte_serialize(signature.as_bytes()).collect::<String>();
403
404 let sign_url = format!("{}?AccessKeyId={}&Expires={}&Signature={}", url_str, self.ak, expires, signature);
406 Ok(sign_url)
407 }
408
409}
410
411
412#[derive(Serialize, Deserialize, Debug)]
416pub struct ObjectMeta {
417
418 pub key: String,
422
423 pub last_modified: String,
427
428 pub etag: String,
432
433 pub size: u64,
437
438 pub storage_class: String,
442}
443
444struct XmlParser {
448 xml: String,
449}
450
451
452impl XmlParser {
453 fn new(xml: &str) -> Self {
454 XmlParser { xml: xml.to_string() }
455 }
456
457 fn parse(&self) -> Vec<ObjectMeta> {
461 let xml = &self.xml;
462
463 let contents_re = Regex::new(r#"<Contents>(.*?)</Contents>"#).unwrap();
465 let key_regex = Regex::new(r#"<Key>(.*?)</Key>"#).unwrap();
466 let last_modified_regex = Regex::new(r#"<LastModified>(.*?)</LastModified>"#).unwrap();
467 let etag_regex = Regex::new(r#"<ETag>(.*?)</ETag>"#).unwrap();
468 let size_regex = Regex::new(r#"<Size>(.*?)</Size>"#).unwrap();
469 let storage_class_regex = Regex::new(r#"<StorageClass>(.*?)</StorageClass>"#).unwrap();
470
471
472 let mut contents_vec = Vec::new();
474 for captures in contents_re.captures_iter(xml) {
475 let inner_content = &captures[1];
476
477 let key = key_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
478 let last_modified = last_modified_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
479 let etag = etag_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
480 let size = size_regex.captures(inner_content).and_then(|cap| cap[1].parse().ok()).unwrap_or(0);
481 let storage_class = storage_class_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
482 let content = ObjectMeta {
483 key,
484 last_modified,
485 etag,
486 size,
487 storage_class,
488 };
489 contents_vec.push(content);
490 }
491
492 contents_vec
493 }
494}
495
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500 use chrono::{Duration, Local};
501 use std::time::{SystemTime, UNIX_EPOCH};
502
503 #[test]
504 fn test_parse_xml() {
505 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>"#;
506 let parser = XmlParser::new(xml);
507 let contents = parser.parse();
508 let json_data = serde_json::to_string_pretty(&contents).unwrap();
509 println!("{}", json_data);
510 }
511
512 #[test]
513 fn test_timestamp() {
514 let now = Local::now();
515 let two_hours = Duration::hours(2);
516 let future_time = now + two_hours;
517
518 let system_time: SystemTime = future_time.into();
519 let duration = system_time.duration_since(UNIX_EPOCH).unwrap();
520 let timestamp = duration.as_secs();
521 println!("timestamp = {}", timestamp);
522 }
523
524}