实现 put object

This commit is contained in:
kingzcheung 2023-06-02 18:08:53 +08:00
parent 0bb6ed21f3
commit cfa84d057a
11 changed files with 189 additions and 148 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/target /target
/Cargo.lock /Cargo.lock
/.env

View File

@ -13,4 +13,7 @@ urlencoding = "2"
chrono = "0.4" chrono = "0.4"
hmac-sha1 = "^0.1" hmac-sha1 = "^0.1"
rustc-serialize = "0.3.24" rustc-serialize = "0.3.24"
base64 = "0.21.2" base64 = "0.21.0"
[dev-dependencies]
dotenvy = "0.15.7"

View File

@ -1,11 +1,11 @@
use crate::{client::Client, error::ObsError}; use crate::{client::Client, config::SignatureType, error::ObsError};
use ::base64::{engine::general_purpose, Engine}; use ::base64::{engine::general_purpose, Engine};
use chrono::{TimeZone, Utc}; use chrono::{TimeZone, Utc};
use hmacsha1::hmac_sha1; use hmacsha1::hmac_sha1;
use rustc_serialize::{base64, hex::ToHex}; use reqwest::header::{HeaderMap, HeaderValue, HeaderName};
use std::collections::HashMap; use std::{collections::HashMap, str::FromStr};
pub const RFC1123: &str = "%a, %d %b %Y %H:%M:%S %Z"; const RFC1123 :&str = "%a, %d %b %Y %H:%M:%S GMT";
pub trait Authorization { pub trait Authorization {
fn signature( fn signature(
@ -19,10 +19,11 @@ pub trait Authorization {
fn auth( fn auth(
&self, &self,
method: &str, method: &str,
bucket:&str,
params: HashMap<String, String>, params: HashMap<String, String>,
headers: HashMap<String, Vec<String>>, headers: HashMap<String, Vec<String>>,
canonicalized_url: String, canonicalized_url: String,
) -> Result<HashMap<String, String>, ObsError>; ) -> Result<HeaderMap, ObsError>;
} }
impl Authorization for Client { impl Authorization for Client {
@ -33,14 +34,17 @@ impl Authorization for Client {
headers: HashMap<String, Vec<String>>, headers: HashMap<String, Vec<String>>,
canonicalized_url: String, canonicalized_url: String,
) -> Result<String, ObsError> { ) -> Result<String, ObsError> {
let attach_headers = attach_headers(headers, true);
let string_to_sign = vec![ let string_to_sign = vec![
method, method, //HTTP-Verb
"\n", "\n",
&attach_headers(headers, true), &attach_headers, // Content-MD5 \n Content-Type \n
"\n", "\n",
&canonicalized_url, &canonicalized_url,
] ]
.join(""); .join("");
let security = self.security(); let security = self.security();
match security { match security {
Some(s) => { Some(s) => {
@ -54,16 +58,33 @@ impl Authorization for Client {
fn auth( fn auth(
&self, &self,
method: &str, method: &str,
bucket:&str,
params: HashMap<String, String>, params: HashMap<String, String>,
headers: HashMap<String, Vec<String>>, mut headers: HashMap<String, Vec<String>>,
canonicalized_url: String, canonicalized_url: String,
) -> Result<HashMap<String, String>, ObsError> { ) -> Result<HeaderMap, ObsError> {
let sign = self.signature(method, params, headers, canonicalized_url)?; let is_v4 = matches!(self.config().signature_type, SignatureType::V4);
headers.insert("Host".into(), vec![format!("{}.{}",bucket,self.config().endpoint())]);
prepare_host_and_date(&mut headers, self.config().endpoint(), is_v4);
let sign = self.signature(method, params, headers.clone(), canonicalized_url)?;
let security = self.security(); let security = self.security();
match security { match security {
Some(s) => { Some(s) => {
let value = format!("OBS {}:{}", s.ak(), sign); let value = format!("OBS {}:{}", s.ak(), sign);
let h = vec![("Authorization".into(), value)].into_iter().collect(); let mut h = HeaderMap::new();
h.insert(
"Authorization",
HeaderValue::from_str(value.as_str()).unwrap(),
);
for (key, value) in headers.iter() {
h.insert(
HeaderName::from_str(key).unwrap(),
HeaderValue::from_str(&value.join(",")).unwrap(),
);
}
Ok(h) Ok(h)
} }
None => Err(ObsError::Security), None => Err(ObsError::Security),
@ -72,11 +93,10 @@ impl Authorization for Client {
} }
fn prepare_host_and_date( fn prepare_host_and_date(
mut headers: HashMap<String, Vec<String>>, headers: &mut HashMap<String, Vec<String>>,
host_name: String, host_name: &str,
is_v4: bool, is_v4: bool,
) { ) {
headers.insert("Host".into(), vec![host_name]);
if let Some(date) = headers.get("x-amz-date") { if let Some(date) = headers.get("x-amz-date") {
let mut flag = false; let mut flag = false;
if date.len() == 1 { if date.len() == 1 {
@ -100,42 +120,42 @@ fn prepare_host_and_date(
} }
} }
fn encode_headers(headers: HashMap<String, Vec<String>>) -> HashMap<String, Vec<String>> { // fn encode_headers(headers: HashMap<String, Vec<String>>) -> HashMap<String, Vec<String>> {
headers // headers
.into_iter() // .into_iter()
.map(|(key, values)| { // .map(|(key, values)| {
( // (
key, // key,
values // values
.iter() // .iter()
.map(|v| urlencoding::encode(v).to_string()) // .map(|v| urlencoding::encode(v).to_string())
.collect::<Vec<String>>(), // .collect::<Vec<String>>(),
) // )
}) // })
.collect::<HashMap<String, Vec<String>>>() // .collect::<HashMap<String, Vec<String>>>()
} // }
fn signature_header( // fn signature_header(
headers: HashMap<String, Vec<String>>, // headers: HashMap<String, Vec<String>>,
) -> (Vec<String>, HashMap<String, Vec<String>>) { // ) -> (Vec<String>, HashMap<String, Vec<String>>) {
let mut signed_headers = vec![]; // let mut signed_headers = vec![];
let mut headers2 = HashMap::with_capacity(headers.len()); // let mut headers2 = HashMap::with_capacity(headers.len());
for (key, value) in headers { // for (key, value) in headers {
let key2 = key.trim().to_lowercase(); // let key2 = key.trim().to_lowercase();
if !key2.is_empty() { // if !key2.is_empty() {
signed_headers.push(key2.clone()); // signed_headers.push(key2.clone());
headers2.insert(key2, value); // headers2.insert(key2, value);
} // }
} // }
signed_headers.sort(); // signed_headers.sort();
(signed_headers, headers2) // (signed_headers, headers2)
} // }
fn credential(ak: &str, region: &str, short_date: &str) -> (String, String) { // fn credential(ak: &str, region: &str, short_date: &str) -> (String, String) {
let scope = format!("{}/{}/{}/{}", short_date, region, "s3", "aws4_request"); // let scope = format!("{}/{}/{}/{}", short_date, region, "s3", "aws4_request");
// return fmt.Sprintf("%s/%s", ak, scope), scope // // return fmt.Sprintf("%s/%s", ak, scope), scope
(format!("{}/{}", ak, &scope), scope) // (format!("{}/{}", ak, &scope), scope)
} // }
fn string_to_sign( fn string_to_sign(
keys: Vec<String>, keys: Vec<String>,
@ -178,11 +198,11 @@ fn attach_headers(headers: HashMap<String, Vec<String>>, is_obs: bool) -> String
.collect::<_>(); .collect::<_>();
for (key, value) in headers { for (key, value) in headers {
let _key = key.trim().to_lowercase(); let _key = key.trim().to_lowercase();
let prefixheader = if is_obs { "x-amz-" } else { "x-obs-" }; let prefix_header = if is_obs { "x-amz-" } else { "x-obs-" };
if _key == "content-md5" if _key == "content-md5"
|| _key == "content-type" || _key == "content-type"
|| _key == "date" || _key == "date"
|| _key.starts_with(prefixheader) || _key.starts_with(prefix_header)
{ {
keys.push(_key.clone()); keys.push(_key.clone());
_headers.insert(_key, value); _headers.insert(_key, value);
@ -199,10 +219,9 @@ fn attach_headers(headers: HashMap<String, Vec<String>>, is_obs: bool) -> String
let date_camel_header = if is_obs { "X-obs-Date" } else { "X-Amz-Date" }; let date_camel_header = if is_obs { "X-obs-Date" } else { "X-Amz-Date" };
let data_header = date_camel_header.to_lowercase(); let data_header = date_camel_header.to_lowercase();
if _headers.contains_key("Date") if (_headers.contains_key(&data_header) || _headers.contains_key(date_camel_header))
&& (_headers.contains_key(&data_header) || _headers.contains_key(date_camel_header))
{ {
_headers.insert(date_camel_header.into(), vec![]); _headers.insert(date_camel_header.into(), vec![rfc_1123()]);
} }
keys.sort(); keys.sort();
@ -218,3 +237,10 @@ fn signature(string_to_sign: &str, sk: &str) -> Result<String, ObsError> {
let hs = general_purpose::STANDARD.encode(hash); let hs = general_purpose::STANDARD.encode(hash);
Ok(hs) Ok(hs)
} }
fn rfc_1123() -> String {
let date = Utc::now().format(RFC1123).to_string();
date
}

10
src/bucket.rs Normal file
View File

@ -0,0 +1,10 @@
pub struct Bucket<'a> {
name: &'a str
}
impl<'a> Bucket<'a> {
pub fn new(name: &'a str) -> Self { Self { name } }
}

View File

@ -1,11 +1,10 @@
use std::time::Duration; use std::{time::Duration, collections::HashMap};
use reqwest::{header::HeaderMap, Body, Method, Response}; use reqwest::{header::{HeaderMap, HeaderValue}, Body, Method, Response};
use crate::{ use crate::{
config::{Config, SignatureType}, config::{Config, SignatureType, SecurityHolder},
error::ObsError, error::ObsError, auth::Authorization,
provider::{SecurityHolder},
}; };
#[derive(Debug)] #[derive(Debug)]
@ -15,6 +14,7 @@ pub struct Client {
} }
impl Client { impl Client {
/// endpoint 格式: https[http]://obs.cn-north-4.myhuaweicloud.com
pub fn new<S:ToString>( pub fn new<S:ToString>(
access_key_id: S, access_key_id: S,
secret_access_key: S, secret_access_key: S,
@ -51,6 +51,7 @@ impl Client {
pub async fn do_action<T>( pub async fn do_action<T>(
&self, &self,
method: Method, method: Method,
bucket_name:&str,
uri: &str, uri: &str,
with_headers: Option<HeaderMap>, with_headers: Option<HeaderMap>,
body: T, body: T,
@ -58,8 +59,10 @@ impl Client {
where where
T: Into<Body>, T: Into<Body>,
{ {
let url = format!("https://{}/{}", self.config().endpoint(), uri); let url = format!("https://{}.{}/{}",bucket_name, self.config().endpoint(), uri);
let mut headers = HeaderMap::new();
let canonicalized_url = self.config().canonicalized_url(bucket_name,uri);
let mut headers = self.auth(method.as_str(),bucket_name, HashMap::new(), HashMap::new(), canonicalized_url)?;
if let Some(wh) = with_headers { if let Some(wh) = with_headers {
headers.extend(wh); headers.extend(wh);
} }
@ -74,6 +77,16 @@ impl Client {
Ok(res) Ok(res)
} }
/// PUT上传
pub async fn put_object(&self, bucket:&str,key:&str, object:&'static [u8]) ->Result<(), ObsError> {
let mut with_headers = HeaderMap::new();
with_headers.insert("Content-Length", HeaderValue::from_str(format!("{}",object.len()).as_str()).unwrap());
let resp = self.do_action(Method::PUT, bucket, key, Some(with_headers), object).await?;
let _ = resp.text().await?;
Ok(())
}
} }
@ -111,8 +124,19 @@ impl ClientBuilder {
self self
} }
/// 节点,支持以下三种格式:
///
/// 1. http://your-endpoint
/// 2. https://your-endpoint
/// 3. your-endpoint
pub fn endpoint<S: ToString>(mut self, value: S) -> ClientBuilder { pub fn endpoint<S: ToString>(mut self, value: S) -> ClientBuilder {
self.config.set_endpoint(value.to_string()); let mut value = value.to_string();
if value.starts_with("https://") {
value = value.replace("https://", "");
}else if value.starts_with("http://") {
value = value.replace("http://","");
}
self.config.set_endpoint(value);
self self
} }
@ -140,7 +164,7 @@ impl ClientBuilder {
self self
} }
fn build(self) -> Result<Client, ObsError> { pub fn build(self) -> Result<Client, ObsError> {
let req_client = reqwest::ClientBuilder::new() let req_client = reqwest::ClientBuilder::new()
.timeout(self.config.timeout()) .timeout(self.config.timeout())
.build(); .build();

View File

@ -1,8 +1,5 @@
use std::time::Duration; use std::time::Duration;
use crate::provider::SecurityHolder;
#[derive(Debug)] #[derive(Debug)]
pub enum SignatureType { pub enum SignatureType {
V2, V2,
@ -10,6 +7,34 @@ pub enum SignatureType {
OBS, OBS,
} }
#[derive(Debug, Default, Clone)]
pub struct SecurityHolder {
ak: String,
sk: String,
security_token: String,
}
impl SecurityHolder {
pub fn new(ak: String, sk: String, security_token: String) -> Self {
Self {
ak,
sk,
security_token,
}
}
pub fn ak(&self) -> &str {
self.ak.as_ref()
}
pub fn sk(&self) -> &str {
self.sk.as_ref()
}
pub fn security_token(&self) -> &str {
self.security_token.as_ref()
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct Config { pub struct Config {
pub(crate) security_providers: Vec<SecurityHolder>, pub(crate) security_providers: Vec<SecurityHolder>,
@ -25,13 +50,19 @@ impl Config {
self.security_providers.as_ref() self.security_providers.as_ref()
} }
/// 暂时不支持自定义域名
pub fn canonicalized_url(&self, bucket_name: &str) -> String { pub fn canonicalized_url(&self, bucket_name: &str, uri: &str) -> String {
if bucket_name.is_empty() { if bucket_name.is_empty() {
String::from("/") String::from("/")
} else { } else {
match self.signature_type { match self.signature_type {
SignatureType::V2 | SignatureType::OBS => format!("/{}/", bucket_name), SignatureType::V2 | SignatureType::OBS => {
if uri.is_empty() {
format!("/{}/", bucket_name)
} else {
format!("/{}/{}", bucket_name, uri)
}
}
SignatureType::V4 => String::from("/"), SignatureType::V4 => String::from("/"),
} }
} }
@ -61,7 +92,6 @@ impl Config {
self.signature_type = signature_type; self.signature_type = signature_type;
} }
pub(crate) fn timeout(&self) -> Duration { pub(crate) fn timeout(&self) -> Duration {
self.timeout self.timeout
} }

View File

@ -2,4 +2,5 @@ pub mod client;
pub mod config; pub mod config;
pub mod error; pub mod error;
pub mod auth; pub mod auth;
pub mod provider; pub mod bucket;
pub mod model;

0
src/model/mod.rs Normal file
View File

View File

@ -1,76 +0,0 @@
pub trait SecurityProvider {
fn security(&self) -> SecurityHolder;
}
#[derive(Debug, Default,Clone)]
pub struct SecurityHolder {
ak: String,
sk: String,
security_token: String,
}
impl SecurityHolder {
pub fn new(ak: String, sk: String, security_token: String) -> Self {
Self {
ak,
sk,
security_token,
}
}
pub fn ak(&self) -> &str {
self.ak.as_ref()
}
pub fn sk(&self) -> &str {
self.sk.as_ref()
}
pub fn security_token(&self) -> &str {
self.security_token.as_ref()
}
}
pub struct EnvSecurityProvider {
sh: SecurityHolder,
suffix: String,
}
impl EnvSecurityProvider {
pub fn new(suffix: String) -> Self {
let suffix = if !suffix.is_empty() {
format!("_{}", suffix)
} else {
suffix
};
Self {
sh: SecurityHolder::default(),
suffix,
}
}
}
impl SecurityProvider for EnvSecurityProvider {
fn security(&self) -> SecurityHolder {
let ak = format!(
"{}{}",
std::env::var("OBS_ACCESS_KEY_ID").unwrap_or_default(),
self.suffix.as_str()
);
let sk = format!(
"{}{}",
std::env::var("OBS_SECRET_ACCESS_KEY").unwrap_or_default(),
self.suffix.as_str()
);
let security_token = format!(
"{}{}",
std::env::var("OBS_SECURITY_TOKEN").unwrap_or_default(),
self.suffix.as_str()
);
SecurityHolder {
ak,
sk,
security_token,
}
}
}

BIN
testdata/test.jpeg vendored Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

22
tests/client.rs Normal file
View File

@ -0,0 +1,22 @@
use std::env;
use huaweicloud_sdk_rust_obs::{client, error::ObsError};
#[tokio::test]
async fn test_put_object() ->Result<(),ObsError> {
dotenvy::dotenv().unwrap();
let ak = env::var("OBS_AK").unwrap();
let sk = env::var("OBS_SK").unwrap();
let obs = client::Client::builder()
.endpoint("https://obs.ap-southeast-1.myhuaweicloud.com")
.security_provider(&ak, &sk) //ifree-test
.build()?;
let object = include_bytes!("../testdata/test.jpeg");
obs.put_object("ifree-test", "obs-client-key.jpeg", object).await?;
Ok(())
}