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
310 pub async fn download_file(&self, obj_key: &str, file_path: &str, overwrite: bool) -> Result<(), Box<dyn std::error::Error>> {
317 let file_path = Path::new(file_path);
318
319 if file_path.exists() && !overwrite {
321 return Err(Box::new(Error::new(ErrorKind::AlreadyExists, "文件已存在,请删除文件或设置覆盖参数")));
322 }
323
324 let parent = file_path.parent().unwrap();
326 if !parent.exists() {
327 fs::create_dir_all(&parent)?;
328 }
329
330 let data = self.download_object(obj_key).await?;
332
333 fs::write(file_path, data)?;
335 Ok(())
336 }
337
338 pub fn url_sign(&self, url_str: &str) -> Result<String, Box<dyn std::error::Error>> {
339 let obs_object_url = Url::parse(url_str)?;
340 let resource_part = obs_object_url.path();
341 let host = obs_object_url.host().unwrap();
342 let domain = match host {
343 url::Host::Domain(domain) => domain.to_string(),
344 _ => format!("{}.{}", self.bucket, self.endpoint)
345 };
346 let parts: Vec<&str> = domain.split(".").collect();
347 let bucket_name = parts[0];
348
349 let timestamp = utils::timestamp(Local::now(), 3600*2);
350
351 let expires = format!("{}", timestamp);
353
354 let hmacsha1 = HmacSha1();
356
357 let string_to_sign = hmacsha1.url_string_to_sign("GET", "", "", &expires, "", &format!("/{}{}", bucket_name, resource_part));
359
360 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
362 let signature = form_urlencoded::byte_serialize(signature.as_bytes()).collect::<String>();
363
364 let sign_url = format!("{}?AccessKeyId={}&Expires={}&Signature={}", url_str, self.ak, expires, signature);
366 Ok(sign_url)
367 }
368
369}
370
371
372#[derive(Serialize, Deserialize, Debug)]
376pub struct ObjectMeta {
377
378 pub key: String,
382
383 pub last_modified: String,
387
388 pub etag: String,
392
393 pub size: u64,
397
398 pub storage_class: String,
402}
403
404struct XmlParser {
408 xml: String,
409}
410
411
412impl XmlParser {
413 fn new(xml: &str) -> Self {
414 XmlParser { xml: xml.to_string() }
415 }
416
417 fn parse(&self) -> Vec<ObjectMeta> {
421 let xml = &self.xml;
422
423 let contents_re = Regex::new(r#"<Contents>(.*?)</Contents>"#).unwrap();
425 let key_regex = Regex::new(r#"<Key>(.*?)</Key>"#).unwrap();
426 let last_modified_regex = Regex::new(r#"<LastModified>(.*?)</LastModified>"#).unwrap();
427 let etag_regex = Regex::new(r#"<ETag>(.*?)</ETag>"#).unwrap();
428 let size_regex = Regex::new(r#"<Size>(.*?)</Size>"#).unwrap();
429 let storage_class_regex = Regex::new(r#"<StorageClass>(.*?)</StorageClass>"#).unwrap();
430
431
432 let mut contents_vec = Vec::new();
434 for captures in contents_re.captures_iter(xml) {
435 let inner_content = &captures[1];
436
437 let key = key_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
438 let last_modified = last_modified_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
439 let etag = etag_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
440 let size = size_regex.captures(inner_content).and_then(|cap| cap[1].parse().ok()).unwrap_or(0);
441 let storage_class = storage_class_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
442 let content = ObjectMeta {
443 key,
444 last_modified,
445 etag,
446 size,
447 storage_class,
448 };
449 contents_vec.push(content);
450 }
451
452 contents_vec
453 }
454}
455
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460 use chrono::{Duration, Local};
461 use std::time::{SystemTime, UNIX_EPOCH};
462
463 #[test]
464 fn test_parse_xml() {
465 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>"#;
466 let parser = XmlParser::new(xml);
467 let contents = parser.parse();
468 let json_data = serde_json::to_string_pretty(&contents).unwrap();
469 println!("{}", json_data);
470 }
471
472 #[test]
473 fn test_timestamp() {
474 let now = Local::now();
475 let two_hours = Duration::hours(2);
476 let future_time = now + two_hours;
477
478 let system_time: SystemTime = future_time.into();
479 let duration = system_time.duration_since(UNIX_EPOCH).unwrap();
480 let timestamp = duration.as_secs();
481 println!("timestamp = {}", timestamp);
482 }
483
484}