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 names_to_object<'a>(
bucket_name: &'a str,
object_name: &'a str,
) -> Result<ObjectId<'a>, InvalidNameError> {
let bucket = BucketName::try_from(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("token does not form a valid HTTP header value: {}", _1.as_str())]
InvalidToken(
#[source] http::header::InvalidHeaderValue,
yup_oauth2::AccessToken,
),
}
#[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: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
{
async fn send_request(
&self,
mut request: http::Request<hyper::Body>,
) -> Result<http::Response<Bytes>, ObjectError> {
if let Some(auth) = &self.auth {
let auth_token = auth
.token(&[READ_WRITE_SCOPE])
.await
.map_err(AuthError::Fetch)?;
crate::auth::add_auth_token(&mut request, &auth_token)
.map_err(|e| AuthError::InvalidToken(e, auth_token))?;
}
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 = BucketName::try_from(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(),
}
}
}