1mod utils;
127mod algorithm;
128
129use algorithm::HmacSha1;
130use std::{fmt, fs, io};
131use std::path::Path;
132use serde::{Serialize, Deserialize};
133use regex::Regex;
134use std::vec::Vec;
135use url::{Url, form_urlencoded};
136use chrono::Local;
137use reqwest::StatusCode;
138use urlencoding::encode;
139
140pub struct ObsClient {
143 pub endpoint: String,
144 pub ak: String,
145 pub sk: String,
146 pub bucket: String,
147}
148
149pub enum ObsError {
150 Common(Box<dyn core::error::Error + Send + Sync + 'static>),
151}
152
153impl <E> From<E> for ObsError
155where
156 E: core::error::Error + Send + Sync + 'static
157{
158 fn from(err: E) -> ObsError {
159 ObsError::Common(Box::new(err))
160 }
161}
162
163impl fmt::Debug for ObsError {
164 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
165 let mut builder = f.debug_struct("obs-sdk::Error");
166 builder.finish()
167 }
168}
169
170impl fmt::Display for ObsError {
171 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
172 match self {
173 ObsError::Common(e) => f.write_str("obs sdk error")?,
174 };
175 Ok(())
176 }
177}
178
179
180
181impl ObsClient {
182
183 pub async fn list(&self, prefix: &str) -> Result<Vec<ObjectMeta>, ObsError> {
192 let url = format!("https://{}.{}/?prefix={}", self.bucket, self.endpoint, prefix);
194
195 let date = utils::now_str_gmt();
197
198 let hmacsha1 = HmacSha1();
200
201 let string_to_sign = hmacsha1.header_string_to_sign("GET", "", "", &date, "", &format!("/{}/", self.bucket));
203
204 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
206
207 let authorization = format!("OBS {}:{}", self.ak, signature);
209 let client = reqwest::Client::new();
211 let res = client.get(url)
212 .header("Date", &date)
213 .header("Authorization", &authorization)
214 .send()
215 .await?;
216
217 if res.status().is_success() {
219 let xml_content_string = res.text().await?;
220 let results = XmlParser::new(&xml_content_string).parse();
221 return Ok(results);
222 }
223
224 Err(ObsError::Common(Box::new(io::Error::new(io::ErrorKind::Other, format!("请求失败,状态码={}", res.status())))))
225 }
226
227 pub async fn upload_object(&self, obj_key: &str, data: Vec<u8>) -> Result<(), ObsError> {
244 let obj_key = &Self::urlencode(obj_key);
245 let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
247
248 let md5_string = utils::base64_md5_str(&data);
249
250 let date = utils::now_str_gmt();
252
253 let hmacsha1 = HmacSha1();
255
256 let file_type = &utils::get_mime_type_from_extension(obj_key)
257 .unwrap_or(String::from("application/octet-stream"));
258
259 let string_to_sign = hmacsha1.header_string_to_sign("PUT", &md5_string, file_type, &date, "", &format!("/{}/{}", self.bucket, obj_key));
261
262 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
264
265 let authorization = format!("OBS {}:{}", self.ak, signature);
267
268 let client = reqwest::Client::new();
270 let res = client.put(url)
271 .header("Content-MD5", &md5_string)
272 .header("Date", &date)
273 .header("Content-Type", file_type)
274 .header("Content-Length", data.len())
275 .header("Authorization", authorization)
276 .body(data)
277 .send()
278 .await?;
279 let _status: StatusCode = res.status();
280 Ok(())
281
282 }
283
284 pub async fn upload_file(&self, obj_key: &str, file_path: &str) -> Result<(), ObsError> {
286 let data = fs::read(file_path)?;
287 self.upload_object(obj_key, data).await
288 }
289
290 pub async fn download_object(&self, obj_key: &str) -> Result<Vec<u8>, ObsError> {
298 let obj_key = &Self::urlencode(obj_key);
299 let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
301
302 let date = utils::now_str_gmt();
304
305 let hmacsha1 = HmacSha1();
307
308 let string_to_sign = hmacsha1.header_string_to_sign("GET", "", "", &date, "", &format!("/{}/{}", self.bucket, obj_key));
310
311 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
313
314 let authorization = format!("OBS {}:{}", self.ak, signature);
316
317 let client = reqwest::Client::new();
319 let res = client.get(url)
320 .header("Authorization", &authorization)
321 .header("Date", &date)
322 .send()
323 .await?;
324
325 if res.status().is_success() {
327 return Ok(res.bytes().await?.to_vec());
328 }
329
330 Err(ObsError::Common(Box::new(io::Error::new(io::ErrorKind::Other, format!("请求失败,状态码={}", res.status())))))
331 }
332
333 pub async fn delete_object(&self, obj_key: &str) -> Result<(), ObsError> {
335 let obj_key = &Self::urlencode(obj_key);
336 let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
338
339 let md5_string = "";
340
341 let date = utils::now_str_gmt();
343
344 let hmacsha1 = HmacSha1();
346
347 let string_to_sign = hmacsha1.header_string_to_sign("DELETE", &md5_string, "", &date, "", &format!("/{}/{}", self.bucket, obj_key));
349
350 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
352
353 let authorization = format!("OBS {}:{}", self.ak, signature);
355
356 let client = reqwest::Client::new();
358 let res = client.delete(url)
359 .header("Date", &date)
360 .header("Authorization", authorization)
361 .send()
362 .await;
363
364 let res = match res {
365 Ok(response) => response,
366 Err(e) => {
367 return Err(ObsError::Common(Box::new(io::Error::new(io::ErrorKind::Other, e))));
368 },
369 };
370 let status = res.status();
371 println!("status = {}, {}", status, res.text_with_charset("utf-8").await?);
372
373 Ok(())
374 }
375
376 pub async fn download_file(&self, obj_key: &str, file_path: &str, overwrite: bool) -> Result<(), ObsError> {
383 let file_path = Path::new(file_path);
384
385 if file_path.exists() && !overwrite {
387 return Err(ObsError::Common(Box::new(io::Error::new(io::ErrorKind::AlreadyExists, "文件已存在,请删除文件或设置覆盖参数"))));
388 }
389
390 let parent = file_path.parent().unwrap();
392 if !parent.exists() {
393 fs::create_dir_all(&parent)?;
394 }
395
396 let data = self.download_object(obj_key).await?;
398
399 fs::write(file_path, data)?;
401 Ok(())
402 }
403
404 pub fn url_sign(&self, url_str: &str) -> Result<String, ObsError> {
405 let obs_object_url = Url::parse(url_str)?;
406 let resource_part = obs_object_url.path();
407 let host = obs_object_url.host().unwrap();
408 let domain = match host {
409 url::Host::Domain(domain) => domain.to_string(),
410 _ => format!("{}.{}", self.bucket, self.endpoint)
411 };
412 let parts: Vec<&str> = domain.split(".").collect();
413 let bucket_name = parts[0];
414
415 let timestamp = utils::timestamp(Local::now(), 3600*2);
416
417 let expires = format!("{}", timestamp);
419
420 let hmacsha1 = HmacSha1();
422
423 let string_to_sign = hmacsha1.url_string_to_sign("GET", "", "", &expires, "", &format!("/{}{}", bucket_name, resource_part));
425
426 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
428 let signature = form_urlencoded::byte_serialize(signature.as_bytes()).collect::<String>();
429
430 let sign_url = format!("{}?AccessKeyId={}&Expires={}&Signature={}", url_str, self.ak, expires, signature);
432 Ok(sign_url)
433 }
434
435 fn urlencode(s: &str) -> String {
436 let tokens: Vec<String> = s.split("/").map(|token| {
437 encode(token).to_string()
438 }).collect();
439 tokens.join("/")
440 }
441
442}
443
444
445#[derive(Serialize, Deserialize, Debug)]
449pub struct ObjectMeta {
450
451 pub key: String,
455
456 pub last_modified: String,
460
461 pub etag: String,
465
466 pub size: u64,
470
471 pub storage_class: String,
475}
476
477struct XmlParser {
481 xml: String,
482}
483
484
485impl XmlParser {
486 fn new(xml: &str) -> Self {
487 XmlParser { xml: xml.to_string() }
488 }
489
490 fn parse(&self) -> Vec<ObjectMeta> {
494 let xml = &self.xml;
495
496 let contents_re = Regex::new(r#"<Contents>(.*?)</Contents>"#).unwrap();
498 let key_regex = Regex::new(r#"<Key>(.*?)</Key>"#).unwrap();
499 let last_modified_regex = Regex::new(r#"<LastModified>(.*?)</LastModified>"#).unwrap();
500 let etag_regex = Regex::new(r#"<ETag>(.*?)</ETag>"#).unwrap();
501 let size_regex = Regex::new(r#"<Size>(.*?)</Size>"#).unwrap();
502 let storage_class_regex = Regex::new(r#"<StorageClass>(.*?)</StorageClass>"#).unwrap();
503
504
505 let mut contents_vec = Vec::new();
507 for captures in contents_re.captures_iter(xml) {
508 let inner_content = &captures[1];
509
510 let key = key_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
511 let last_modified = last_modified_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
512 let etag = etag_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
513 let size = size_regex.captures(inner_content).and_then(|cap| cap[1].parse().ok()).unwrap_or(0);
514 let storage_class = storage_class_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
515 let content = ObjectMeta {
516 key,
517 last_modified,
518 etag,
519 size,
520 storage_class,
521 };
522 contents_vec.push(content);
523 }
524
525 contents_vec
526 }
527}
528
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533 use chrono::{Duration, Local};
534 use std::time::{SystemTime, UNIX_EPOCH};
535
536 #[test]
537 fn test_parse_xml() {
538 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>"#;
539 let parser = XmlParser::new(xml);
540 let contents = parser.parse();
541 let json_data = serde_json::to_string_pretty(&contents).unwrap();
542 println!("{}", json_data);
543 }
544
545 #[test]
546 fn test_timestamp() {
547 let now = Local::now();
548 let two_hours = Duration::hours(2);
549 let future_time = now + two_hours;
550
551 let system_time: SystemTime = future_time.into();
552 let duration = system_time.duration_since(UNIX_EPOCH).unwrap();
553 let timestamp = duration.as_secs();
554 println!("timestamp = {}", timestamp);
555 }
556
557}