use std::convert::TryFrom;
pub use api::objects::Metadata;
pub use hyper::body::Bytes;
use hyper::client::Client;
pub use tame_gcs as api;
use tame_gcs::{
objects,
types::{BucketName, ObjectId, ObjectName},
ApiResponse,
};
use crate::builder;
const READ_WRITE_SCOPE: &str = "https://www.googleapis.com/auth/devstorage.read_write";
fn empty_body(req: http::Request<std::io::Empty>) -> http::Request<hyper::Body> {
let (parts, std::io::Empty { .. }) = req.into_parts();
http::Request::from_parts(parts, hyper::Body::empty())
}
async fn collect_body(
response: http::Response<hyper::Body>,
) -> Result<http::Response<Bytes>, hyper::Error> {
let (parts, body) = response.into_parts();
let bytes = hyper::body::to_bytes(body).await?;
Ok(http::Response::from_parts(parts, bytes))
}
#[derive(Debug, thiserror::Error)]
pub enum InvalidNameError {
#[error("error validating bucket name {1}")]
Bucket(#[source] api::Error, String),
#[error("error validating object name {1}")]
Object(#[source] api::Error, String),
}
fn bucket(name: &str) -> Result<BucketName, api::Error> {
let count = name.chars().count();
if !(3..=63).contains(&count) {
return Err(api::Error::InvalidCharacterCount {
len: count,
min: 3,
max: 63,
});
}
let last = count - 1;
for (i, c) in name.chars().enumerate() {
if c.is_ascii_uppercase() {
return Err(api::Error::InvalidCharacter(i, c));
}
match c {
'a'..='z' | '0'..='9' => {}
'-' | '_' | '.' => {
if i == 0 || i == last {
return Err(api::Error::InvalidCharacter(i, c));
}
}
c => {
return Err(api::Error::InvalidCharacter(i, c));
}
}
}
if name.starts_with("goog") {
return Err(api::Error::InvalidPrefix("goog"));
}
if name.contains("google") || name.contains("g00gle") {
return Err(api::Error::InvalidSequence("google"));
}
Ok(BucketName::non_validated(name.into()))
}
fn names_to_object<'a>(
bucket_name: &'a str,
object_name: &'a str,
) -> Result<ObjectId<'a>, InvalidNameError> {
let bucket =
bucket(bucket_name).map_err(|e| InvalidNameError::Bucket(e, bucket_name.to_owned()))?;
let object = ObjectName::try_from(object_name)
.map_err(|e| InvalidNameError::Object(e, object_name.to_owned()))?;
Ok(ObjectId { bucket, object })
}
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
#[error("error in fetching auth token")]
Fetch(#[source] yup_oauth2::Error),
#[error("Auth did not generate a token")]
MissingToken,
#[error("token does not form a valid HTTP header value: {}", _1)]
InvalidToken(#[source] http::header::InvalidHeaderValue, String),
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ObjectError {
#[error(transparent)]
InvalidName(#[from] InvalidNameError),
#[error("error in creating request")]
InvalidRequest(#[source] api::Error),
#[error(transparent)]
Auth(#[from] AuthError),
#[error("error in sending request")]
Request(#[source] hyper::Error),
#[error("error in receiving response")]
Response(#[source] hyper::Error),
#[error("received unsuccessful response")]
Failure(#[source] api::Error),
}
pub struct StorageClient<C = builder::DefaultConnector> {
client: Client<C>,
auth: Option<crate::Auth<C>>,
}
impl<C> StorageClient<C>
where
C: tower::Service<http::Uri> + Clone + Send + Sync + 'static,
C::Response: hyper::client::connect::Connection
+ tokio::io::AsyncRead
+ tokio::io::AsyncWrite
+ Send
+ Unpin
+ 'static,
C::Future: Send + Unpin + 'static,
C::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
{
async fn send_request(
&self,
mut request: http::Request<hyper::Body>,
) -> Result<http::Response<Bytes>, ObjectError> {
if let Some(auth) = &self.auth {
let token = auth
.token(&[READ_WRITE_SCOPE])
.await
.map_err(AuthError::Fetch)?;
let token = token.token().ok_or(AuthError::MissingToken)?;
crate::auth::add_auth_token(&mut request, &token)
.map_err(|e| AuthError::InvalidToken(e, token.to_owned()))?;
}
let response = self
.client
.request(request)
.await
.map_err(ObjectError::Request)?;
let response_bytes = collect_body(response)
.await
.map_err(ObjectError::Response)?;
Ok(response_bytes)
}
pub async fn get_object(
&self,
bucket_name: impl AsRef<str>,
object_name: impl AsRef<str>,
) -> Result<Bytes, ObjectError> {
let oid = names_to_object(bucket_name.as_ref(), object_name.as_ref())?;
let request = objects::Object::download(&oid, None).map_err(ObjectError::InvalidRequest)?;
let response = self.send_request(empty_body(request)).await?;
Ok(objects::DownloadObjectResponse::try_from_parts(response)
.map_err(ObjectError::Failure)?
.consume())
}
pub async fn get_metadata(
&self,
bucket_name: impl AsRef<str>,
object_name: impl AsRef<str>,
) -> Result<Metadata, ObjectError> {
let oid = names_to_object(bucket_name.as_ref(), object_name.as_ref())?;
let request = objects::Object::get(&oid, None).map_err(ObjectError::InvalidRequest)?;
let response = self.send_request(empty_body(request)).await?;
Ok(objects::GetObjectResponse::try_from_parts(response)
.map_err(ObjectError::Failure)?
.metadata)
}
pub async fn insert_object(
&self,
bucket_name: impl AsRef<str>,
object_name: impl AsRef<str>,
data: impl Into<Bytes>,
) -> Result<Metadata, ObjectError> {
let oid = names_to_object(bucket_name.as_ref(), object_name.as_ref())?;
let data: Bytes = data.into();
let data_len = data.len();
let body = hyper::Body::from(data);
let request = objects::Object::insert_simple(
&oid,
body,
u64::try_from(data_len).expect("data length should fit in u64"),
None,
)
.map_err(ObjectError::InvalidRequest)?;
let response = self.send_request(request).await?;
Ok(objects::InsertResponse::try_from_parts(response)
.map_err(ObjectError::Failure)?
.metadata)
}
pub async fn insert_with_metadata(
&self,
bucket_name: impl AsRef<str>,
metadata: &Metadata,
data: impl Into<Bytes>,
) -> Result<Metadata, ObjectError> {
let bucket = bucket(bucket_name.as_ref())
.map_err(|e| InvalidNameError::Bucket(e, bucket_name.as_ref().to_owned()))?;
let data: Bytes = data.into();
let data_len = data.len();
let request = objects::Object::insert_multipart(
&bucket,
std::io::Cursor::new(data),
u64::try_from(data_len).expect("data length should fit in u64"),
metadata,
None,
)
.map_err(ObjectError::InvalidRequest)?;
let (parts, mut content) = request.into_parts();
let mut buf = Vec::with_capacity(
usize::try_from(content.total_len()).expect("content length should fit in usize"),
);
<objects::Multipart<std::io::Cursor<Bytes>> as std::io::Read>::read_to_end(
&mut content,
&mut buf,
)
.expect("in-memory read should not error");
let request = http::Request::from_parts(parts, hyper::Body::from(buf));
let response = self.send_request(request).await?;
Ok(objects::InsertResponse::try_from_parts(response)
.map_err(ObjectError::Failure)?
.metadata)
}
}
impl<C> builder::ClientBuilder<C>
where
C: Clone,
{
pub fn build_storage_client(&self) -> StorageClient<C> {
StorageClient {
client: self.client.clone(),
auth: self.auth.clone(),
}
}
}