mirror of
https://github.com/minio/minio-rs.git
synced 2026-01-22 15:42:10 +08:00
Add listen bucket notification (#3)
This commit is contained in:
parent
89d0c3accf
commit
128ecb871f
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
/target
|
/target
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
.idea
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "minio-rs"
|
name = "minio-rs"
|
||||||
version = "0.1.0"
|
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"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@ -12,6 +12,10 @@ hyper = "0.12.28"
|
|||||||
hyper-tls = "0.3.2"
|
hyper-tls = "0.3.2"
|
||||||
ring = "0.14.6"
|
ring = "0.14.6"
|
||||||
roxmltree = "0.6.0"
|
roxmltree = "0.6.0"
|
||||||
|
serde = "1.0.92"
|
||||||
|
serde_derive = "1.0.91"
|
||||||
|
serde_json = "1.0.39"
|
||||||
time = "0.1.42"
|
time = "0.1.42"
|
||||||
|
tokio = "0.1.21"
|
||||||
xml-rs = "0.8.0"
|
xml-rs = "0.8.0"
|
||||||
|
|
||||||
|
|||||||
23
src/lib.rs
23
src/lib.rs
@ -1,9 +1,18 @@
|
|||||||
#[cfg(test)]
|
/*
|
||||||
mod tests {
|
* MinIO Rust Library for Amazon S3 Compatible Cloud Storage
|
||||||
#[test]
|
* Copyright 2019 MinIO, Inc.
|
||||||
fn it_works() {
|
*
|
||||||
assert_eq!(2 + 2, 4);
|
* 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;
|
pub mod minio;
|
||||||
|
|||||||
21
src/main.rs
21
src/main.rs
@ -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 futures::{future::Future, stream::Stream};
|
||||||
use hyper::rt;
|
use hyper::rt;
|
||||||
|
|
||||||
use minio::BucketInfo;
|
use minio::BucketInfo;
|
||||||
|
|
||||||
|
mod minio;
|
||||||
|
|
||||||
fn get_local_default_server() -> minio::Client {
|
fn get_local_default_server() -> minio::Client {
|
||||||
match minio::Client::new("http://localhost:9000") {
|
match minio::Client::new("http://localhost:9000") {
|
||||||
Ok(mut c) => {
|
Ok(mut c) => {
|
||||||
|
|||||||
244
src/minio.rs
244
src/minio.rs
@ -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 api;
|
||||||
|
mod net;
|
||||||
|
mod notification;
|
||||||
mod sign;
|
mod sign;
|
||||||
mod types;
|
mod types;
|
||||||
mod xml;
|
mod xml;
|
||||||
|
|
||||||
mod woxml;
|
mod woxml;
|
||||||
|
|
||||||
use bytes::Bytes;
|
pub const SPACE_BYTE: &[u8; 1] = b" ";
|
||||||
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;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Credentials {
|
pub struct Credentials {
|
||||||
@ -71,18 +94,20 @@ pub struct Client {
|
|||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
pub fn new(server: &str) -> Result<Client, Err> {
|
pub fn new(server: &str) -> Result<Client, Err> {
|
||||||
let v = server.parse::<Uri>();
|
let valid = server.parse::<Uri>();
|
||||||
match v {
|
match valid {
|
||||||
Ok(s) => {
|
Ok(server_uri) => {
|
||||||
if s.host().is_none() {
|
if server_uri.host().is_none() {
|
||||||
Err(Err::InvalidUrl("no host specified!".to_string()))
|
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()))
|
Err(Err::InvalidUrl("invalid scheme!".to_string()))
|
||||||
} else {
|
} else {
|
||||||
Ok(Client {
|
Ok(Client {
|
||||||
server: s.clone(),
|
server: server_uri.clone(),
|
||||||
region: Region::empty(),
|
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())
|
ConnClient::HttpCC(client::Client::new())
|
||||||
} else {
|
} else {
|
||||||
let https = HttpsConnector::new(4).unwrap();
|
let https = HttpsConnector::new(4).unwrap();
|
||||||
@ -106,14 +131,14 @@ impl Client {
|
|||||||
self.region = r;
|
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() {
|
let host_val = match self.server.port_part() {
|
||||||
Some(port) => format!("{}:{}", self.server.host().unwrap_or(""), port),
|
Some(port) => format!("{}:{}", self.server.host().unwrap_or(""), port),
|
||||||
None => self.server.host().unwrap_or("").to_string(),
|
None => self.server.host().unwrap_or("").to_string(),
|
||||||
};
|
};
|
||||||
match header::HeaderValue::from_str(&host_val) {
|
match header::HeaderValue::from_str(&host_val) {
|
||||||
Ok(v) => {
|
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> {
|
/// get_bucket_location - Get location for the bucket_name.
|
||||||
let mut qp = HashMap::new();
|
pub fn get_bucket_location(
|
||||||
qp.insert("location".to_string(), None);
|
&self,
|
||||||
|
bucket_name: &str,
|
||||||
|
) -> impl Future<Item = Region, Error = Err> {
|
||||||
|
let mut qp = Values::new();
|
||||||
|
qp.set_value("location", None);
|
||||||
|
|
||||||
let s3_req = S3Req {
|
let s3_req = S3Req {
|
||||||
method: Method::GET,
|
method: Method::GET,
|
||||||
bucket: Some(b.to_string()),
|
bucket: Some(bucket_name.to_string()),
|
||||||
object: None,
|
object: None,
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
query: qp,
|
query: qp,
|
||||||
@ -204,18 +233,18 @@ impl Client {
|
|||||||
resp.into_body()
|
resp.into_body()
|
||||||
.concat2()
|
.concat2()
|
||||||
.map_err(|err| Err::HyperErr(err))
|
.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))
|
.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 {
|
let s3_req = S3Req {
|
||||||
method: Method::DELETE,
|
method: Method::DELETE,
|
||||||
bucket: Some(b.to_string()),
|
bucket: Some(bucket_name.to_string()),
|
||||||
object: None,
|
object: None,
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
query: HashMap::new(),
|
query: Values::new(),
|
||||||
body: Body::empty(),
|
body: Body::empty(),
|
||||||
ts: time::now_utc(),
|
ts: time::now_utc(),
|
||||||
};
|
};
|
||||||
@ -223,13 +252,13 @@ impl Client {
|
|||||||
.and_then(|_| Ok(()))
|
.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 {
|
let s3_req = S3Req {
|
||||||
method: Method::HEAD,
|
method: Method::HEAD,
|
||||||
bucket: Some(b.to_string()),
|
bucket: Some(bucket_name.to_string()),
|
||||||
object: None,
|
object: None,
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
query: HashMap::new(),
|
query: Values::new(),
|
||||||
body: Body::empty(),
|
body: Body::empty(),
|
||||||
ts: time::now_utc(),
|
ts: time::now_utc(),
|
||||||
};
|
};
|
||||||
@ -250,7 +279,7 @@ impl Client {
|
|||||||
|
|
||||||
pub fn get_object_req(
|
pub fn get_object_req(
|
||||||
&self,
|
&self,
|
||||||
b: &str,
|
bucket_name: &str,
|
||||||
key: &str,
|
key: &str,
|
||||||
get_obj_opts: Vec<(HeaderName, HeaderValue)>,
|
get_obj_opts: Vec<(HeaderName, HeaderValue)>,
|
||||||
) -> impl Future<Item = GetObjectResp, Error = Err> {
|
) -> impl Future<Item = GetObjectResp, Error = Err> {
|
||||||
@ -264,10 +293,10 @@ impl Client {
|
|||||||
|
|
||||||
let s3_req = S3Req {
|
let s3_req = S3Req {
|
||||||
method: Method::GET,
|
method: Method::GET,
|
||||||
bucket: Some(b.to_string()),
|
bucket: Some(bucket_name.to_string()),
|
||||||
object: Some(key.to_string()),
|
object: Some(key.to_string()),
|
||||||
headers: h,
|
headers: h,
|
||||||
query: HashMap::new(),
|
query: Values::new(),
|
||||||
body: Body::empty(),
|
body: Body::empty(),
|
||||||
ts: time::now_utc(),
|
ts: time::now_utc(),
|
||||||
};
|
};
|
||||||
@ -276,14 +305,14 @@ impl Client {
|
|||||||
.and_then(GetObjectResp::new)
|
.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 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 {
|
let s3_req = S3Req {
|
||||||
method: Method::PUT,
|
method: Method::PUT,
|
||||||
bucket: Some(bucket),
|
bucket: Some(bucket),
|
||||||
object: None,
|
object: None,
|
||||||
query: HashMap::new(),
|
query: Values::new(),
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
body: Body::empty(),
|
body: Body::empty(),
|
||||||
ts: time::now_utc(),
|
ts: time::now_utc(),
|
||||||
@ -297,7 +326,7 @@ impl Client {
|
|||||||
method: Method::GET,
|
method: Method::GET,
|
||||||
bucket: None,
|
bucket: None,
|
||||||
object: None,
|
object: None,
|
||||||
query: HashMap::new(),
|
query: Values::new(),
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
body: Body::empty(),
|
body: Body::empty(),
|
||||||
ts: time::now_utc(),
|
ts: time::now_utc(),
|
||||||
@ -308,7 +337,7 @@ impl Client {
|
|||||||
resp.into_body()
|
resp.into_body()
|
||||||
.concat2()
|
.concat2()
|
||||||
.map_err(|err| Err::HyperErr(err))
|
.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))
|
.and_then(|s| xml::parse_bucket_list(s))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -321,21 +350,21 @@ impl Client {
|
|||||||
delimiter: Option<&str>,
|
delimiter: Option<&str>,
|
||||||
max_keys: Option<i32>,
|
max_keys: Option<i32>,
|
||||||
) -> impl Future<Item = ListObjectsResp, Error = Err> {
|
) -> impl Future<Item = ListObjectsResp, Error = Err> {
|
||||||
let mut qparams = HashMap::new();
|
let mut qparams: Values = Values::new();
|
||||||
qparams.insert("list-type".to_string(), Some("2".to_string()));
|
qparams.set_value("list-type", Some("2".to_string()));
|
||||||
if let Some(d) = delimiter {
|
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 {
|
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 {
|
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 {
|
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 {
|
let s3_req = S3Req {
|
||||||
@ -352,10 +381,68 @@ impl Client {
|
|||||||
resp.into_body()
|
resp.into_body()
|
||||||
.concat2()
|
.concat2()
|
||||||
.map_err(|err| Err::HyperErr(err))
|
.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))
|
.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(
|
fn run_req_future(
|
||||||
@ -375,10 +462,11 @@ fn run_req_future(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn b2s(b: Bytes) -> Result<String, Err> {
|
/// Converts a `hyper::Chunk` into a string.
|
||||||
match String::from_utf8(b.iter().map(|x| x.clone()).collect::<Vec<u8>>()) {
|
fn chunk_to_string(chunk: &hyper::Chunk) -> Result<String, Err> {
|
||||||
|
match String::from_utf8(chunk.to_vec()) {
|
||||||
Err(e) => Err(Err::Utf8DecodingErr(e)),
|
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>,
|
bucket: Option<String>,
|
||||||
object: Option<String>,
|
object: Option<String>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
query: HashMap<String, Option<String>>,
|
query: Values,
|
||||||
body: Body,
|
body: Body,
|
||||||
ts: Tm,
|
ts: Tm,
|
||||||
}
|
}
|
||||||
@ -405,14 +493,54 @@ impl S3Req {
|
|||||||
res
|
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 {
|
fn mk_query(&self) -> String {
|
||||||
self.query
|
self.query
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(x, y)| match y {
|
.map(|(key, values)| {
|
||||||
Some(v) => format!("{}={}", x, v),
|
values.iter().map(move |value| match value {
|
||||||
None => x.to_string(),
|
Some(v) => format!("{}={}", &key, v),
|
||||||
|
None => format!("{}=", &key,),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
.flatten()
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join("&")
|
.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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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 hyper::{header::HeaderName, header::HeaderValue, Body, Request};
|
||||||
|
|
||||||
|
use crate::minio;
|
||||||
|
|
||||||
pub fn mk_request(
|
pub fn mk_request(
|
||||||
r: &minio::S3Req,
|
r: &minio::S3Req,
|
||||||
svr_str: &str,
|
svr_str: &str,
|
||||||
|
|||||||
113
src/minio/net.rs
Normal file
113
src/minio/net.rs
Normal 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
112
src/minio/notification.rs
Normal 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>,
|
||||||
|
}
|
||||||
@ -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::{
|
use hyper::header::{
|
||||||
HeaderMap, HeaderName, HeaderValue, AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE, USER_AGENT,
|
HeaderMap, HeaderName, HeaderValue, AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE, USER_AGENT,
|
||||||
};
|
};
|
||||||
use ring::{digest, hmac};
|
use ring::{digest, hmac};
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use time::Tm;
|
use time::Tm;
|
||||||
|
|
||||||
use crate::minio;
|
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()
|
s.chars().map(|x| uri_encode(x, encode_slash)).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_canonical_querystr(q: &HashMap<String, Option<String>>) -> String {
|
fn get_canonical_querystr(q: &HashMap<String, Vec<Option<String>>>) -> String {
|
||||||
let mut hs: Vec<(String, Option<String>)> = q.clone().drain().collect();
|
let mut hs: Vec<(String, Vec<Option<String>>)> = q.clone().drain().collect();
|
||||||
|
// sort keys
|
||||||
hs.sort();
|
hs.sort();
|
||||||
let vs: Vec<String> = hs
|
// Build canonical query string
|
||||||
.drain(..)
|
hs.iter()
|
||||||
.map(|(x, y)| {
|
.map(|(key, values)| {
|
||||||
let val_str = match y {
|
values.iter().map(move |value| match value {
|
||||||
Some(s) => uri_encode_str(&s, true),
|
Some(v) => format!("{}={}", &key, uri_encode_str(&v, true)),
|
||||||
None => "".to_string(),
|
None => format!("{}=", &key),
|
||||||
};
|
})
|
||||||
uri_encode_str(&x, true) + "=" + &val_str
|
|
||||||
})
|
})
|
||||||
.collect();
|
.flatten()
|
||||||
vs[..].join("&")
|
.collect::<Vec<String>>()
|
||||||
|
.join("&")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_canonical_request(
|
fn get_canonical_request(
|
||||||
@ -180,3 +199,25 @@ pub fn sign_v4(
|
|||||||
vec![auth_hdr, date_hdr]
|
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="
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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 bytes::Bytes;
|
||||||
use futures::stream::Stream;
|
use futures::stream::Stream;
|
||||||
use hyper::header::{
|
use hyper::header::{
|
||||||
|
|||||||
@ -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;
|
extern crate xml;
|
||||||
|
|
||||||
use xml::writer::{EmitterConfig, EventWriter, XmlEvent};
|
use xml::writer::{EmitterConfig, EventWriter, XmlEvent};
|
||||||
|
|||||||
@ -1,10 +1,29 @@
|
|||||||
use crate::minio::types::{BucketInfo, Err, ListObjectsResp, ObjectInfo, Region};
|
/*
|
||||||
use crate::minio::woxml;
|
* MinIO Rust Library for Amazon S3 Compatible Cloud Storage
|
||||||
use hyper::body::Body;
|
* Copyright 2019 MinIO, Inc.
|
||||||
use roxmltree;
|
*
|
||||||
|
* 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::collections::HashMap;
|
||||||
use std::str::FromStr;
|
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> {
|
pub fn parse_bucket_location(s: String) -> Result<Region, Err> {
|
||||||
let res = roxmltree::Document::parse(&s);
|
let res = roxmltree::Document::parse(&s);
|
||||||
match res {
|
match res {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user