实现 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
/Cargo.lock
/.env

View File

@ -13,4 +13,7 @@ urlencoding = "2"
chrono = "0.4"
hmac-sha1 = "^0.1"
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 chrono::{TimeZone, Utc};
use hmacsha1::hmac_sha1;
use rustc_serialize::{base64, hex::ToHex};
use std::collections::HashMap;
use reqwest::header::{HeaderMap, HeaderValue, HeaderName};
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 {
fn signature(
@ -19,10 +19,11 @@ pub trait Authorization {
fn auth(
&self,
method: &str,
bucket:&str,
params: HashMap<String, String>,
headers: HashMap<String, Vec<String>>,
canonicalized_url: String,
) -> Result<HashMap<String, String>, ObsError>;
) -> Result<HeaderMap, ObsError>;
}
impl Authorization for Client {
@ -33,14 +34,17 @@ impl Authorization for Client {
headers: HashMap<String, Vec<String>>,
canonicalized_url: String,
) -> Result<String, ObsError> {
let attach_headers = attach_headers(headers, true);
let string_to_sign = vec![
method,
method, //HTTP-Verb
"\n",
&attach_headers(headers, true),
&attach_headers, // Content-MD5 \n Content-Type \n
"\n",
&canonicalized_url,
]
.join("");
let security = self.security();
match security {
Some(s) => {
@ -54,16 +58,33 @@ impl Authorization for Client {
fn auth(
&self,
method: &str,
bucket:&str,
params: HashMap<String, String>,
headers: HashMap<String, Vec<String>>,
mut headers: HashMap<String, Vec<String>>,
canonicalized_url: String,
) -> Result<HashMap<String, String>, ObsError> {
let sign = self.signature(method, params, headers, canonicalized_url)?;
) -> Result<HeaderMap, ObsError> {
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();
match security {
Some(s) => {
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)
}
None => Err(ObsError::Security),
@ -72,11 +93,10 @@ impl Authorization for Client {
}
fn prepare_host_and_date(
mut headers: HashMap<String, Vec<String>>,
host_name: String,
headers: &mut HashMap<String, Vec<String>>,
host_name: &str,
is_v4: bool,
) {
headers.insert("Host".into(), vec![host_name]);
if let Some(date) = headers.get("x-amz-date") {
let mut flag = false;
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>> {
headers
.into_iter()
.map(|(key, values)| {
(
key,
values
.iter()
.map(|v| urlencoding::encode(v).to_string())
.collect::<Vec<String>>(),
)
})
.collect::<HashMap<String, Vec<String>>>()
}
// fn encode_headers(headers: HashMap<String, Vec<String>>) -> HashMap<String, Vec<String>> {
// headers
// .into_iter()
// .map(|(key, values)| {
// (
// key,
// values
// .iter()
// .map(|v| urlencoding::encode(v).to_string())
// .collect::<Vec<String>>(),
// )
// })
// .collect::<HashMap<String, Vec<String>>>()
// }
fn signature_header(
headers: HashMap<String, Vec<String>>,
) -> (Vec<String>, HashMap<String, Vec<String>>) {
let mut signed_headers = vec![];
let mut headers2 = HashMap::with_capacity(headers.len());
for (key, value) in headers {
let key2 = key.trim().to_lowercase();
if !key2.is_empty() {
signed_headers.push(key2.clone());
headers2.insert(key2, value);
}
}
signed_headers.sort();
(signed_headers, headers2)
}
// fn signature_header(
// headers: HashMap<String, Vec<String>>,
// ) -> (Vec<String>, HashMap<String, Vec<String>>) {
// let mut signed_headers = vec![];
// let mut headers2 = HashMap::with_capacity(headers.len());
// for (key, value) in headers {
// let key2 = key.trim().to_lowercase();
// if !key2.is_empty() {
// signed_headers.push(key2.clone());
// headers2.insert(key2, value);
// }
// }
// signed_headers.sort();
// (signed_headers, headers2)
// }
fn credential(ak: &str, region: &str, short_date: &str) -> (String, String) {
let scope = format!("{}/{}/{}/{}", short_date, region, "s3", "aws4_request");
// return fmt.Sprintf("%s/%s", ak, scope), scope
(format!("{}/{}", ak, &scope), scope)
}
// fn credential(ak: &str, region: &str, short_date: &str) -> (String, String) {
// let scope = format!("{}/{}/{}/{}", short_date, region, "s3", "aws4_request");
// // return fmt.Sprintf("%s/%s", ak, scope), scope
// (format!("{}/{}", ak, &scope), scope)
// }
fn string_to_sign(
keys: Vec<String>,
@ -178,11 +198,11 @@ fn attach_headers(headers: HashMap<String, Vec<String>>, is_obs: bool) -> String
.collect::<_>();
for (key, value) in headers {
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"
|| _key == "content-type"
|| _key == "date"
|| _key.starts_with(prefixheader)
|| _key.starts_with(prefix_header)
{
keys.push(_key.clone());
_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 data_header = date_camel_header.to_lowercase();
if _headers.contains_key("Date")
&& (_headers.contains_key(&data_header) || _headers.contains_key(date_camel_header))
if (_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();
@ -218,3 +237,10 @@ fn signature(string_to_sign: &str, sk: &str) -> Result<String, ObsError> {
let hs = general_purpose::STANDARD.encode(hash);
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::{
config::{Config, SignatureType},
error::ObsError,
provider::{SecurityHolder},
config::{Config, SignatureType, SecurityHolder},
error::ObsError, auth::Authorization,
};
#[derive(Debug)]
@ -15,6 +14,7 @@ pub struct Client {
}
impl Client {
/// endpoint 格式: https[http]://obs.cn-north-4.myhuaweicloud.com
pub fn new<S:ToString>(
access_key_id: S,
secret_access_key: S,
@ -51,6 +51,7 @@ impl Client {
pub async fn do_action<T>(
&self,
method: Method,
bucket_name:&str,
uri: &str,
with_headers: Option<HeaderMap>,
body: T,
@ -58,8 +59,10 @@ impl Client {
where
T: Into<Body>,
{
let url = format!("https://{}/{}", self.config().endpoint(), uri);
let mut headers = HeaderMap::new();
let url = format!("https://{}.{}/{}",bucket_name, self.config().endpoint(), uri);
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 {
headers.extend(wh);
}
@ -74,6 +77,16 @@ impl Client {
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
}
/// 节点,支持以下三种格式:
///
/// 1. http://your-endpoint
/// 2. https://your-endpoint
/// 3. your-endpoint
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
}
@ -140,7 +164,7 @@ impl ClientBuilder {
self
}
fn build(self) -> Result<Client, ObsError> {
pub fn build(self) -> Result<Client, ObsError> {
let req_client = reqwest::ClientBuilder::new()
.timeout(self.config.timeout())
.build();

View File

@ -1,8 +1,5 @@
use std::time::Duration;
use crate::provider::SecurityHolder;
#[derive(Debug)]
pub enum SignatureType {
V2,
@ -10,6 +7,34 @@ pub enum SignatureType {
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)]
pub struct Config {
pub(crate) security_providers: Vec<SecurityHolder>,
@ -25,13 +50,19 @@ impl Config {
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() {
String::from("/")
} else {
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("/"),
}
}
@ -61,7 +92,6 @@ impl Config {
self.signature_type = signature_type;
}
pub(crate) fn timeout(&self) -> Duration {
self.timeout
}
@ -69,4 +99,4 @@ impl Config {
pub fn endpoint(&self) -> &str {
self.endpoint.as_ref()
}
}
}

View File

@ -2,4 +2,5 @@ pub mod client;
pub mod config;
pub mod error;
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(())
}