Add listen bucket notification (#3)

This commit is contained in:
Daniel Valdivia 2019-06-24 17:04:28 -07:00 committed by Aditya Manthramurthy
parent 89d0c3accf
commit 128ecb871f
12 changed files with 583 additions and 87 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
/target
**/*.rs.bk
Cargo.lock
Cargo.lock
.idea

View File

@ -1,7 +1,7 @@
[package]
name = "minio-rs"
version = "0.1.0"
authors = ["Aditya Manthramurthy <aditya.mmy@gmail.com>", "Krishnan Parthasarathi <krishnan.parthasarathi@gmail.com>"]
authors = ["MinIO Dev Team <dev@min.io>"]
edition = "2018"
[dependencies]
@ -12,6 +12,10 @@ hyper = "0.12.28"
hyper-tls = "0.3.2"
ring = "0.14.6"
roxmltree = "0.6.0"
serde = "1.0.92"
serde_derive = "1.0.91"
serde_json = "1.0.39"
time = "0.1.42"
tokio = "0.1.21"
xml-rs = "0.8.0"

View File

@ -1,9 +1,18 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
/*
* MinIO Rust Library for Amazon S3 Compatible Cloud Storage
* Copyright 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
pub mod minio;

View File

@ -1,9 +1,26 @@
mod minio;
/*
* MinIO Rust Library for Amazon S3 Compatible Cloud Storage
* Copyright 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use futures::{future::Future, stream::Stream};
use hyper::rt;
use minio::BucketInfo;
mod minio;
fn get_local_default_server() -> minio::Client {
match minio::Client::new("http://localhost:9000") {
Ok(mut c) => {

View File

@ -1,26 +1,49 @@
/*
* MinIO Rust Library for Amazon S3 Compatible Cloud Storage
* Copyright 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use std::collections::HashMap;
use std::env;
use std::string::String;
use futures::future::{self, Future};
use futures::{stream, Stream};
use http;
use hyper::header::{HeaderName, HeaderValue};
use hyper::{body::Body, client, header, header::HeaderMap, Method, Request, Response, Uri};
use hyper_tls::HttpsConnector;
use time;
use time::Tm;
pub use types::BucketInfo;
use types::{Err, GetObjectResp, ListObjectsResp, Region};
use crate::minio::net::{Values, ValuesAccess};
use crate::minio::notification::NotificationInfo;
mod api;
mod net;
mod notification;
mod sign;
mod types;
mod xml;
mod woxml;
use bytes::Bytes;
use futures::future::{self, Future};
use futures::stream::Stream;
use http;
use hyper::header::{HeaderName, HeaderValue};
use hyper::{body::Body, client, header, header::HeaderMap, Method, Request, Response, Uri};
use hyper_tls::HttpsConnector;
use std::collections::HashMap;
use std::env;
use std::string::String;
use time;
use time::Tm;
use types::{Err, GetObjectResp, ListObjectsResp, Region};
pub use types::BucketInfo;
pub const SPACE_BYTE: &[u8; 1] = b" ";
#[derive(Debug, Clone)]
pub struct Credentials {
@ -71,18 +94,20 @@ pub struct Client {
impl Client {
pub fn new(server: &str) -> Result<Client, Err> {
let v = server.parse::<Uri>();
match v {
Ok(s) => {
if s.host().is_none() {
let valid = server.parse::<Uri>();
match valid {
Ok(server_uri) => {
if server_uri.host().is_none() {
Err(Err::InvalidUrl("no host specified!".to_string()))
} else if s.scheme_str() != Some("http") && s.scheme_str() != Some("https") {
} else if server_uri.scheme_str() != Some("http")
&& server_uri.scheme_str() != Some("https")
{
Err(Err::InvalidUrl("invalid scheme!".to_string()))
} else {
Ok(Client {
server: s.clone(),
server: server_uri.clone(),
region: Region::empty(),
conn_client: if s.scheme_str() == Some("http") {
conn_client: if server_uri.scheme_str() == Some("http") {
ConnClient::HttpCC(client::Client::new())
} else {
let https = HttpsConnector::new(4).unwrap();
@ -106,14 +131,14 @@ impl Client {
self.region = r;
}
fn add_host_header(&self, h: &mut HeaderMap) {
fn add_host_header(&self, header_map: &mut HeaderMap) {
let host_val = match self.server.port_part() {
Some(port) => format!("{}:{}", self.server.host().unwrap_or(""), port),
None => self.server.host().unwrap_or("").to_string(),
};
match header::HeaderValue::from_str(&host_val) {
Ok(v) => {
h.insert(header::HOST, v);
header_map.insert(header::HOST, v);
}
_ => {}
}
@ -185,13 +210,17 @@ impl Client {
})
}
pub fn get_bucket_location(&self, b: &str) -> impl Future<Item = Region, Error = Err> {
let mut qp = HashMap::new();
qp.insert("location".to_string(), None);
/// get_bucket_location - Get location for the bucket_name.
pub fn get_bucket_location(
&self,
bucket_name: &str,
) -> impl Future<Item = Region, Error = Err> {
let mut qp = Values::new();
qp.set_value("location", None);
let s3_req = S3Req {
method: Method::GET,
bucket: Some(b.to_string()),
bucket: Some(bucket_name.to_string()),
object: None,
headers: HeaderMap::new(),
query: qp,
@ -204,18 +233,18 @@ impl Client {
resp.into_body()
.concat2()
.map_err(|err| Err::HyperErr(err))
.and_then(move |chunk| b2s(chunk.into_bytes()))
.and_then(move |chunk| chunk_to_string(&chunk))
.and_then(|s| xml::parse_bucket_location(s))
})
}
pub fn delete_bucket(&self, b: &str) -> impl Future<Item = (), Error = Err> {
pub fn delete_bucket(&self, bucket_name: &str) -> impl Future<Item = (), Error = Err> {
let s3_req = S3Req {
method: Method::DELETE,
bucket: Some(b.to_string()),
bucket: Some(bucket_name.to_string()),
object: None,
headers: HeaderMap::new(),
query: HashMap::new(),
query: Values::new(),
body: Body::empty(),
ts: time::now_utc(),
};
@ -223,13 +252,13 @@ impl Client {
.and_then(|_| Ok(()))
}
pub fn bucket_exists(&self, b: &str) -> impl Future<Item = bool, Error = Err> {
pub fn bucket_exists(&self, bucket_name: &str) -> impl Future<Item = bool, Error = Err> {
let s3_req = S3Req {
method: Method::HEAD,
bucket: Some(b.to_string()),
bucket: Some(bucket_name.to_string()),
object: None,
headers: HeaderMap::new(),
query: HashMap::new(),
query: Values::new(),
body: Body::empty(),
ts: time::now_utc(),
};
@ -250,7 +279,7 @@ impl Client {
pub fn get_object_req(
&self,
b: &str,
bucket_name: &str,
key: &str,
get_obj_opts: Vec<(HeaderName, HeaderValue)>,
) -> impl Future<Item = GetObjectResp, Error = Err> {
@ -264,10 +293,10 @@ impl Client {
let s3_req = S3Req {
method: Method::GET,
bucket: Some(b.to_string()),
bucket: Some(bucket_name.to_string()),
object: Some(key.to_string()),
headers: h,
query: HashMap::new(),
query: Values::new(),
body: Body::empty(),
ts: time::now_utc(),
};
@ -276,14 +305,14 @@ impl Client {
.and_then(GetObjectResp::new)
}
pub fn make_bucket(&self, b: &str) -> impl Future<Item = (), Error = Err> {
pub fn make_bucket(&self, bucket_name: &str) -> impl Future<Item = (), Error = Err> {
let xml_body_res = xml::get_mk_bucket_body();
let bucket = b.clone().to_string();
let bucket = bucket_name.clone().to_string();
let s3_req = S3Req {
method: Method::PUT,
bucket: Some(bucket),
object: None,
query: HashMap::new(),
query: Values::new(),
headers: HeaderMap::new(),
body: Body::empty(),
ts: time::now_utc(),
@ -297,7 +326,7 @@ impl Client {
method: Method::GET,
bucket: None,
object: None,
query: HashMap::new(),
query: Values::new(),
headers: HeaderMap::new(),
body: Body::empty(),
ts: time::now_utc(),
@ -308,7 +337,7 @@ impl Client {
resp.into_body()
.concat2()
.map_err(|err| Err::HyperErr(err))
.and_then(move |chunk| b2s(chunk.into_bytes()))
.and_then(move |chunk| chunk_to_string(&chunk))
.and_then(|s| xml::parse_bucket_list(s))
})
}
@ -321,21 +350,21 @@ impl Client {
delimiter: Option<&str>,
max_keys: Option<i32>,
) -> impl Future<Item = ListObjectsResp, Error = Err> {
let mut qparams = HashMap::new();
qparams.insert("list-type".to_string(), Some("2".to_string()));
let mut qparams: Values = Values::new();
qparams.set_value("list-type", Some("2".to_string()));
if let Some(d) = delimiter {
qparams.insert("delimiter".to_string(), Some(d.to_string()));
qparams.set_value("delimiter", Some(d.to_string()));
}
if let Some(m) = marker {
qparams.insert("marker".to_string(), Some(m.to_string()));
qparams.set_value("marker", Some(m.to_string()));
}
if let Some(p) = prefix {
qparams.insert("prefix".to_string(), Some(p.to_string()));
qparams.set_value("prefix", Some(p.to_string()));
}
if let Some(mkeys) = max_keys {
qparams.insert("max-keys".to_string(), Some(mkeys.to_string()));
qparams.set_value("max-keys", Some(mkeys.to_string()));
}
let s3_req = S3Req {
@ -352,10 +381,68 @@ impl Client {
resp.into_body()
.concat2()
.map_err(|err| Err::HyperErr(err))
.and_then(move |chunk| b2s(chunk.into_bytes()))
.and_then(move |chunk| chunk_to_string(&chunk))
.and_then(|s| xml::parse_list_objects(s))
})
}
/// listen_bucket_notificaion - Get bucket notifications for the bucket_name.
pub fn listen_bucket_notificaion(
&self,
bucket_name: &str,
prefix: Option<String>,
suffix: Option<String>,
events: Vec<String>,
) -> impl Stream<Item = notification::NotificationInfo, Error = Err> {
// Prepare request query parameters
let mut query_params: Values = Values::new();
query_params.set_value("prefix", prefix);
query_params.set_value("suffix", suffix);
let opt_events: Vec<Option<String>> = events.into_iter().map(|evt| Some(evt)).collect();
query_params.insert("events".to_string(), opt_events);
// build signed request
let s3_req = S3Req {
method: Method::GET,
bucket: Some(bucket_name.to_string()),
object: None,
headers: HeaderMap::new(),
query: query_params,
body: Body::empty(),
ts: time::now_utc(),
};
self.signed_req_future(s3_req, Ok(Body::empty()))
.map(|resp| {
// Read the whole body for bucket location response.
resp.into_body()
.map_err(|e| Err::HyperErr(e))
.filter(|c| {
// filter out white spaces sent by the server to indicate it's still alive
c[0] != SPACE_BYTE[0]
})
.map(|chunk| {
// Split the chunk by lines and process.
// TODO: Handle case when partial lines are present in the chunk
let chunk_lines = String::from_utf8(chunk.to_vec())
.map(|p| {
let lines =
p.lines().map(|s| s.to_string()).collect::<Vec<String>>();
stream::iter_ok(lines.into_iter())
})
.map_err(|e| Err::Utf8DecodingErr(e));
futures::future::result(chunk_lines).flatten_stream()
})
.flatten()
.map(|line| {
// Deserialize the notification
let notification_info: NotificationInfo =
serde_json::from_str(&line).unwrap();
notification_info
})
})
.flatten_stream()
}
}
fn run_req_future(
@ -375,10 +462,11 @@ fn run_req_future(
})
}
fn b2s(b: Bytes) -> Result<String, Err> {
match String::from_utf8(b.iter().map(|x| x.clone()).collect::<Vec<u8>>()) {
/// Converts a `hyper::Chunk` into a string.
fn chunk_to_string(chunk: &hyper::Chunk) -> Result<String, Err> {
match String::from_utf8(chunk.to_vec()) {
Err(e) => Err(Err::Utf8DecodingErr(e)),
Ok(s) => Ok(s),
Ok(s) => Ok(s.to_string()),
}
}
@ -387,7 +475,7 @@ pub struct S3Req {
bucket: Option<String>,
object: Option<String>,
headers: HeaderMap,
query: HashMap<String, Option<String>>,
query: Values,
body: Body,
ts: Tm,
}
@ -405,14 +493,54 @@ impl S3Req {
res
}
/// Takes the query_parameters and turn them into a valid query string for example:
/// {"key1":["val1","val2"],"key2":["val1","val2"]}
/// will be returned as:
/// "key1=val1&key1=val2&key2=val3&key2=val4"
fn mk_query(&self) -> String {
self.query
.iter()
.map(|(x, y)| match y {
Some(v) => format!("{}={}", x, v),
None => x.to_string(),
.map(|(key, values)| {
values.iter().map(move |value| match value {
Some(v) => format!("{}={}", &key, v),
None => format!("{}=", &key,),
})
})
.flatten()
.collect::<Vec<String>>()
.join("&")
}
}
#[cfg(test)]
mod minio_tests {
use super::*;
#[test]
fn serialize_query_parameters() {
let mut query_params: HashMap<String, Vec<Option<String>>> = HashMap::new();
query_params.insert(
"key1".to_string(),
vec![Some("val1".to_string()), Some("val2".to_string())],
);
query_params.insert(
"key2".to_string(),
vec![Some("val3".to_string()), Some("val4".to_string())],
);
let s3_req = S3Req {
method: Method::GET,
bucket: None,
object: None,
headers: HeaderMap::new(),
query: query_params,
body: Body::empty(),
ts: time::now_utc(),
};
let result = s3_req.mk_query();
assert!(result.contains("key1=val1"));
assert!(result.contains("key1=val2"));
assert!(result.contains("key2=val3"));
assert!(result.contains("key2=val4"));
}
}

View File

@ -1,6 +1,24 @@
use crate::minio;
/*
* MinIO Rust Library for Amazon S3 Compatible Cloud Storage
* Copyright 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use hyper::{header::HeaderName, header::HeaderValue, Body, Request};
use crate::minio;
pub fn mk_request(
r: &minio::S3Req,
svr_str: &str,

113
src/minio/net.rs Normal file
View File

@ -0,0 +1,113 @@
/*
* MinIO Rust Library for Amazon S3 Compatible Cloud Storage
* Copyright 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use std::collections::HashMap;
pub type Values = HashMap<String, Vec<Option<String>>>;
pub trait ValuesAccess {
fn get_value(&self, key: &str) -> Option<String>;
fn set_value(&mut self, key: &str, value: Option<String>);
fn add_value(&mut self, key: &str, value: Option<String>);
fn del_value(&mut self, key: &str);
}
impl ValuesAccess for Values {
/// Gets the first item for a given key. If the key is invalid it returns `None`
/// To get multiple values use the `Values` instance as map.
fn get_value(&self, key: &str) -> Option<String> {
let value_vec = match self.get(key) {
Some(v) => v,
None => return None,
};
if value_vec.len() == 0 {
return None;
}
return value_vec.get(0).unwrap().clone();
}
/// Sets the key to value. It replaces any existing values.
fn set_value(&mut self, key: &str, value: Option<String>) {
self.insert(key.to_string(), vec![value]);
}
/// Add adds the value to key. It appends to any existing values associated with key.
fn add_value(&mut self, key: &str, value: Option<String>) {
match self.get_mut(key) {
Some(value_vec) => value_vec.push(value),
None => (),
}
}
// Del deletes the values associated with key.
fn del_value(&mut self, key: &str) {
self.remove(key);
}
}
#[cfg(test)]
mod net_tests {
use super::*;
#[test]
fn values_set() {
let mut values = Values::new();
values.set_value("key", Some("value".to_string()));
assert_eq!(values.len(), 1);
assert_eq!(values.get("key").unwrap().len(), 1);
values.set_value("key", None);
assert_eq!(values.len(), 1);
assert_eq!(values.get("key").unwrap().len(), 1);
}
#[test]
fn values_add() {
let mut values = Values::new();
values.set_value("key", Some("value".to_string()));
assert_eq!(values.get("key").unwrap().len(), 1);
values.add_value("key", None);
assert_eq!(values.get("key").unwrap().len(), 2);
}
#[test]
fn values_del() {
let mut values = Values::new();
values.set_value("key", Some("value".to_string()));
values.add_value("key", None);
values.del_value("key");
assert_eq!(values.len(), 0);
let mut values2 = Values::new();
values2.set_value("key", Some("value".to_string()));
values2.add_value("key", None);
values2.set_value("key2", Some("value".to_string()));
values2.add_value("key2", None);
values2.del_value("key");
assert_eq!(values2.len(), 1);
}
#[test]
fn values_get() {
let mut values = Values::new();
values.set_value("key", Some("value".to_string()));
values.add_value("key", None);
assert_eq!(values.get_value("key"), Some("value".to_string()));
}
}

112
src/minio/notification.rs Normal file
View File

@ -0,0 +1,112 @@
/*
* MinIO Rust Library for Amazon S3 Compatible Cloud Storage
* Copyright 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use std::collections::HashMap;
use serde_derive::Deserialize;
/// Notification event object metadata.
#[derive(Deserialize, Debug)]
pub struct ObjectMeta {
#[serde(rename(deserialize = "key"))]
pub key: String,
#[serde(rename(deserialize = "size"))]
pub size: Option<i64>,
#[serde(rename(deserialize = "eTag"))]
pub e_tag: Option<String>,
#[serde(rename(deserialize = "versionId"))]
pub version_id: Option<String>,
#[serde(rename(deserialize = "sequencer"))]
pub sequencer: String,
}
/// Notification event bucket metadata.
#[derive(Deserialize, Debug)]
pub struct BucketMeta {
#[serde(rename(deserialize = "name"))]
pub name: String,
#[serde(rename(deserialize = "ownerIdentity"))]
pub owner_identity: Identity,
#[serde(rename(deserialize = "arn"))]
pub arn: String,
}
/// Indentity represents the user id, this is a compliance field.
#[derive(Deserialize, Debug)]
pub struct Identity {
#[serde(rename(deserialize = "principalId"))]
pub principal_id: String,
}
//// sourceInfo represents information on the client that
//// triggered the event notification.
#[derive(Deserialize, Debug)]
pub struct SourceInfo {
#[serde(rename(deserialize = "host"))]
pub host: String,
#[serde(rename(deserialize = "port"))]
pub port: String,
#[serde(rename(deserialize = "userAgent"))]
pub user_agent: String,
}
/// Notification event server specific metadata.
#[derive(Deserialize, Debug)]
pub struct EventMeta {
#[serde(rename(deserialize = "s3SchemaVersion"))]
pub schema_version: String,
#[serde(rename(deserialize = "configurationId"))]
pub configuration_id: String,
#[serde(rename(deserialize = "bucket"))]
pub bucket: BucketMeta,
#[serde(rename(deserialize = "object"))]
pub object: ObjectMeta,
}
/// NotificationEvent represents an Amazon an S3 bucket notification event.
#[derive(Deserialize, Debug)]
pub struct NotificationEvent {
#[serde(rename(deserialize = "eventVersion"))]
pub event_version: String,
#[serde(rename(deserialize = "eventSource"))]
pub event_source: String,
#[serde(rename(deserialize = "awsRegion"))]
pub aws_region: String,
#[serde(rename(deserialize = "eventTime"))]
pub event_time: String,
#[serde(rename(deserialize = "eventName"))]
pub event_name: String,
#[serde(rename(deserialize = "source"))]
pub source: SourceInfo,
#[serde(rename(deserialize = "userIdentity"))]
pub user_identity: Identity,
#[serde(rename(deserialize = "requestParameters"))]
pub request_parameters: HashMap<String, String>,
#[serde(rename(deserialize = "responseElements"))]
pub response_elements: HashMap<String, String>,
#[serde(rename(deserialize = "s3"))]
pub s3: EventMeta,
}
/// NotificationInfo - represents the collection of notification events, additionally
/// also reports errors if any while listening on bucket notifications.
#[derive(Deserialize, Debug)]
pub struct NotificationInfo {
#[serde(rename(deserialize = "Records"), default = "Vec::new")]
pub records: Vec<NotificationEvent>,
pub err: Option<String>,
}

View File

@ -1,8 +1,26 @@
/*
* MinIO Rust Library for Amazon S3 Compatible Cloud Storage
* Copyright 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use std::collections::{HashMap, HashSet};
use hyper::header::{
HeaderMap, HeaderName, HeaderValue, AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE, USER_AGENT,
};
use ring::{digest, hmac};
use std::collections::{HashMap, HashSet};
use time::Tm;
use crate::minio;
@ -66,20 +84,21 @@ fn uri_encode_str(s: &str, encode_slash: bool) -> String {
s.chars().map(|x| uri_encode(x, encode_slash)).collect()
}
fn get_canonical_querystr(q: &HashMap<String, Option<String>>) -> String {
let mut hs: Vec<(String, Option<String>)> = q.clone().drain().collect();
fn get_canonical_querystr(q: &HashMap<String, Vec<Option<String>>>) -> String {
let mut hs: Vec<(String, Vec<Option<String>>)> = q.clone().drain().collect();
// sort keys
hs.sort();
let vs: Vec<String> = hs
.drain(..)
.map(|(x, y)| {
let val_str = match y {
Some(s) => uri_encode_str(&s, true),
None => "".to_string(),
};
uri_encode_str(&x, true) + "=" + &val_str
// Build canonical query string
hs.iter()
.map(|(key, values)| {
values.iter().map(move |value| match value {
Some(v) => format!("{}={}", &key, uri_encode_str(&v, true)),
None => format!("{}=", &key),
})
})
.collect();
vs[..].join("&")
.flatten()
.collect::<Vec<String>>()
.join("&")
}
fn get_canonical_request(
@ -180,3 +199,25 @@ pub fn sign_v4(
vec![auth_hdr, date_hdr]
})
}
#[cfg(test)]
mod sign_tests {
use super::*;
#[test]
fn canonical_ordered() {
let mut query_params: HashMap<String, Vec<Option<String>>> = HashMap::new();
query_params.insert("key2".to_string(), vec![Some("val3".to_string()), None]);
query_params.insert(
"key1".to_string(),
vec![Some("val1".to_string()), Some("val2".to_string())],
);
assert_eq!(
get_canonical_querystr(&query_params),
"key1=val1&key1=val2&key2=val3&key2="
);
}
}

View File

@ -1,3 +1,20 @@
/*
* MinIO Rust Library for Amazon S3 Compatible Cloud Storage
* Copyright 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use bytes::Bytes;
use futures::stream::Stream;
use hyper::header::{

View File

@ -1,3 +1,20 @@
/*
* MinIO Rust Library for Amazon S3 Compatible Cloud Storage
* Copyright 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
extern crate xml;
use xml::writer::{EmitterConfig, EventWriter, XmlEvent};

View File

@ -1,10 +1,29 @@
use crate::minio::types::{BucketInfo, Err, ListObjectsResp, ObjectInfo, Region};
use crate::minio::woxml;
use hyper::body::Body;
use roxmltree;
/*
* MinIO Rust Library for Amazon S3 Compatible Cloud Storage
* Copyright 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use std::collections::HashMap;
use std::str::FromStr;
use hyper::body::Body;
use roxmltree;
use crate::minio::types::{BucketInfo, Err, ListObjectsResp, ObjectInfo, Region};
use crate::minio::woxml;
pub fn parse_bucket_location(s: String) -> Result<Region, Err> {
let res = roxmltree::Document::parse(&s);
match res {