├── .gitignore ├── Cargo.toml ├── Readme.md ├── example ├── .gitignore ├── Cargo.toml └── src │ └── main.rs └── src ├── async_object.rs ├── async_service.rs ├── auth.rs ├── errors.rs ├── lib.rs ├── multi_part.rs ├── object.rs ├── oss.rs ├── prelude.rs ├── service.rs └── utils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | tests -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oss-rust-sdk" 3 | version = "0.10.0" 4 | authors = ["NoXF "] 5 | edition = "2021" 6 | readme = "README.md" 7 | description = "Aliyun OSS SDK" 8 | license = "Apache-2.0" 9 | repository = "https://github.com/NoXF/oss-rust-sdk" 10 | 11 | [dependencies] 12 | reqwest = { version = "0.11.13", features = ["blocking"], default-features = false, optional = true } 13 | base64 = "0.13" 14 | chrono = "0.4.20" 15 | log = "0.4.17" 16 | quick-xml = { version = "0.28.0", features = ["serialize"] } 17 | derive_more = "0.99.5" 18 | bytes = "1.0" 19 | async-trait = "0.1.53" 20 | httpdate = "1.0.2" 21 | hmac = "0.12" 22 | sha1 = "0.10" 23 | serde = { version = "1.0.156", features = ["derive"] } 24 | urlencoding = "2.1.3" 25 | 26 | [dev-dependencies] 27 | tokio = { version = "1.1", features = ["full"] } 28 | 29 | [features] 30 | default = [ "native-tls" ] 31 | native-tls = [ "reqwest/default-tls" ] 32 | rustls-tls = [ "reqwest/rustls-tls" ] 33 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # OSS-Rust-SDK 2 | 3 | [![Latest Version](https://img.shields.io/crates/v/oss-rust-sdk.svg)](https://crates.io/crates/oss-rust-sdk) 4 | 5 | It's an unofficial Rust port for https://github.com/aliyun/aliyun-oss-cpp-sdk, just implement core APIs of https://help.aliyun.com/document_detail/31977.html, everyone is welcome to submit a PR to implement which interface you need. 6 | 7 | # Getting Started 8 | 9 | ## List your_Buckets 10 | ```rust 11 | use oss_rust_sdk::prelude::*; 12 | let oss_instance = OSS::new("your_AccessKeyId", "your_AccessKeySecret", "your_Endpoint", "your_Bucket"); 13 | let list_buckets = oss_instance.list_bucket(None).unwrap(); 14 | 15 | let id = list_buckets.id(); 16 | let your_Buckets = list_buckets.buckets(); 17 | let your_Bucket_names: Vec<&str> = your_Buckets.iter().map(|obj| obj.name()).collect(); 18 | ``` 19 | 20 | ## Get Object 21 | ```rust 22 | use oss_rust_sdk::prelude::*; 23 | use std::collections::HashMap; 24 | 25 | let oss_instance = OSS::new("your_AccessKeyId", "your_AccessKeySecret", "your_Endpoint", "your_Bucket"); 26 | 27 | /// if have extra header 28 | let mut extra_header = HashMap::new(); 29 | extra_header.insert("content-type", "text/plain"); 30 | /// if have oss_sub_resource 31 | let mut oss_sub_resource = HashMap::new(); 32 | oss_sub_resource.insert("acl", None); 33 | oss_sub_resource.insert("response-content-type", Some("ContentType")); 34 | 35 | let result = oss_instance.get_object("object", extar_header, oss_sub_resource); 36 | /// or you may just get object 37 | /// let result = oss_instance.get_object("object", None, None); 38 | assert_eq!(result.is_ok(), true); 39 | let buffer = result.unwrap(); 40 | ``` 41 | 42 | ## Get Object async 43 | ```rust 44 | use oss_rust_sdk::oss::OSS; 45 | use oss_rust_sdk::async_object::*; 46 | use tokio::runtime::Runtime; 47 | 48 | fn async_get_object_demo() { 49 | let oss_instance = OSS::new("your_AccessKeyId", "your_AccessKeySecret", "your_Endpoint", "your_Bucket"); 50 | 51 | let mut rt = Runtime::new().expect("failed to start runtime"); 52 | 53 | rt.block_on(async move { 54 | let _ = oss_instance.get_object("objectName", None::>, None).await.unwrap(); 55 | println!("buffer = {:?}", String::from_utf8(result.unwrap())); 56 | }); 57 | } 58 | 59 | or 60 | 61 | async fn async_get_object_demo() -> Reuslt { 62 | let oss_instance = OSS::new("your_AccessKeyId", "your_AccessKeySecret", "your_Endpoint", "your_Bucket"); 63 | let buf = oss_instance.async_get_object("objectName", None, None).await?; 64 | String::from_utf8(buf)? 65 | } 66 | ``` 67 | 68 | ## Put Object by file 69 | ```rust 70 | use oss_rust_sdk::prelude::*; 71 | let filename = "filename"; 72 | let oss_instance = OSS::new("your_AccessKeyId", "your_AccessKeySecret", "your_Endpoint", "your_Bucket"); 73 | let result = oss_instance.put_object_from_file(filename, "object", None, None); 74 | assert_eq!(result.is_ok(), true) 75 | ``` 76 | 77 | ## Put Ojbect by buffer 78 | ```rust 79 | let buffer = "some thing you want put to oss"; 80 | let oss_instance = OSS::new("your_AccessKeyId", "your_AccessKeySecret", "your_Endpoint", "your_Bucket"); 81 | let result = oss_instance.put_object_from_buffer(buffer.as_bytes(), "object", None, None); 82 | assert_eq!(result.is_ok(), true) 83 | ``` 84 | 85 | ## Pub Object Async 86 | ```rust 87 | use oss_rust_sdk::oss::OSS; 88 | use oss_rust_sdk::async_object::*; 89 | 90 | let buffer = "test async put object from buffer"; 91 | let oss_instance = OSS::new("your_AccessKeyId", "your_AccessKeySecret", "your_Endpoint", "your_Bucket"); 92 | let mut headers = HashMap::new(); 93 | headers.insert("content-type", "text/plain"); 94 | 95 | oss_instance.put_object(buffer.as_bytes(),"your_object_name", headers,None).await?; 96 | ``` 97 | 98 | ## Copy Object 99 | ```rust 100 | use oss_rust_sdk::prelude::*; 101 | let oss_instance = OSS::new("your_AccessKeyId", "your_AccessKeySecret", "your_Endpoint", "your_Bucket"); 102 | let result = oss_instance.copy_object_from_object("src_object", "dest_object", None, None); 103 | assert_eq!(result.is_ok(), true) 104 | ``` 105 | 106 | ## Delete Ojbect 107 | ```rust 108 | use oss_rust_sdk::prelude::*; 109 | let oss_instance = OSS::new("your_AccessKeyId", "your_AccessKeySecret", "your_Endpoint", "your_Bucket"); 110 | let result = oss_instance.delete_object("object"); 111 | assert_eq!(result.is_ok(), true) 112 | ``` 113 | 114 | You can use `oss_instance.set_bucket("your_Bucket")` to change specific bucket after create the oss instance. 115 | 116 | ## License 117 | 118 | - Apache License 2.0. 119 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | tests 5 | -------------------------------------------------------------------------------- /example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | version = "0.1.0" 4 | authors = ["神龙 "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | oss-rust-sdk = { path = "../" } 9 | tokio = { version = "1.18", features = ["full"] } 10 | -------------------------------------------------------------------------------- /example/src/main.rs: -------------------------------------------------------------------------------- 1 | use oss_rust_sdk::oss::*; 2 | use std::collections::HashMap; 3 | use std::str; 4 | use tokio::runtime::Runtime; 5 | 6 | fn main() { 7 | get_object(); 8 | async_get_object(); 9 | list_object(); 10 | async_list_object(); 11 | } 12 | 13 | fn get_object() { 14 | use oss_rust_sdk::object::*; 15 | 16 | // use your own oss config 17 | let oss_instance = OSS::new( 18 | "your_AccessKeyId", 19 | "your_AccessKeySecret", 20 | "your_Endpoint", 21 | "your_Bucket", 22 | ); 23 | 24 | let result = oss_instance.get_object("objectName", None::>, None); 25 | assert_eq!(result.is_ok(), true); 26 | println!("text = {:?}", String::from_utf8(result.unwrap())); 27 | } 28 | 29 | fn async_get_object() { 30 | use oss_rust_sdk::async_object::*; 31 | 32 | // use your own oss config 33 | let oss_instance = OSS::new( 34 | "your_AccessKeyId", 35 | "your_AccessKeySecret", 36 | "your_Endpoint", 37 | "your_Bucket", 38 | ); 39 | 40 | let mut rt = Runtime::new().expect("failed to start runtime"); 41 | 42 | rt.block_on(async move { 43 | let buf = oss_instance 44 | .get_object("objectName", None::>, None) 45 | .await 46 | .unwrap(); 47 | println!("buffer = {:?}", String::from_utf8(buf.to_vec())); 48 | }); 49 | } 50 | 51 | fn async_list_object() { 52 | use oss_rust_sdk::async_object::*; 53 | 54 | // use your own oss config 55 | let oss_instance = OSS::new( 56 | "your_AccessKeyId", 57 | "your_AccessKeySecret", 58 | "your_Endpoint", 59 | "your_Bucket", 60 | ); 61 | let mut params = HashMap::new(); 62 | params.insert("max-keys", Some("5")); 63 | 64 | let mut rt = Runtime::new().expect("failed to start runtime"); 65 | rt.block_on(async move { 66 | let result = oss_instance.list_object(None, params).await.unwrap(); 67 | 68 | for object in result.contents() { 69 | dbg!(&object.key()); 70 | } 71 | }); 72 | } 73 | 74 | fn list_object() { 75 | use oss_rust_sdk::object::*; 76 | 77 | // use your own oss config 78 | let oss_instance = OSS::new( 79 | "your_AccessKeyId", 80 | "your_AccessKeySecret", 81 | "your_Endpoint", 82 | "your_Bucket", 83 | ); 84 | 85 | let mut params = HashMap::new(); 86 | params.insert("max-keys", Some("5")); 87 | let result = oss_instance.list_object(None, params).unwrap(); 88 | 89 | for object in result.contents() { 90 | dbg!(&object.key()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/async_object.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{ 4 | multi_part::{CompleteMultipartUploadResult, InitiateMultipartUploadResult}, 5 | oss::{ObjectMeta, RequestType}, 6 | prelude::{ListObjects, OSS}, 7 | }; 8 | 9 | use super::errors::{Error, ObjectError}; 10 | 11 | use async_trait::async_trait; 12 | use bytes::Bytes; 13 | 14 | #[async_trait] 15 | pub trait AsyncObjectAPI { 16 | async fn list_object(&self, headers: H, resources: R) -> Result 17 | where 18 | S: AsRef, 19 | H: Into>> + Send, 20 | R: Into>>> + Send; 21 | 22 | async fn get_object( 23 | &self, 24 | object_name: S1, 25 | headers: H, 26 | resources: R, 27 | ) -> Result 28 | where 29 | S1: AsRef + Send, 30 | S2: AsRef + Send, 31 | H: Into>> + Send, 32 | R: Into>>> + Send; 33 | 34 | async fn put_object( 35 | &self, 36 | buf: &[u8], 37 | object_name: S1, 38 | headers: H, 39 | resources: R, 40 | ) -> Result<(), Error> 41 | where 42 | S1: AsRef + Send, 43 | S2: AsRef + Send, 44 | H: Into>> + Send, 45 | R: Into>>> + Send; 46 | 47 | async fn copy_object_from_object( 48 | &self, 49 | src: S1, 50 | dest: S2, 51 | headers: H, 52 | resources: R, 53 | ) -> Result<(), Error> 54 | where 55 | S1: AsRef + Send, 56 | S2: AsRef + Send, 57 | S3: AsRef + Send, 58 | H: Into>> + Send, 59 | R: Into>>> + Send; 60 | 61 | async fn delete_object(&self, object_name: S) -> Result<(), Error> 62 | where 63 | S: AsRef + Send; 64 | 65 | async fn head_object(&self, object_name: S) -> Result 66 | where 67 | S: AsRef + Send; 68 | 69 | /// Notify oss to init a Multipart Upload event 70 | async fn init_multi( 71 | &self, 72 | object_name: S1, 73 | headers: H, 74 | resources: R, 75 | ) -> Result 76 | where 77 | S1: AsRef + Send, 78 | S2: AsRef + Send, 79 | H: Into>> + Send, 80 | R: Into>>> + Send; 81 | 82 | /// Upload data in chunks according to the specified Object name and uploadId 83 | async fn upload_part( 84 | &self, 85 | buf: &[u8], 86 | object_name: S1, 87 | headers: H, 88 | resources: R, 89 | ) -> Result 90 | where 91 | S1: AsRef + Send, 92 | S2: AsRef + Send, 93 | H: Into>> + Send, 94 | R: Into>>> + Send; 95 | 96 | /// Complete the multipart upload of the entire file 97 | /// 98 | /// body format 99 | /// 100 | /// 101 | /// PartNumber 102 | /// ETag 103 | /// 104 | /// ... 105 | /// 106 | /// 107 | /// # Examples 108 | /// 109 | /// #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] 110 | /// #[serde(rename_all = "PascalCase")] 111 | /// pub struct PartWrapper { 112 | /// pub part: Vec, 113 | /// } 114 | /// 115 | /// #[derive(Debug, Serialize, Deserialize, PartialEq)] 116 | /// #[serde(rename_all = "PascalCase")] 117 | /// pub struct Part { 118 | /// part_number: usize, 119 | /// e_tag: String, 120 | /// } 121 | /// 122 | /// let parts = CompleteDTO { 123 | /// part: vec![Part { 124 | /// part_number: 1, 125 | /// e_tag: "50BE5FACC702C5B945588031C6*****".to_string(), 126 | /// }], 127 | /// }; 128 | /// 129 | /// let body = quick_xml::se::to_string_with_root("CompleteMultipartUpload", &parts).unwrap(); 130 | /// 131 | async fn complete_multi( 132 | &self, 133 | body: String, 134 | object_name: S1, 135 | headers: H, 136 | resources: R, 137 | ) -> Result 138 | where 139 | S1: AsRef + Send, 140 | S2: AsRef + Send, 141 | H: Into>> + Send, 142 | R: Into>>> + Send; 143 | 144 | /// Cancel the MultipartUpload event and delete the corresponding Part data 145 | async fn abort_multi( 146 | &self, 147 | object_name: S1, 148 | headers: H, 149 | resources: R, 150 | ) -> Result<(), Error> 151 | where 152 | S1: AsRef + Send, 153 | S2: AsRef + Send, 154 | H: Into>> + Send, 155 | R: Into>>> + Send; 156 | } 157 | 158 | #[async_trait] 159 | impl<'a> AsyncObjectAPI for OSS<'a> { 160 | async fn list_object(&self, headers: H, resources: R) -> Result 161 | where 162 | S: AsRef, 163 | H: Into>> + Send, 164 | R: Into>>> + Send, 165 | { 166 | let (host, headers) = 167 | self.build_request(RequestType::Get, String::new(), headers, resources)?; 168 | 169 | let resp = self.http_client.get(host).headers(headers).send().await?; 170 | let body = resp.text().await?; 171 | let list_objects = quick_xml::de::from_str::(&body)?; 172 | 173 | Ok(list_objects) 174 | } 175 | 176 | async fn get_object( 177 | &self, 178 | object_name: S1, 179 | headers: H, 180 | resources: R, 181 | ) -> Result 182 | where 183 | S1: AsRef + Send, 184 | S2: AsRef + Send, 185 | H: Into>> + Send, 186 | R: Into>>> + Send, 187 | { 188 | let (host, headers) = 189 | self.build_request(RequestType::Get, object_name, headers, resources)?; 190 | 191 | let resp = self.http_client.get(&host).headers(headers).send().await?; 192 | 193 | if resp.status().is_success() { 194 | Ok(resp.bytes().await?) 195 | } else { 196 | Err(Error::Object(ObjectError::GetError { 197 | msg: format!("can not get object, status code: {}", resp.status()).into(), 198 | })) 199 | } 200 | } 201 | 202 | async fn put_object( 203 | &self, 204 | buf: &[u8], 205 | object_name: S1, 206 | headers: H, 207 | resources: R, 208 | ) -> Result<(), Error> 209 | where 210 | S1: AsRef + Send, 211 | S2: AsRef + Send, 212 | H: Into>> + Send, 213 | R: Into>>> + Send, 214 | { 215 | let (host, headers) = 216 | self.build_request(RequestType::Put, object_name, headers, resources)?; 217 | 218 | let resp = self 219 | .http_client 220 | .put(&host) 221 | .headers(headers) 222 | .body(buf.to_owned()) 223 | .send() 224 | .await?; 225 | 226 | if resp.status().is_success() { 227 | Ok(()) 228 | } else { 229 | Err(Error::Object(ObjectError::DeleteError { 230 | msg: format!( 231 | "can not put object, status code, status code: {}", 232 | resp.status() 233 | ) 234 | .into(), 235 | })) 236 | } 237 | } 238 | 239 | async fn copy_object_from_object( 240 | &self, 241 | src: S1, 242 | dest: S2, 243 | headers: H, 244 | resources: R, 245 | ) -> Result<(), Error> 246 | where 247 | S1: AsRef + Send, 248 | S2: AsRef + Send, 249 | S3: AsRef + Send, 250 | H: Into>> + Send, 251 | R: Into>>> + Send, 252 | { 253 | let (host, mut headers) = self.build_request(RequestType::Put, dest, headers, resources)?; 254 | headers.insert("x-oss-copy-source", src.as_ref().parse()?); 255 | 256 | let resp = self.http_client.put(&host).headers(headers).send().await?; 257 | 258 | if resp.status().is_success() { 259 | Ok(()) 260 | } else { 261 | Err(Error::Object(ObjectError::CopyError { 262 | msg: format!("can not copy object, status code: {}", resp.status()).into(), 263 | })) 264 | } 265 | } 266 | 267 | async fn delete_object(&self, object_name: S) -> Result<(), Error> 268 | where 269 | S: AsRef + Send, 270 | { 271 | let headers = HashMap::::new(); 272 | let (host, headers) = 273 | self.build_request(RequestType::Delete, object_name, Some(headers), None)?; 274 | 275 | let resp = self 276 | .http_client 277 | .delete(&host) 278 | .headers(headers) 279 | .send() 280 | .await?; 281 | 282 | if resp.status().is_success() { 283 | Ok(()) 284 | } else { 285 | Err(Error::Object(ObjectError::DeleteError { 286 | msg: format!("can not delete object, status code: {}", resp.status()).into(), 287 | })) 288 | } 289 | } 290 | 291 | async fn head_object(&self, object_name: S) -> Result 292 | where 293 | S: AsRef + Send, 294 | { 295 | let (host, headers) = self.build_request( 296 | RequestType::Head, 297 | object_name, 298 | None::>, 299 | None, 300 | )?; 301 | 302 | let resp = self.http_client.head(&host).headers(headers).send().await?; 303 | 304 | if resp.status().is_success() { 305 | Ok(ObjectMeta::from_header_map(resp.headers())?) 306 | } else { 307 | Err(Error::Object(ObjectError::DeleteError { 308 | msg: format!("can not head object, status code: {}", resp.status()).into(), 309 | })) 310 | } 311 | } 312 | 313 | async fn init_multi( 314 | &self, 315 | object_name: S1, 316 | headers: H, 317 | resources: R, 318 | ) -> Result 319 | where 320 | S1: AsRef + Send, 321 | S2: AsRef + Send, 322 | H: Into>> + Send, 323 | R: Into>>> + Send, 324 | { 325 | let (host, headers) = 326 | self.build_request(RequestType::Post, object_name, headers, resources)?; 327 | 328 | let resp = self.http_client.post(&host).headers(headers).send().await?; 329 | 330 | if resp.status().is_success() { 331 | let body = resp.text().await?; 332 | let res = quick_xml::de::from_str::(&body)?; 333 | Ok(res) 334 | } else { 335 | Err(Error::Object(ObjectError::PostError { 336 | msg: format!( 337 | "init multi failed, status code, status code: {}", 338 | resp.status() 339 | ) 340 | .into(), 341 | })) 342 | } 343 | } 344 | 345 | async fn upload_part( 346 | &self, 347 | buf: &[u8], 348 | object_name: S1, 349 | headers: H, 350 | resources: R, 351 | ) -> Result 352 | where 353 | S1: AsRef + Send, 354 | S2: AsRef + Send, 355 | H: Into>> + Send, 356 | R: Into>>> + Send, 357 | { 358 | let (host, headers) = 359 | self.build_request(RequestType::Put, object_name, headers, resources)?; 360 | 361 | let resp = self 362 | .http_client 363 | .put(&host) 364 | .headers(headers) 365 | .body(buf.to_owned()) 366 | .send() 367 | .await?; 368 | 369 | if resp.status().is_success() { 370 | let e_tag = resp.headers().get("ETag").unwrap().to_str().unwrap(); 371 | Ok(e_tag.to_string()) 372 | } else { 373 | Err(Error::Object(ObjectError::PutError { 374 | msg: format!( 375 | "can not put object, status code, status code: {}", 376 | resp.status() 377 | ) 378 | .into(), 379 | })) 380 | } 381 | } 382 | 383 | async fn complete_multi( 384 | &self, 385 | body: String, 386 | object_name: S1, 387 | headers: H, 388 | resources: R, 389 | ) -> Result 390 | where 391 | S1: AsRef + Send, 392 | S2: AsRef + Send, 393 | H: Into>> + Send, 394 | R: Into>>> + Send, 395 | { 396 | let (host, headers) = 397 | self.build_request(RequestType::Post, object_name, headers, resources)?; 398 | 399 | let resp = self 400 | .http_client 401 | .post(&host) 402 | .headers(headers) 403 | .body(body) 404 | .send() 405 | .await?; 406 | 407 | if resp.status().is_success() { 408 | let body = resp.text().await?; 409 | let res = quick_xml::de::from_str::(&body)?; 410 | Ok(res) 411 | } else { 412 | Err(Error::Object(ObjectError::PostError { 413 | msg: format!( 414 | "complete multi failed, status code, status code: {}", 415 | resp.status() 416 | ) 417 | .into(), 418 | })) 419 | } 420 | } 421 | 422 | async fn abort_multi( 423 | &self, 424 | object_name: S1, 425 | headers: H, 426 | resources: R, 427 | ) -> Result<(), Error> 428 | where 429 | S1: AsRef + Send, 430 | S2: AsRef + Send, 431 | H: Into>> + Send, 432 | R: Into>>> + Send, 433 | { 434 | let (host, headers) = 435 | self.build_request(RequestType::Delete, object_name, headers, resources)?; 436 | 437 | let resp = self 438 | .http_client 439 | .delete(&host) 440 | .headers(headers) 441 | .send() 442 | .await?; 443 | 444 | if resp.status().is_success() { 445 | Ok(()) 446 | } else { 447 | Err(Error::Object(ObjectError::DeleteError { 448 | msg: format!( 449 | "abort multi failed, status code, status code: {}", 450 | resp.status() 451 | ) 452 | .into(), 453 | })) 454 | } 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /src/async_service.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use reqwest::header::{HeaderMap, DATE}; 3 | use std::collections::HashMap; 4 | 5 | use crate::prelude::ListBuckets; 6 | 7 | use super::auth::*; 8 | use super::errors::Error; 9 | use super::oss::OSS; 10 | 11 | #[derive(Clone, Debug)] 12 | pub struct Bucket { 13 | name: String, 14 | creation_date: String, 15 | location: String, 16 | extranet_endpoint: String, 17 | intranet_endpoint: String, 18 | storage_class: String, 19 | } 20 | 21 | impl Bucket { 22 | pub fn new( 23 | name: String, 24 | creation_date: String, 25 | location: String, 26 | extranet_endpoint: String, 27 | intranet_endpoint: String, 28 | storage_class: String, 29 | ) -> Self { 30 | Bucket { 31 | name, 32 | creation_date, 33 | location, 34 | extranet_endpoint, 35 | intranet_endpoint, 36 | storage_class, 37 | } 38 | } 39 | 40 | pub fn name(&self) -> &str { 41 | &self.name 42 | } 43 | 44 | pub fn creation_date(&self) -> &str { 45 | &self.creation_date 46 | } 47 | 48 | pub fn location(&self) -> &str { 49 | &self.location 50 | } 51 | 52 | pub fn extranet_endpoint(&self) -> &str { 53 | &self.extranet_endpoint 54 | } 55 | 56 | pub fn intranet_endpoint(&self) -> &str { 57 | &self.intranet_endpoint 58 | } 59 | 60 | pub fn storage_class(&self) -> &str { 61 | &self.storage_class 62 | } 63 | } 64 | 65 | #[async_trait] 66 | pub trait ServiceAPI { 67 | async fn list_bucket(&self, resources: R) -> Result 68 | where 69 | S: AsRef + Send, 70 | R: Into>>> + Send; 71 | } 72 | 73 | #[async_trait] 74 | impl<'a> ServiceAPI for OSS<'a> { 75 | async fn list_bucket(&self, resources: R) -> Result 76 | where 77 | S: AsRef + Send, 78 | R: Into>>> + Send, 79 | { 80 | let resources_str = if let Some(r) = resources.into() { 81 | self.get_resources_str(&r) 82 | } else { 83 | String::new() 84 | }; 85 | let host = self.endpoint(); 86 | let date = self.date(); 87 | 88 | let mut headers = HeaderMap::new(); 89 | headers.insert(DATE, date.parse()?); 90 | let authorization = self.oss_sign( 91 | "GET", 92 | self.key_id(), 93 | self.key_secret(), 94 | "", 95 | "", 96 | &resources_str, 97 | &headers, 98 | ); 99 | headers.insert("Authorization", authorization.parse()?); 100 | 101 | let resp = self.http_client.get(host).headers(headers).send().await?; 102 | 103 | let body = resp.text().await?; 104 | let list_buckets = quick_xml::de::from_str::(&body)?; 105 | 106 | Ok(list_buckets) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/auth.rs: -------------------------------------------------------------------------------- 1 | use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; 2 | use reqwest::header::{CONTENT_TYPE, DATE}; 3 | 4 | use base64::encode; 5 | use hmac::{Hmac, Mac}; 6 | 7 | type HmacSha1 = Hmac; 8 | 9 | use super::oss::OSS; 10 | 11 | pub trait Auth { 12 | fn sign( 13 | &self, 14 | verb: &str, 15 | key_secret: &str, 16 | bucket: &str, 17 | object: &str, 18 | oss_resources: &str, 19 | headers: &HeaderMap, 20 | ) -> String; 21 | 22 | fn oss_sign( 23 | &self, 24 | verb: &str, 25 | key_id: &str, 26 | key_secret: &str, 27 | bucket: &str, 28 | object: &str, 29 | oss_resources: &str, 30 | headers: &HeaderMap, 31 | ) -> String; 32 | } 33 | 34 | impl<'a> Auth for OSS<'a> { 35 | fn sign( 36 | &self, 37 | verb: &str, 38 | key_secret: &str, 39 | bucket: &str, 40 | object: &str, 41 | oss_resources: &str, 42 | headers: &HeaderMap, 43 | ) -> String { 44 | let date = headers 45 | .get(DATE) 46 | .and_then(|d| Some(d.to_str().unwrap_or_default())) 47 | .unwrap_or_default(); 48 | let content_type = headers 49 | .get(CONTENT_TYPE) 50 | .and_then(|c| Some(c.to_str().unwrap_or_default())) 51 | .unwrap_or_default(); 52 | let content_md5 = headers 53 | .get("Content-MD5") 54 | .and_then(|md5| Some(encode(md5.to_str().unwrap_or_default()))) 55 | .unwrap_or_default(); 56 | 57 | let mut oss_headers: Vec<(&HeaderName, &HeaderValue)> = headers 58 | .iter() 59 | .filter(|(k, _)| k.as_str().contains("x-oss-")) 60 | .collect(); 61 | oss_headers.sort_by(|a, b| a.0.to_string().cmp(&b.0.to_string())); 62 | let mut oss_headers_str = String::new(); 63 | for (k, v) in oss_headers { 64 | oss_headers_str += &format!( 65 | "{}:{}\n", 66 | k.to_owned().as_str(), 67 | v.to_owned().to_str().unwrap_or("") 68 | ); 69 | } 70 | 71 | let oss_resource_str = get_oss_resource_str(bucket, object, oss_resources); 72 | let sign_str = format!( 73 | "{}\n{}\n{}\n{}\n{}{}", 74 | verb, content_md5, content_type, date, oss_headers_str, oss_resource_str 75 | ); 76 | 77 | let mut hasher = HmacSha1::new_from_slice(key_secret.as_bytes()) 78 | .expect("Hmac can take key of any size, should not happned"); 79 | hasher.update(sign_str.as_bytes()); 80 | 81 | encode(&hasher.finalize().into_bytes()) 82 | } 83 | 84 | fn oss_sign( 85 | &self, 86 | verb: &str, 87 | key_id: &str, 88 | key_secret: &str, 89 | bucket: &str, 90 | object: &str, 91 | oss_resources: &str, 92 | headers: &HeaderMap, 93 | ) -> String { 94 | let sign_str_base64 = self.sign(verb, key_secret, bucket, object, oss_resources, headers); 95 | let authorization = format!("OSS {}:{}", key_id, sign_str_base64); 96 | debug!("authorization: {}", authorization); 97 | authorization 98 | } 99 | } 100 | 101 | #[inline] 102 | fn get_oss_resource_str(bucket: &str, object: &str, oss_resources: &str) -> String { 103 | let oss_resources = if oss_resources != "" { 104 | String::from("?") + oss_resources 105 | } else { 106 | String::new() 107 | }; 108 | if bucket == "" { 109 | format!("/{}{}", bucket, oss_resources) 110 | } else { 111 | format!("/{}/{}{}", bucket, object, oss_resources) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use quick_xml::DeError; 2 | use quick_xml::Error as QxmlError; 3 | use reqwest::header::InvalidHeaderName as HttpInvalidHeaderNameError; 4 | use reqwest::header::InvalidHeaderValue as HttpInvalidHeaderValueError; 5 | use reqwest::Error as ReqwestError; 6 | use std::error::Error as StdError; 7 | use std::io::Error as IoError; 8 | use std::string::FromUtf8Error; 9 | 10 | #[derive(Debug, Display)] 11 | pub enum Error { 12 | Object(ObjectError), 13 | Io(IoError), 14 | String(FromUtf8Error), 15 | Reqwest(ReqwestError), 16 | Qxml(QxmlError), 17 | Http(HttpError), 18 | DeserializeError(DeError), 19 | } 20 | 21 | #[derive(Debug, Display)] 22 | pub enum HttpError { 23 | HttpInvalidHeaderValue(HttpInvalidHeaderValueError), 24 | HttpInvalidHeaderName(HttpInvalidHeaderNameError), 25 | } 26 | 27 | impl From for Error { 28 | fn from(e: QxmlError) -> Error { 29 | Error::Qxml(e) 30 | } 31 | } 32 | 33 | impl From for Error { 34 | fn from(e: IoError) -> Error { 35 | Error::Io(e) 36 | } 37 | } 38 | 39 | impl From for Error { 40 | fn from(e: ReqwestError) -> Error { 41 | Error::Reqwest(e) 42 | } 43 | } 44 | 45 | impl From for Error { 46 | fn from(e: HttpInvalidHeaderValueError) -> Error { 47 | Error::Http(HttpError::HttpInvalidHeaderValue(e)) 48 | } 49 | } 50 | 51 | impl From for Error { 52 | fn from(e: HttpInvalidHeaderNameError) -> Error { 53 | Error::Http(HttpError::HttpInvalidHeaderName(e)) 54 | } 55 | } 56 | 57 | impl From for Error { 58 | fn from(e: FromUtf8Error) -> Error { 59 | Error::String(e) 60 | } 61 | } 62 | 63 | impl From for Error { 64 | fn from(value: DeError) -> Error { 65 | Error::DeserializeError(value) 66 | } 67 | } 68 | 69 | #[derive(Debug, Display)] 70 | pub enum ObjectError { 71 | #[display(fmt = "PUT ERROR: {}", msg)] 72 | PutError { msg: String }, 73 | #[display(fmt = "GET ERROR: {}", msg)] 74 | GetError { msg: String }, 75 | #[display(fmt = "COPY ERROR: {}", msg)] 76 | CopyError { msg: String }, 77 | #[display(fmt = "DELETE ERROR: {}", msg)] 78 | DeleteError { msg: String }, 79 | #[display(fmt = "HEAD ERROR: {}", msg)] 80 | HeadError { msg: String }, 81 | #[display(fmt = "POST ERROR: {}", msg)] 82 | PostError { msg: String }, 83 | } 84 | 85 | impl StdError for Error {} 86 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate derive_more; 3 | #[macro_use] 4 | extern crate log; 5 | 6 | pub mod async_object; 7 | pub mod async_service; 8 | pub mod errors; 9 | pub mod multi_part; 10 | pub mod object; 11 | pub mod oss; 12 | pub mod prelude; 13 | pub mod service; 14 | 15 | mod auth; 16 | mod utils; 17 | -------------------------------------------------------------------------------- /src/multi_part.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Serialize, Deserialize, PartialEq)] 4 | #[serde(rename_all = "PascalCase")] 5 | pub struct InitiateMultipartUploadResult { 6 | pub bucket: String, 7 | pub key: String, 8 | pub upload_id: String, 9 | } 10 | 11 | #[derive(Debug, Serialize, Deserialize, PartialEq)] 12 | #[serde(rename_all = "PascalCase")] 13 | pub struct CompleteMultipartUploadResult { 14 | pub location: String, 15 | pub bucket: String, 16 | pub key: String, 17 | pub e_tag: String, 18 | } -------------------------------------------------------------------------------- /src/object.rs: -------------------------------------------------------------------------------- 1 | use quick_xml::{events::Event, Reader}; 2 | use reqwest::header::{HeaderMap, HeaderValue, DATE}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::collections::HashMap; 5 | 6 | use crate::auth::Auth; 7 | use crate::oss::RequestType; 8 | 9 | use super::errors::{Error, ObjectError}; 10 | use super::oss::OSS; 11 | use super::utils::*; 12 | 13 | #[derive(Clone, Debug, Serialize, Deserialize)] 14 | #[serde(rename_all = "PascalCase")] 15 | pub struct CommonPrefix { 16 | prefix: String, 17 | } 18 | 19 | impl CommonPrefix { 20 | pub fn new(prefix: String) -> Self { 21 | Self { prefix } 22 | } 23 | 24 | pub fn prefix(&self) -> &str { 25 | &self.prefix 26 | } 27 | } 28 | 29 | #[derive(Clone, Debug, Serialize, Deserialize)] 30 | #[serde(rename_all = "PascalCase")] 31 | pub struct ListObjects { 32 | name: String, 33 | delimiter: String, 34 | prefix: String, 35 | marker: String, 36 | max_keys: String, 37 | is_truncated: bool, 38 | 39 | #[serde(default)] 40 | contents: Vec, 41 | #[serde(default)] 42 | common_prefixes: Vec, 43 | } 44 | 45 | impl ListObjects { 46 | pub fn new( 47 | name: String, 48 | delimiter: String, 49 | prefix: String, 50 | marker: String, 51 | max_keys: String, 52 | is_truncated: bool, 53 | 54 | contents: Vec, 55 | common_prefixes: Vec, 56 | ) -> Self { 57 | ListObjects { 58 | name, 59 | delimiter, 60 | prefix, 61 | marker, 62 | max_keys, 63 | is_truncated, 64 | 65 | contents, 66 | common_prefixes, 67 | } 68 | } 69 | 70 | pub fn name(&self) -> &str { 71 | &self.name 72 | } 73 | 74 | pub fn delimiter(&self) -> &str { 75 | &self.delimiter 76 | } 77 | 78 | pub fn prefix(&self) -> &str { 79 | &self.prefix 80 | } 81 | 82 | pub fn marker(&self) -> &str { 83 | &self.marker 84 | } 85 | 86 | pub fn max_keys(&self) -> &str { 87 | &self.max_keys 88 | } 89 | 90 | pub fn is_truncated(&self) -> bool { 91 | self.is_truncated 92 | } 93 | 94 | pub fn contents(&self) -> &Vec { 95 | &self.contents 96 | } 97 | 98 | pub fn common_prefixes(&self) -> &Vec { 99 | &self.common_prefixes 100 | } 101 | } 102 | 103 | #[derive(Clone, Debug, Serialize, Deserialize, Default)] 104 | #[serde(rename_all = "PascalCase")] 105 | pub struct Owner { 106 | #[serde(alias = "ID")] 107 | pub id: String, 108 | pub display_name: String, 109 | } 110 | 111 | #[derive(Clone, Debug, Serialize, Deserialize)] 112 | #[serde(rename_all = "PascalCase")] 113 | pub struct Object { 114 | key: String, 115 | last_modified: String, 116 | size: usize, 117 | e_tag: String, 118 | r#type: String, 119 | storage_class: String, 120 | owner: Owner, 121 | } 122 | 123 | impl Object { 124 | pub fn new( 125 | key: String, 126 | last_modified: String, 127 | size: usize, 128 | 129 | e_tag: String, 130 | r#type: String, 131 | storage_class: String, 132 | owner: Owner, 133 | ) -> Self { 134 | Object { 135 | key, 136 | last_modified, 137 | size, 138 | e_tag, 139 | r#type, 140 | storage_class, 141 | owner, 142 | } 143 | } 144 | 145 | pub fn key(&self) -> &str { 146 | &self.key 147 | } 148 | 149 | pub fn last_modified(&self) -> &str { 150 | &self.last_modified 151 | } 152 | 153 | pub fn size(&self) -> usize { 154 | self.size 155 | } 156 | 157 | pub fn e_tag(&self) -> &str { 158 | &self.e_tag 159 | } 160 | 161 | pub fn r#type(&self) -> &str { 162 | &self.r#type 163 | } 164 | 165 | pub fn storage_class(&self) -> &str { 166 | &self.storage_class 167 | } 168 | 169 | pub fn id(&self) -> &str { 170 | &self.owner.id 171 | } 172 | 173 | pub fn display_name(&self) -> &str { 174 | &self.owner.display_name 175 | } 176 | } 177 | 178 | trait PrivateObjectAPI { 179 | fn generate_presigned_path(&self, object_name: S1, expires: usize) -> String 180 | where 181 | S1: AsRef + Send; 182 | } 183 | 184 | pub trait ObjectAPI { 185 | fn list_object(&self, headers: H, resources: R) -> Result 186 | where 187 | S: AsRef, 188 | H: Into>>, 189 | R: Into>>>; 190 | 191 | fn get_object( 192 | &self, 193 | object_name: S1, 194 | headers: H, 195 | resources: R, 196 | ) -> Result, Error> 197 | where 198 | S1: AsRef, 199 | S2: AsRef, 200 | H: Into>>, 201 | R: Into>>>; 202 | 203 | fn get_object_acl(&self, object_name: S) -> Result 204 | where 205 | S: AsRef; 206 | 207 | fn put_object_from_file( 208 | &self, 209 | file: S1, 210 | object_name: S2, 211 | headers: H, 212 | resources: R, 213 | ) -> Result<(), Error> 214 | where 215 | S1: AsRef, 216 | S2: AsRef, 217 | S3: AsRef, 218 | H: Into>>, 219 | R: Into>>>; 220 | 221 | fn put_object_from_buffer( 222 | &self, 223 | buf: &[u8], 224 | object_name: S1, 225 | headers: H, 226 | resources: R, 227 | ) -> Result<(), Error> 228 | where 229 | S1: AsRef, 230 | S2: AsRef, 231 | H: Into>>, 232 | R: Into>>>; 233 | 234 | fn get_object_signed_url(&self, object_name: S1, expires: usize) -> String 235 | where 236 | S1: AsRef + Send; 237 | 238 | fn copy_object_from_object( 239 | &self, 240 | src: S1, 241 | dest: S2, 242 | headers: H, 243 | resources: R, 244 | ) -> Result<(), Error> 245 | where 246 | S1: AsRef, 247 | S2: AsRef, 248 | S3: AsRef, 249 | H: Into>>, 250 | R: Into>>>; 251 | 252 | fn delete_object(&self, object_name: S) -> Result<(), Error> 253 | where 254 | S: AsRef; 255 | } 256 | 257 | impl<'a> PrivateObjectAPI for OSS<'a> { 258 | fn generate_presigned_path(&self, object_name: S1, expires: usize) -> String 259 | where 260 | S1: AsRef + Send, 261 | { 262 | let object_name = object_name.as_ref(); 263 | let mut headers = HeaderMap::new(); 264 | headers.insert(DATE, HeaderValue::from_str(&expires.to_string()).unwrap()); 265 | let signature = self.sign( 266 | RequestType::Get.as_str(), 267 | self.key_secret(), 268 | self.bucket(), 269 | object_name, 270 | "", 271 | &headers, 272 | ); 273 | format!( 274 | "/{}?Expires={}&OSSAccessKeyId={}&Signature={}", 275 | urlencoding::encode(object_name), 276 | expires, 277 | urlencoding::encode(self.key_id()), 278 | urlencoding::encode(&signature) 279 | ) 280 | } 281 | } 282 | 283 | impl<'a> ObjectAPI for OSS<'a> { 284 | fn list_object(&self, headers: H, resources: R) -> Result 285 | where 286 | S: AsRef, 287 | H: Into>>, 288 | R: Into>>>, 289 | { 290 | let (host, headers) = 291 | self.build_request(RequestType::Get, String::new(), headers, resources)?; 292 | 293 | let resp = reqwest::blocking::Client::new() 294 | .get(&host) 295 | .headers(headers) 296 | .send()?; 297 | 298 | let body = resp.text()?; 299 | let list_objects = quick_xml::de::from_str::(&body)?; 300 | 301 | Ok(list_objects) 302 | } 303 | 304 | fn get_object( 305 | &self, 306 | object_name: S1, 307 | headers: H, 308 | resources: R, 309 | ) -> Result, Error> 310 | where 311 | S1: AsRef, 312 | S2: AsRef, 313 | H: Into>>, 314 | R: Into>>>, 315 | { 316 | let (host, headers) = 317 | self.build_request(RequestType::Get, object_name, headers, resources)?; 318 | 319 | let mut resp = reqwest::blocking::Client::new() 320 | .get(&host) 321 | .headers(headers) 322 | .send()?; 323 | let mut buf: Vec = vec![]; 324 | 325 | if resp.status().is_success() { 326 | resp.copy_to(&mut buf)?; 327 | Ok(buf) 328 | } else { 329 | Err(Error::Object(ObjectError::GetError { 330 | msg: format!("can not get object, status code: {}", resp.status()).into(), 331 | })) 332 | } 333 | } 334 | 335 | fn get_object_acl(&self, object_name: S) -> Result 336 | where 337 | S: AsRef, 338 | { 339 | let object_name = object_name.as_ref(); 340 | let mut params: HashMap<&str, Option<&str>> = HashMap::new(); 341 | params.insert("acl", None); 342 | let result = String::from_utf8(self.get_object(object_name, None, Some(params))?)?; 343 | let mut reader = Reader::from_str(&result); 344 | reader.trim_text(true); 345 | let mut grant = String::new(); 346 | 347 | loop { 348 | match reader.read_event() { 349 | Ok(Event::Start(ref e)) if e.name().as_ref() == b"Grant" => { 350 | grant = reader.read_text(e.name())?.to_string(); 351 | } 352 | Ok(Event::Eof) => break, 353 | Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e), 354 | _ => (), 355 | } 356 | } 357 | 358 | Ok(grant) 359 | } 360 | 361 | fn get_object_signed_url(&self, object_name: S1, expires: usize) -> String 362 | where 363 | S1: AsRef + Send, 364 | { 365 | format!( 366 | "https://{}.{}{}", 367 | self.bucket(), 368 | self.endpoint(), 369 | self.generate_presigned_path(object_name, expires), 370 | ) 371 | } 372 | 373 | fn put_object_from_file( 374 | &self, 375 | file: S1, 376 | object_name: S2, 377 | headers: H, 378 | resources: R, 379 | ) -> Result<(), Error> 380 | where 381 | S1: AsRef, 382 | S2: AsRef, 383 | S3: AsRef, 384 | H: Into>>, 385 | R: Into>>>, 386 | { 387 | let (host, headers) = 388 | self.build_request(RequestType::Put, object_name, headers, resources)?; 389 | 390 | let buf = load_file(file)?; 391 | 392 | let resp = reqwest::blocking::Client::new() 393 | .put(&host) 394 | .headers(headers) 395 | .body(buf) 396 | .send()?; 397 | 398 | if resp.status().is_success() { 399 | Ok(()) 400 | } else { 401 | Err(Error::Object(ObjectError::PutError { 402 | msg: format!("can not put object, status code: {}", resp.status()).into(), 403 | })) 404 | } 405 | } 406 | 407 | fn put_object_from_buffer( 408 | &self, 409 | buf: &[u8], 410 | object_name: S1, 411 | headers: H, 412 | resources: R, 413 | ) -> Result<(), Error> 414 | where 415 | S1: AsRef, 416 | S2: AsRef, 417 | H: Into>>, 418 | R: Into>>>, 419 | { 420 | let (host, headers) = 421 | self.build_request(RequestType::Put, object_name, headers, resources)?; 422 | 423 | let resp = reqwest::blocking::Client::new() 424 | .put(&host) 425 | .headers(headers) 426 | .body(buf.to_owned()) 427 | .send()?; 428 | 429 | if resp.status().is_success() { 430 | Ok(()) 431 | } else { 432 | Err(Error::Object(ObjectError::PutError { 433 | msg: format!("can not put object, status code: {}", resp.status()).into(), 434 | })) 435 | } 436 | } 437 | 438 | fn copy_object_from_object( 439 | &self, 440 | src: S1, 441 | object_name: S2, 442 | headers: H, 443 | resources: R, 444 | ) -> Result<(), Error> 445 | where 446 | S1: AsRef, 447 | S2: AsRef, 448 | S3: AsRef, 449 | H: Into>>, 450 | R: Into>>>, 451 | { 452 | let (host, mut headers) = 453 | self.build_request(RequestType::Put, object_name, headers, resources)?; 454 | headers.insert("x-oss-copy-source", src.as_ref().parse()?); 455 | 456 | let resp = reqwest::blocking::Client::new() 457 | .put(&host) 458 | .headers(headers) 459 | .send()?; 460 | 461 | if resp.status().is_success() { 462 | Ok(()) 463 | } else { 464 | Err(Error::Object(ObjectError::CopyError { 465 | msg: format!("can not copy object, status code: {}", resp.status()).into(), 466 | })) 467 | } 468 | } 469 | 470 | fn delete_object(&self, object_name: S) -> Result<(), Error> 471 | where 472 | S: AsRef, 473 | { 474 | let headers = HashMap::::new(); 475 | let (host, headers) = 476 | self.build_request(RequestType::Delete, object_name, Some(headers), None)?; 477 | 478 | let resp = reqwest::blocking::Client::new() 479 | .delete(&host) 480 | .headers(headers) 481 | .send()?; 482 | 483 | if resp.status().is_success() { 484 | Ok(()) 485 | } else { 486 | Err(Error::Object(ObjectError::DeleteError { 487 | msg: format!("can not delete object, status code: {}", resp.status()).into(), 488 | })) 489 | } 490 | } 491 | } 492 | -------------------------------------------------------------------------------- /src/oss.rs: -------------------------------------------------------------------------------- 1 | use chrono::prelude::*; 2 | use reqwest::header::{HeaderMap, DATE}; 3 | use reqwest::Client; 4 | use std::borrow::Cow; 5 | use std::collections::HashMap; 6 | use std::str; 7 | use std::time::{Duration, SystemTime}; 8 | 9 | use crate::errors::ObjectError; 10 | 11 | use super::auth::*; 12 | use super::errors::Error; 13 | use super::utils::*; 14 | 15 | const RESOURCES: [&str; 50] = [ 16 | "acl", 17 | "uploads", 18 | "location", 19 | "cors", 20 | "logging", 21 | "website", 22 | "referer", 23 | "lifecycle", 24 | "delete", 25 | "append", 26 | "tagging", 27 | "objectMeta", 28 | "uploadId", 29 | "partNumber", 30 | "security-token", 31 | "position", 32 | "img", 33 | "style", 34 | "styleName", 35 | "replication", 36 | "replicationProgress", 37 | "replicationLocation", 38 | "cname", 39 | "bucketInfo", 40 | "comp", 41 | "qos", 42 | "live", 43 | "status", 44 | "vod", 45 | "startTime", 46 | "endTime", 47 | "symlink", 48 | "x-oss-process", 49 | "response-content-type", 50 | "response-content-language", 51 | "response-expires", 52 | "response-cache-control", 53 | "response-content-disposition", 54 | "response-content-encoding", 55 | "udf", 56 | "udfName", 57 | "udfImage", 58 | "udfId", 59 | "udfImageDesc", 60 | "udfApplication", 61 | "comp", 62 | "udfApplicationLog", 63 | "restore", 64 | "callback", 65 | "callback-var", 66 | ]; 67 | 68 | #[derive(Clone, Debug)] 69 | pub struct OSS<'a> { 70 | key_id: Cow<'a, str>, 71 | key_secret: Cow<'a, str>, 72 | endpoint: Cow<'a, str>, 73 | bucket: Cow<'a, str>, 74 | 75 | pub(crate) http_client: Client, 76 | } 77 | 78 | #[derive(Default)] 79 | pub struct Options { 80 | pub pool_max_idle_per_host: Option, 81 | pub timeout: Option, 82 | } 83 | 84 | impl<'a> OSS<'a> { 85 | pub fn new(key_id: S, key_secret: S, endpoint: S, bucket: S) -> Self 86 | where 87 | S: Into>, 88 | { 89 | Self::new_with_opts(key_id, key_secret, endpoint, bucket, Default::default()) 90 | } 91 | 92 | pub fn new_with_opts(key_id: S, key_secret: S, endpoint: S, bucket: S, opts: Options) -> Self 93 | where 94 | S: Into>, 95 | { 96 | let mut builder = Client::builder(); 97 | if let Some(timeout) = opts.timeout { 98 | builder = builder.timeout(timeout); 99 | } 100 | if let Some(max_per_host) = opts.pool_max_idle_per_host { 101 | builder = builder.pool_max_idle_per_host(max_per_host); 102 | } 103 | 104 | let http_client = builder.build().expect("Build http client failed"); 105 | OSS { 106 | key_id: key_id.into(), 107 | key_secret: key_secret.into(), 108 | endpoint: endpoint.into(), 109 | bucket: bucket.into(), 110 | http_client, 111 | } 112 | } 113 | 114 | pub fn bucket(&self) -> &str { 115 | &self.bucket 116 | } 117 | 118 | pub fn endpoint(&self) -> &str { 119 | &self.endpoint 120 | } 121 | 122 | pub fn key_id(&self) -> &str { 123 | &self.key_id 124 | } 125 | 126 | pub fn key_secret(&self) -> &str { 127 | &self.key_secret 128 | } 129 | 130 | pub fn set_bucket(&mut self, bucket: &'a str) { 131 | self.bucket = bucket.into() 132 | } 133 | 134 | pub fn host(&self, bucket: &str, object: &str, resources_str: &str) -> String { 135 | if self.endpoint.starts_with("https") { 136 | format!( 137 | "https://{}.{}/{}?{}", 138 | bucket, 139 | self.endpoint.replacen("https://", "", 1), 140 | object, 141 | resources_str 142 | ) 143 | } else { 144 | format!( 145 | "http://{}.{}/{}?{}", 146 | bucket, 147 | self.endpoint.replacen("http://", "", 1), 148 | object, 149 | resources_str 150 | ) 151 | } 152 | } 153 | 154 | pub fn date(&self) -> String { 155 | let now: DateTime = Utc::now(); 156 | now.format("%a, %d %b %Y %T GMT").to_string() 157 | } 158 | 159 | pub fn get_resources_str(&self, params: &HashMap>) -> String 160 | where 161 | S: AsRef, 162 | { 163 | let mut resources: Vec<(&S, &Option)> = params 164 | .iter() 165 | .filter(|(k, _)| RESOURCES.contains(&k.as_ref())) 166 | .collect(); 167 | resources.sort_by(|a, b| a.0.as_ref().to_string().cmp(&b.0.as_ref().to_string())); 168 | let mut result = String::new(); 169 | for (k, v) in resources { 170 | if !result.is_empty() { 171 | result += "&"; 172 | } 173 | if let Some(vv) = v { 174 | result += &format!("{}={}", k.as_ref().to_owned(), vv.as_ref()); 175 | } else { 176 | result += k.as_ref(); 177 | } 178 | } 179 | result 180 | } 181 | 182 | pub fn get_params_str(&self, params: &HashMap>) -> String 183 | where 184 | S: AsRef, 185 | { 186 | let mut resources: Vec<(&S, &Option)> = params.iter().collect(); 187 | resources.sort_by(|a, b| a.0.as_ref().to_string().cmp(&b.0.as_ref().to_string())); 188 | let mut result = String::new(); 189 | for (k, v) in resources { 190 | if !result.is_empty() { 191 | result += "&"; 192 | } 193 | if let Some(vv) = v { 194 | result += &format!("{}={}", k.as_ref().to_owned(), vv.as_ref()); 195 | } else { 196 | result += k.as_ref(); 197 | } 198 | } 199 | result 200 | } 201 | 202 | /// Build a request. Return url and header for reqwest client builder. 203 | pub fn build_request( 204 | &self, 205 | req_type: RequestType, 206 | object_name: S1, 207 | headers: H, 208 | resources: R, 209 | ) -> Result<(String, HeaderMap), Error> 210 | where 211 | S1: AsRef, 212 | S2: AsRef, 213 | H: Into>>, 214 | R: Into>>>, 215 | { 216 | let object_name = object_name.as_ref(); 217 | let (resources_str, params_str) = if let Some(r) = resources.into() { 218 | (self.get_resources_str(&r), self.get_params_str(&r)) 219 | } else { 220 | (String::new(), String::new()) 221 | }; 222 | 223 | let host = self.host(self.bucket(), object_name, ¶ms_str); 224 | let date = self.date(); 225 | let mut headers = if let Some(h) = headers.into() { 226 | to_headers(h)? 227 | } else { 228 | HeaderMap::new() 229 | }; 230 | headers.insert(DATE, date.parse()?); 231 | let authorization = self.oss_sign( 232 | req_type.as_str(), 233 | self.key_id(), 234 | self.key_secret(), 235 | self.bucket(), 236 | object_name, 237 | &resources_str, 238 | &headers, 239 | ); 240 | headers.insert("Authorization", authorization.parse()?); 241 | 242 | Ok((host, headers)) 243 | } 244 | } 245 | 246 | pub enum RequestType { 247 | Get, 248 | Put, 249 | Post, 250 | Delete, 251 | Head, 252 | } 253 | 254 | impl RequestType { 255 | pub(crate) fn as_str(&self) -> &str { 256 | match self { 257 | RequestType::Get => "GET", 258 | RequestType::Put => "PUT", 259 | RequestType::Post => "POST", 260 | RequestType::Delete => "DELETE", 261 | RequestType::Head => "HEAD", 262 | } 263 | } 264 | } 265 | 266 | #[derive(Debug)] 267 | pub struct ObjectMeta { 268 | /// The last modified time 269 | pub last_modified: SystemTime, 270 | /// The size in bytes of the object 271 | pub size: usize, 272 | /// 128-bits RFC 1864 MD5. This field only presents in normal file. Multipart and append-able file will have empty md5. 273 | pub md5: String, 274 | } 275 | 276 | impl ObjectMeta { 277 | pub fn from_header_map(header: &HeaderMap) -> Result { 278 | let getter = |key: &str| -> Result<&str, Error> { 279 | let value = header 280 | .get(key) 281 | .ok_or_else(|| { 282 | Error::Object(ObjectError::HeadError { 283 | msg: format!( 284 | "can not find {} in head response, response header: {:?}", 285 | key, header 286 | ) 287 | .into(), 288 | }) 289 | })? 290 | .to_str() 291 | .map_err(|_| { 292 | Error::Object(ObjectError::HeadError { 293 | msg: format!("header entry {} contains invalid ASCII code", key).into(), 294 | }) 295 | })?; 296 | Ok(value) 297 | }; 298 | 299 | let last_modified = httpdate::parse_http_date(getter("Last-Modified")?).map_err(|e| { 300 | Error::Object(ObjectError::HeadError { 301 | msg: format!("cannot parse to system time: {}", e).into(), 302 | }) 303 | })?; 304 | let size = getter("Content-Length")?.parse().map_err(|e| { 305 | Error::Object(ObjectError::HeadError { 306 | msg: format!("cannot parse to number: {}", e).into(), 307 | }) 308 | })?; 309 | let md5 = getter("Content-Md5")?.to_string(); 310 | 311 | Ok(Self { 312 | last_modified, 313 | size, 314 | md5, 315 | }) 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/prelude.rs: -------------------------------------------------------------------------------- 1 | pub use super::object::*; 2 | pub use super::oss::OSS; 3 | pub use super::service::*; 4 | -------------------------------------------------------------------------------- /src/service.rs: -------------------------------------------------------------------------------- 1 | use reqwest::header::{HeaderMap, DATE}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::collections::HashMap; 4 | 5 | use crate::prelude::Owner; 6 | 7 | use super::auth::*; 8 | use super::errors::Error; 9 | use super::oss::OSS; 10 | 11 | #[derive(Clone, Debug, Serialize, Deserialize)] 12 | #[serde(rename_all = "PascalCase")] 13 | pub struct ListBuckets { 14 | #[serde(default)] 15 | prefix: String, 16 | #[serde(default)] 17 | marker: String, 18 | #[serde(default)] 19 | max_keys: String, 20 | #[serde(default)] 21 | is_truncated: bool, 22 | #[serde(default)] 23 | next_marker: String, 24 | 25 | owner: Owner, 26 | 27 | #[serde(default)] 28 | buckets: Vec, 29 | } 30 | 31 | impl ListBuckets { 32 | pub fn new( 33 | prefix: String, 34 | marker: String, 35 | max_keys: String, 36 | is_truncated: bool, 37 | next_marker: String, 38 | owner: Owner, 39 | buckets: Vec, 40 | ) -> Self { 41 | ListBuckets { 42 | prefix, 43 | marker, 44 | max_keys, 45 | is_truncated, 46 | next_marker, 47 | owner, 48 | buckets, 49 | } 50 | } 51 | 52 | pub fn prefix(&self) -> &str { 53 | &self.prefix 54 | } 55 | 56 | pub fn marker(&self) -> &str { 57 | &self.marker 58 | } 59 | 60 | pub fn max_keys(&self) -> &str { 61 | &self.max_keys 62 | } 63 | 64 | pub fn is_truncated(&self) -> bool { 65 | self.is_truncated 66 | } 67 | 68 | pub fn next_marker(&self) -> &str { 69 | &self.next_marker 70 | } 71 | 72 | pub fn id(&self) -> &str { 73 | &self.owner.id 74 | } 75 | 76 | pub fn display_name(&self) -> &str { 77 | &self.owner.display_name 78 | } 79 | 80 | pub fn buckets(&self) -> &Vec { 81 | &self.buckets 82 | } 83 | } 84 | 85 | #[derive(Clone, Debug, Serialize, Deserialize)] 86 | #[serde(rename_all = "PascalCase")] 87 | pub struct Bucket { 88 | #[serde(default)] 89 | name: String, 90 | #[serde(default)] 91 | creation_date: String, 92 | #[serde(default)] 93 | location: String, 94 | #[serde(default)] 95 | extranet_endpoint: String, 96 | #[serde(default)] 97 | intranet_endpoint: String, 98 | #[serde(default)] 99 | storage_class: String, 100 | } 101 | 102 | impl Bucket { 103 | pub fn new( 104 | name: String, 105 | creation_date: String, 106 | location: String, 107 | extranet_endpoint: String, 108 | intranet_endpoint: String, 109 | storage_class: String, 110 | ) -> Self { 111 | Bucket { 112 | name, 113 | creation_date, 114 | location, 115 | extranet_endpoint, 116 | intranet_endpoint, 117 | storage_class, 118 | } 119 | } 120 | 121 | pub fn name(&self) -> &str { 122 | &self.name 123 | } 124 | 125 | pub fn creation_date(&self) -> &str { 126 | &self.creation_date 127 | } 128 | 129 | pub fn location(&self) -> &str { 130 | &self.location 131 | } 132 | 133 | pub fn extranet_endpoint(&self) -> &str { 134 | &self.extranet_endpoint 135 | } 136 | 137 | pub fn intranet_endpoint(&self) -> &str { 138 | &self.intranet_endpoint 139 | } 140 | 141 | pub fn storage_class(&self) -> &str { 142 | &self.storage_class 143 | } 144 | } 145 | 146 | pub trait ServiceAPI { 147 | fn list_bucket(&self, resources: R) -> Result 148 | where 149 | S: AsRef, 150 | R: Into>>>; 151 | } 152 | 153 | impl<'a> ServiceAPI for OSS<'a> { 154 | fn list_bucket(&self, resources: R) -> Result 155 | where 156 | S: AsRef, 157 | R: Into>>>, 158 | { 159 | let resources_str = if let Some(r) = resources.into() { 160 | self.get_resources_str(&r) 161 | } else { 162 | String::new() 163 | }; 164 | let host = self.endpoint(); 165 | let date = self.date(); 166 | 167 | let mut headers = HeaderMap::new(); 168 | headers.insert(DATE, date.parse()?); 169 | let authorization = self.oss_sign( 170 | "GET", 171 | self.key_id(), 172 | self.key_secret(), 173 | "", 174 | "", 175 | &resources_str, 176 | &headers, 177 | ); 178 | headers.insert("Authorization", authorization.parse()?); 179 | 180 | let resp = reqwest::blocking::Client::new() 181 | .get(host) 182 | .headers(headers) 183 | .send()?; 184 | 185 | let body = resp.text()?; 186 | let list_buckets = quick_xml::de::from_str::(&body)?; 187 | 188 | Ok(list_buckets) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use super::errors::Error; 2 | use reqwest::header::{HeaderMap, HeaderName}; 3 | use std::collections::HashMap; 4 | use std::fs::File; 5 | use std::io::{BufReader, Read}; 6 | 7 | #[inline] 8 | pub fn load_file(p: S) -> Result, Error> 9 | where 10 | S: AsRef, 11 | { 12 | let p = p.as_ref(); 13 | let f = File::open(p)?; 14 | let mut f = BufReader::new(f); 15 | let mut s = Vec::new(); 16 | f.read_to_end(&mut s)?; 17 | Ok(s) 18 | } 19 | 20 | pub fn to_headers(hashmap: HashMap) -> Result 21 | where 22 | S: AsRef, 23 | { 24 | let mut headers = HeaderMap::new(); 25 | for (key, val) in hashmap.iter() { 26 | let key = key.as_ref(); 27 | let val = val.as_ref(); 28 | headers.insert(HeaderName::from_bytes(key.as_bytes())?, val.parse()?); 29 | } 30 | Ok(headers) 31 | } 32 | --------------------------------------------------------------------------------