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;
139use urlencoding::encode;
140
141pub struct ObsClient {
144 pub endpoint: String,
145 pub ak: String,
146 pub sk: String,
147 pub bucket: String,
148}
149
150impl ObsClient {
151
152 pub async fn list(&self, prefix: &str) -> Result<Vec<ObjectMeta>, Box<dyn std::error::Error>> {
161 let url = format!("https://{}.{}/?prefix={}", self.bucket, self.endpoint, prefix);
163
164 let date = utils::now_str_gmt();
166
167 let hmacsha1 = HmacSha1();
169
170 let string_to_sign = hmacsha1.header_string_to_sign("GET", "", "", &date, "", &format!("/{}/", self.bucket));
172
173 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
175
176 let authorization = format!("OBS {}:{}", self.ak, signature);
178 let client = reqwest::Client::new();
180 let res = client.get(url)
181 .header("Date", &date)
182 .header("Authorization", &authorization)
183 .send()
184 .await?;
185
186 if res.status().is_success() {
188 let xml_content_string = res.text().await?;
189 let results = XmlParser::new(&xml_content_string).parse();
190 return Ok(results);
191 }
192
193 Err(Box::new(Error::new(ErrorKind::Other, format!("请求失败,状态码={}", res.status()))))
194 }
195
196 pub async fn upload_object(&self, obj_key: &str, data: Vec<u8>) -> Result<(), Box<dyn std::error::Error>> {
213 let obj_key = &Self::urlencode(obj_key);
214 let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
216
217 let md5_string = utils::base64_md5_str(&data);
218
219 let date = utils::now_str_gmt();
221
222 let hmacsha1 = HmacSha1();
224
225 let file_type = &utils::get_mime_type_from_extension(obj_key)
226 .expect("资源对应类型暂不支持上传,请在方法get_mime_type_from_extension中添加文件类型");
227
228 let string_to_sign = hmacsha1.header_string_to_sign("PUT", &md5_string, file_type, &date, "", &format!("/{}/{}", self.bucket, obj_key));
230
231 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
233
234 let authorization = format!("OBS {}:{}", self.ak, signature);
236
237 let client = reqwest::Client::new();
239 let res = client.put(url)
240 .header("Content-MD5", &md5_string)
241 .header("Date", &date)
242 .header("Content-Type", file_type)
243 .header("Content-Length", data.len())
244 .header("Authorization", authorization)
245 .body(data)
246 .send()
247 .await;
248
249 let res = match res {
250 Ok(response) => response,
251 Err(e) => {
252 return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e)));
253 },
254 };
255 let _status: StatusCode = res.status();
256
257 Ok(())
258
259 }
260
261 pub async fn upload_file(&self, obj_key: &str, file_path: &str) -> Result<(), Box<dyn std::error::Error>> {
263 let data = fs::read(file_path)?;
264 self.upload_object(obj_key, data).await
265 }
266
267 pub async fn download_object(&self, obj_key: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
275 let obj_key = &Self::urlencode(obj_key);
276 let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
278
279 let date = utils::now_str_gmt();
281
282 let hmacsha1 = HmacSha1();
284
285 let string_to_sign = hmacsha1.header_string_to_sign("GET", "", "", &date, "", &format!("/{}/{}", self.bucket, obj_key));
287
288 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
290
291 let authorization = format!("OBS {}:{}", self.ak, signature);
293
294 let client = reqwest::Client::new();
296 let res = client.get(url)
297 .header("Authorization", &authorization)
298 .header("Date", &date)
299 .send()
300 .await?;
301
302 if res.status().is_success() {
304 return Ok(res.bytes().await?.to_vec());
305 }
306
307 Err(Box::new(Error::new(ErrorKind::Other, format!("请求失败,状态码={}", res.status()))))
308 }
309
310 pub async fn delete_object(&self, obj_key: &str) -> Result<(), Box<dyn std::error::Error>> {
312 let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
314
315 let md5_string = "";
316
317 let date = utils::now_str_gmt();
319
320 let hmacsha1 = HmacSha1();
322
323 let string_to_sign = hmacsha1.header_string_to_sign("DELETE", &md5_string, "", &date, "", &format!("/{}/{}", self.bucket, obj_key));
325
326 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
328
329 let authorization = format!("OBS {}:{}", self.ak, signature);
331
332 let client = reqwest::Client::new();
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 fn urlencode(s: &str) -> String {
412 let tokens: Vec<String> = s.split("/").map(|token| {
413 encode(token).to_string()
414 }).collect();
415 tokens.join("/")
416 }
417
418}
419
420
421#[derive(Serialize, Deserialize, Debug)]
425pub struct ObjectMeta {
426
427 pub key: String,
431
432 pub last_modified: String,
436
437 pub etag: String,
441
442 pub size: u64,
446
447 pub storage_class: String,
451}
452
453struct XmlParser {
457 xml: String,
458}
459
460
461impl XmlParser {
462 fn new(xml: &str) -> Self {
463 XmlParser { xml: xml.to_string() }
464 }
465
466 fn parse(&self) -> Vec<ObjectMeta> {
470 let xml = &self.xml;
471
472 let contents_re = Regex::new(r#"<Contents>(.*?)</Contents>"#).unwrap();
474 let key_regex = Regex::new(r#"<Key>(.*?)</Key>"#).unwrap();
475 let last_modified_regex = Regex::new(r#"<LastModified>(.*?)</LastModified>"#).unwrap();
476 let etag_regex = Regex::new(r#"<ETag>(.*?)</ETag>"#).unwrap();
477 let size_regex = Regex::new(r#"<Size>(.*?)</Size>"#).unwrap();
478 let storage_class_regex = Regex::new(r#"<StorageClass>(.*?)</StorageClass>"#).unwrap();
479
480
481 let mut contents_vec = Vec::new();
483 for captures in contents_re.captures_iter(xml) {
484 let inner_content = &captures[1];
485
486 let key = key_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
487 let last_modified = last_modified_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
488 let etag = etag_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
489 let size = size_regex.captures(inner_content).and_then(|cap| cap[1].parse().ok()).unwrap_or(0);
490 let storage_class = storage_class_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
491 let content = ObjectMeta {
492 key,
493 last_modified,
494 etag,
495 size,
496 storage_class,
497 };
498 contents_vec.push(content);
499 }
500
501 contents_vec
502 }
503}
504
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509 use chrono::{Duration, Local};
510 use std::time::{SystemTime, UNIX_EPOCH};
511
512 #[test]
513 fn test_parse_xml() {
514 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>"#;
515 let parser = XmlParser::new(xml);
516 let contents = parser.parse();
517 let json_data = serde_json::to_string_pretty(&contents).unwrap();
518 println!("{}", json_data);
519 }
520
521 #[test]
522 fn test_timestamp() {
523 let now = Local::now();
524 let two_hours = Duration::hours(2);
525 let future_time = now + two_hours;
526
527 let system_time: SystemTime = future_time.into();
528 let duration = system_time.duration_since(UNIX_EPOCH).unwrap();
529 let timestamp = duration.as_secs();
530 println!("timestamp = {}", timestamp);
531 }
532
533}