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 .unwrap_or(String::from("application/octet-stream"));
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 obj_key = &Self::urlencode(obj_key);
313 let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);
315
316 let md5_string = "";
317
318 let date = utils::now_str_gmt();
320
321 let hmacsha1 = HmacSha1();
323
324 let string_to_sign = hmacsha1.header_string_to_sign("DELETE", &md5_string, "", &date, "", &format!("/{}/{}", self.bucket, obj_key));
326
327 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
329
330 let authorization = format!("OBS {}:{}", self.ak, signature);
332
333 let client = reqwest::Client::new();
335 let res = client.delete(url)
336 .header("Date", &date)
337 .header("Authorization", authorization)
338 .send()
339 .await;
340
341 let res = match res {
342 Ok(response) => response,
343 Err(e) => {
344 return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e)));
345 },
346 };
347 let status = res.status();
348 println!("status = {}, {}", status, res.text_with_charset("utf-8").await?);
349
350 Ok(())
351 }
352
353 pub async fn download_file(&self, obj_key: &str, file_path: &str, overwrite: bool) -> Result<(), Box<dyn std::error::Error>> {
360 let file_path = Path::new(file_path);
361
362 if file_path.exists() && !overwrite {
364 return Err(Box::new(Error::new(ErrorKind::AlreadyExists, "文件已存在,请删除文件或设置覆盖参数")));
365 }
366
367 let parent = file_path.parent().unwrap();
369 if !parent.exists() {
370 fs::create_dir_all(&parent)?;
371 }
372
373 let data = self.download_object(obj_key).await?;
375
376 fs::write(file_path, data)?;
378 Ok(())
379 }
380
381 pub fn url_sign(&self, url_str: &str) -> Result<String, Box<dyn std::error::Error>> {
382 let obs_object_url = Url::parse(url_str)?;
383 let resource_part = obs_object_url.path();
384 let host = obs_object_url.host().unwrap();
385 let domain = match host {
386 url::Host::Domain(domain) => domain.to_string(),
387 _ => format!("{}.{}", self.bucket, self.endpoint)
388 };
389 let parts: Vec<&str> = domain.split(".").collect();
390 let bucket_name = parts[0];
391
392 let timestamp = utils::timestamp(Local::now(), 3600*2);
393
394 let expires = format!("{}", timestamp);
396
397 let hmacsha1 = HmacSha1();
399
400 let string_to_sign = hmacsha1.url_string_to_sign("GET", "", "", &expires, "", &format!("/{}{}", bucket_name, resource_part));
402
403 let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
405 let signature = form_urlencoded::byte_serialize(signature.as_bytes()).collect::<String>();
406
407 let sign_url = format!("{}?AccessKeyId={}&Expires={}&Signature={}", url_str, self.ak, expires, signature);
409 Ok(sign_url)
410 }
411
412 fn urlencode(s: &str) -> String {
413 let tokens: Vec<String> = s.split("/").map(|token| {
414 encode(token).to_string()
415 }).collect();
416 tokens.join("/")
417 }
418
419}
420
421
422#[derive(Serialize, Deserialize, Debug)]
426pub struct ObjectMeta {
427
428 pub key: String,
432
433 pub last_modified: String,
437
438 pub etag: String,
442
443 pub size: u64,
447
448 pub storage_class: String,
452}
453
454struct XmlParser {
458 xml: String,
459}
460
461
462impl XmlParser {
463 fn new(xml: &str) -> Self {
464 XmlParser { xml: xml.to_string() }
465 }
466
467 fn parse(&self) -> Vec<ObjectMeta> {
471 let xml = &self.xml;
472
473 let contents_re = Regex::new(r#"<Contents>(.*?)</Contents>"#).unwrap();
475 let key_regex = Regex::new(r#"<Key>(.*?)</Key>"#).unwrap();
476 let last_modified_regex = Regex::new(r#"<LastModified>(.*?)</LastModified>"#).unwrap();
477 let etag_regex = Regex::new(r#"<ETag>(.*?)</ETag>"#).unwrap();
478 let size_regex = Regex::new(r#"<Size>(.*?)</Size>"#).unwrap();
479 let storage_class_regex = Regex::new(r#"<StorageClass>(.*?)</StorageClass>"#).unwrap();
480
481
482 let mut contents_vec = Vec::new();
484 for captures in contents_re.captures_iter(xml) {
485 let inner_content = &captures[1];
486
487 let key = key_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
488 let last_modified = last_modified_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
489 let etag = etag_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
490 let size = size_regex.captures(inner_content).and_then(|cap| cap[1].parse().ok()).unwrap_or(0);
491 let storage_class = storage_class_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
492 let content = ObjectMeta {
493 key,
494 last_modified,
495 etag,
496 size,
497 storage_class,
498 };
499 contents_vec.push(content);
500 }
501
502 contents_vec
503 }
504}
505
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use chrono::{Duration, Local};
511 use std::time::{SystemTime, UNIX_EPOCH};
512
513 #[test]
514 fn test_parse_xml() {
515 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>"#;
516 let parser = XmlParser::new(xml);
517 let contents = parser.parse();
518 let json_data = serde_json::to_string_pretty(&contents).unwrap();
519 println!("{}", json_data);
520 }
521
522 #[test]
523 fn test_timestamp() {
524 let now = Local::now();
525 let two_hours = Duration::hours(2);
526 let future_time = now + two_hours;
527
528 let system_time: SystemTime = future_time.into();
529 let duration = system_time.duration_since(UNIX_EPOCH).unwrap();
530 let timestamp = duration.as_secs();
531 println!("timestamp = {}", timestamp);
532 }
533
534}