use std::error::Error;
use async_trait::async_trait;
use http::{
header::{HeaderName, CONTENT_LENGTH, CONTENT_TYPE},
HeaderValue, Method,
};
use reqwest::{Response, Url};
use crate::{
bucket::Bucket,
builder::{ArcPointer, BuilderError, RequestBuilder},
decode::RefineObject,
object::{Object, ObjectList},
types::object::{ObjectBase, ObjectPath},
types::{CanonicalizedResource, ContentRange},
};
#[cfg(feature = "put_file")]
use infer::Infer;
#[cfg(test)]
mod test;
const ETAG: &str = "ETag";
const RANGE: &str = "Range";
#[async_trait]
pub trait File<Client>: GetStd
where
Client: Files<ObjectPath>,
{
fn oss_client(&self) -> Client;
async fn put_oss(&self, content: Vec<u8>, content_type: &str) -> Result<Response, FileError> {
let (url, canonicalized) = self.get_std().ok_or(FileError {
kind: FileErrorKind::NotFoundCanonicalizedResource,
})?;
let content_length = content.len().to_string();
let headers = vec![
(CONTENT_LENGTH, header_from_content_length(&content_length)?),
(
CONTENT_TYPE,
content_type.parse().map_err(|e| FileError {
kind: FileErrorKind::InvalidContentType(e),
})?,
),
];
self.oss_client()
.builder_with_header(Method::PUT, url, canonicalized, headers)?
.body(content)
.send_adjust_error()
.await
.map_err(FileError::from)
}
async fn get_oss<Num, R>(&self, range: R) -> Result<Vec<u8>, FileError>
where
R: Into<ContentRange<Num>> + Send + Sync,
ContentRange<Num>: Into<HeaderValue>,
{
let (url, canonicalized) = self.get_std().ok_or(FileError {
kind: FileErrorKind::NotFoundCanonicalizedResource,
})?;
let list: Vec<(_, HeaderValue)> = vec![(
{
#[allow(clippy::unwrap_used)]
RANGE.parse().unwrap()
},
range.into().into(),
)];
let content = self
.oss_client()
.builder_with_header(Method::GET, url, canonicalized, list)?
.send_adjust_error()
.await?
.text()
.await?;
Ok(content.into_bytes())
}
async fn delete_oss(&self) -> Result<(), FileError> {
let (url, canonicalized) = self.get_std().ok_or(FileError {
kind: FileErrorKind::NotFoundCanonicalizedResource,
})?;
self.oss_client()
.builder(Method::DELETE, url, canonicalized)?
.send_adjust_error()
.await?;
Ok(())
}
}
pub trait GetStd {
fn get_std(&self) -> Option<(Url, CanonicalizedResource)>;
}
impl GetStd for ObjectBase<ArcPointer> {
fn get_std(&self) -> Option<(Url, CanonicalizedResource)> {
Some(self.get_url_resource([]))
}
}
impl GetStd for Object<ArcPointer> {
fn get_std(&self) -> Option<(Url, CanonicalizedResource)> {
Some(self.base.get_url_resource([]))
}
}
pub trait GetStdWithPath<Path> {
fn get_std_with_path(&self, _path: Path) -> Option<(Url, CanonicalizedResource)>;
}
#[doc(hidden)]
pub mod std_path_impl {
use std::error::Error;
#[cfg(feature = "blocking")]
use crate::builder::RcPointer;
#[cfg(feature = "blocking")]
use crate::client::ClientRc;
use super::{GetStd, GetStdWithPath};
use crate::{
bucket::Bucket,
builder::ArcPointer,
client::ClientArc,
config::{get_url_resource2 as get_url_resource, BucketBase},
decode::RefineObject,
object::ObjectList,
types::{object::ObjectBase, CanonicalizedResource},
ObjectPath,
};
use oss_derive::oss_gen_rc;
use reqwest::Url;
#[oss_gen_rc]
impl GetStdWithPath<String> for ClientArc {
fn get_std_with_path(&self, path: String) -> Option<(Url, CanonicalizedResource)> {
<Self as GetStdWithPath<&str>>::get_std_with_path(self, &path)
}
}
#[oss_gen_rc]
impl GetStdWithPath<&str> for ClientArc {
fn get_std_with_path(&self, path: &str) -> Option<(Url, CanonicalizedResource)> {
let object_path = path.try_into().ok()?;
Some(get_url_resource(self, self, &object_path))
}
}
#[oss_gen_rc]
impl GetStdWithPath<ObjectPath> for ClientArc {
fn get_std_with_path(&self, path: ObjectPath) -> Option<(Url, CanonicalizedResource)> {
<Self as GetStdWithPath<&ObjectPath>>::get_std_with_path(self, &path)
}
}
#[oss_gen_rc]
impl GetStdWithPath<&ObjectPath> for ClientArc {
fn get_std_with_path(&self, path: &ObjectPath) -> Option<(Url, CanonicalizedResource)> {
Some(get_url_resource(self, self, path))
}
}
#[oss_gen_rc]
impl<Path: AsRef<ObjectPath>> GetStdWithPath<Path> for ClientArc {
fn get_std_with_path(&self, path: Path) -> Option<(Url, CanonicalizedResource)> {
Some(get_url_resource(self, self, path.as_ref()))
}
}
impl<B: AsRef<BucketBase>> GetStdWithPath<String> for B {
fn get_std_with_path(&self, path: String) -> Option<(Url, CanonicalizedResource)> {
<Self as GetStdWithPath<&str>>::get_std_with_path(self, &path)
}
}
impl<B: AsRef<BucketBase>> GetStdWithPath<&str> for B {
fn get_std_with_path(&self, path: &str) -> Option<(Url, CanonicalizedResource)> {
let path = path.try_into().ok()?;
Some(self.as_ref().get_url_resource_with_path(&path))
}
}
impl<B: AsRef<BucketBase>> GetStdWithPath<ObjectPath> for B {
#[inline]
fn get_std_with_path(&self, path: ObjectPath) -> Option<(Url, CanonicalizedResource)> {
<Self as GetStdWithPath<&ObjectPath>>::get_std_with_path(self, &path)
}
}
impl<B: AsRef<BucketBase>> GetStdWithPath<&ObjectPath> for B {
#[inline]
fn get_std_with_path(&self, path: &ObjectPath) -> Option<(Url, CanonicalizedResource)> {
Some(self.as_ref().get_url_resource_with_path(path))
}
}
#[oss_gen_rc]
impl GetStdWithPath<ObjectBase<ArcPointer>> for Bucket {
#[inline]
fn get_std_with_path(
&self,
base: ObjectBase<ArcPointer>,
) -> Option<(Url, CanonicalizedResource)> {
Some(base.get_url_resource([]))
}
}
#[oss_gen_rc]
impl GetStdWithPath<&ObjectBase<ArcPointer>> for Bucket {
#[inline]
fn get_std_with_path(
&self,
base: &ObjectBase<ArcPointer>,
) -> Option<(Url, CanonicalizedResource)> {
Some(base.get_url_resource([]))
}
}
impl<Item: RefineObject<E> + Send + Sync, E: Error + Send + Sync, U: GetStd> GetStdWithPath<U>
for ObjectList<ArcPointer, Item, E>
{
#[inline]
fn get_std_with_path(&self, path: U) -> Option<(Url, CanonicalizedResource)> {
path.get_std()
}
}
}
#[async_trait]
pub trait Files<Path>: AlignBuilder + GetStdWithPath<Path>
where
Path: Send + Sync + 'static,
{
const DEFAULT_CONTENT_TYPE: &'static str = "application/octet-stream";
#[cfg(feature = "put_file")]
async fn put_file<
P: Into<std::path::PathBuf> + std::convert::AsRef<std::path::Path> + Send + Sync,
>(
&self,
file_name: P,
path: Path,
) -> Result<String, FileError> {
let file_content = std::fs::read(file_name).map_err(|e| FileError {
kind: error_impl::FileErrorKind::FileRead(e),
})?;
let get_content_type =
|content: &Vec<u8>| Infer::new().get(content).map(|con| con.mime_type());
self.put_content(file_content, path, get_content_type).await
}
async fn put_content<F>(
&self,
content: Vec<u8>,
path: Path,
get_content_type: F,
) -> Result<String, FileError>
where
F: Fn(&Vec<u8>) -> Option<&'static str> + Send + Sync,
{
let content_type = get_content_type(&content).unwrap_or(Self::DEFAULT_CONTENT_TYPE);
let content = self.put_content_base(content, content_type, path).await?;
let result = content
.headers()
.get(ETAG)
.ok_or(FileError {
kind: FileErrorKind::EtagNotFound,
})?
.to_str()
.map_err(|e| FileError {
kind: FileErrorKind::InvalidEtag(e),
})?;
Ok(result.to_string())
}
async fn put_content_base(
&self,
content: Vec<u8>,
content_type: &str,
path: Path,
) -> Result<Response, FileError> {
let (url, canonicalized) = self.get_std_with_path(path).ok_or(FileError {
kind: FileErrorKind::NotFoundCanonicalizedResource,
})?;
let content_length = content.len().to_string();
let headers = vec![
(CONTENT_LENGTH, header_from_content_length(&content_length)?),
(
CONTENT_TYPE,
content_type.parse().map_err(|e| FileError {
kind: FileErrorKind::InvalidContentType(e),
})?,
),
];
self.builder_with_header(Method::PUT, url, canonicalized, headers)?
.body(content)
.send_adjust_error()
.await
.map_err(FileError::from)
}
async fn get_object<Num, R>(&self, path: Path, range: R) -> Result<Vec<u8>, FileError>
where
R: Into<ContentRange<Num>> + Send + Sync,
ContentRange<Num>: Into<HeaderValue>,
{
let (url, canonicalized) = self.get_std_with_path(path).ok_or(FileError {
kind: FileErrorKind::NotFoundCanonicalizedResource,
})?;
let list: Vec<(_, HeaderValue)> = vec![(
{
#[allow(clippy::unwrap_used)]
RANGE.parse().unwrap()
},
range.into().into(),
)];
let content = self
.builder_with_header(Method::GET, url, canonicalized, list)?
.send_adjust_error()
.await?
.text()
.await?;
Ok(content.into_bytes())
}
async fn delete_object(&self, path: Path) -> Result<(), FileError> {
let (url, canonicalized) = self.get_std_with_path(path).ok_or(FileError {
kind: FileErrorKind::NotFoundCanonicalizedResource,
})?;
self.builder(Method::DELETE, url, canonicalized)?
.send_adjust_error()
.await?;
Ok(())
}
}
fn header_from_content_length(content: &str) -> Result<HeaderValue, FileError> {
HeaderValue::from_str(content).map_err(|e| FileError {
kind: FileErrorKind::InvalidContentLength(e),
})
}
impl<P: Send + Sync + 'static, T: AlignBuilder + GetStdWithPath<P>> Files<P> for T {}
#[derive(Debug)]
pub struct FileError {
kind: error_impl::FileErrorKind,
}
impl FileError {
#[cfg(test)]
pub(crate) fn test_new() -> Self {
Self {
kind: error_impl::FileErrorKind::EtagNotFound,
}
}
}
mod error_impl {
use std::{error::Error, fmt::Display};
use http::header::InvalidHeaderValue;
use crate::builder::BuilderError;
use super::FileError;
impl Display for FileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use FileErrorKind::*;
match &self.kind {
#[cfg(feature = "put_file")]
FileRead(_) => write!(f, "file read failed"),
InvalidContentLength(_) => write!(f, "invalid content length"),
InvalidContentType(_) => write!(f, "invalid content type"),
Build(to) => write!(f, "{to}"),
Reqwest(_) => write!(f, "reqwest error"),
EtagNotFound => write!(f, "failed to get etag"),
InvalidEtag(_) => write!(f, "invalid etag"),
NotFoundCanonicalizedResource => write!(f, "not found canonicalized-resource"),
}
}
}
impl Error for FileError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
use FileErrorKind::*;
match &self.kind {
#[cfg(feature = "put_file")]
FileRead(e) => Some(e),
InvalidContentLength(e) | InvalidContentType(e) => Some(e),
Build(e) => e.source(),
Reqwest(e) => Some(e),
InvalidEtag(e) => Some(e),
EtagNotFound | NotFoundCanonicalizedResource => None,
}
}
}
#[derive(Debug)]
pub(super) enum FileErrorKind {
#[cfg(feature = "put_file")]
FileRead(std::io::Error),
InvalidContentLength(InvalidHeaderValue),
InvalidContentType(InvalidHeaderValue),
Build(BuilderError),
Reqwest(reqwest::Error),
EtagNotFound,
InvalidEtag(http::header::ToStrError),
NotFoundCanonicalizedResource,
}
impl From<BuilderError> for FileError {
fn from(value: BuilderError) -> Self {
Self {
kind: FileErrorKind::Build(value),
}
}
}
impl From<reqwest::Error> for FileError {
fn from(value: reqwest::Error) -> Self {
Self {
kind: FileErrorKind::Reqwest(value),
}
}
}
}
pub trait AlignBuilder: Send + Sync {
#[inline]
fn builder(
&self,
method: Method,
url: Url,
resource: CanonicalizedResource,
) -> Result<RequestBuilder, BuilderError> {
self.builder_with_header(method, url, resource, [])
}
fn builder_with_header<H: IntoIterator<Item = (HeaderName, HeaderValue)>>(
&self,
method: Method,
url: Url,
resource: CanonicalizedResource,
headers: H,
) -> Result<RequestBuilder, BuilderError>;
}
impl AlignBuilder for Bucket {
#[inline]
fn builder_with_header<H: IntoIterator<Item = (HeaderName, HeaderValue)>>(
&self,
method: Method,
url: Url,
resource: CanonicalizedResource,
headers: H,
) -> Result<RequestBuilder, BuilderError> {
self.client()
.builder_with_header(method, url, resource, headers)
}
}
impl<Item: RefineObject<E> + Send + Sync, E: Error + Send + Sync> AlignBuilder
for ObjectList<ArcPointer, Item, E>
{
#[inline]
fn builder_with_header<H: IntoIterator<Item = (HeaderName, HeaderValue)>>(
&self,
method: Method,
url: Url,
resource: CanonicalizedResource,
headers: H,
) -> Result<RequestBuilder, BuilderError> {
self.client()
.builder_with_header(method, url, resource, headers)
}
}
#[cfg(feature = "blocking")]
pub use blocking::Files as BlockingFiles;
use self::error_impl::FileErrorKind;
#[cfg(feature = "blocking")]
pub mod blocking {
use super::{
error_impl::FileErrorKind, header_from_content_length, FileError, GetStdWithPath, ETAG,
RANGE,
};
use crate::{
blocking::builder::RequestBuilder,
bucket::Bucket,
builder::{BuilderError, RcPointer},
object::ObjectList,
types::{CanonicalizedResource, ContentRange},
};
use http::{
header::{HeaderName, CONTENT_LENGTH, CONTENT_TYPE},
HeaderValue, Method,
};
#[cfg(feature = "put_file")]
use infer::Infer;
use reqwest::{blocking::Response, Url};
pub trait Files<Path>: AlignBuilder + GetStdWithPath<Path> {
const DEFAULT_CONTENT_TYPE: &'static str = "application/octet-stream";
#[cfg(feature = "put_file")]
fn put_file<P: Into<std::path::PathBuf> + std::convert::AsRef<std::path::Path>>(
&self,
file_name: P,
path: Path,
) -> Result<String, FileError> {
let file_content = std::fs::read(file_name).map_err(|e| FileError {
kind: FileErrorKind::FileRead(e),
})?;
let get_content_type =
|content: &Vec<u8>| Infer::new().get(content).map(|con| con.mime_type());
self.put_content(file_content, path, get_content_type)
}
fn put_content<F>(
&self,
content: Vec<u8>,
path: Path,
get_content_type: F,
) -> Result<String, FileError>
where
F: Fn(&Vec<u8>) -> Option<&'static str>,
{
let content_type = get_content_type(&content).unwrap_or(Self::DEFAULT_CONTENT_TYPE);
let content = self.put_content_base(content, content_type, path)?;
let result = content
.headers()
.get(ETAG)
.ok_or(FileError {
kind: FileErrorKind::EtagNotFound,
})?
.to_str()
.map_err(|e| FileError {
kind: FileErrorKind::InvalidEtag(e),
})?;
Ok(result.to_string())
}
fn put_content_base(
&self,
content: Vec<u8>,
content_type: &str,
path: Path,
) -> Result<Response, FileError> {
let (url, canonicalized) = self.get_std_with_path(path).ok_or(FileError {
kind: FileErrorKind::NotFoundCanonicalizedResource,
})?;
let content_length = content.len().to_string();
let headers = vec![
(CONTENT_LENGTH, header_from_content_length(&content_length)?),
(
CONTENT_TYPE,
content_type.parse().map_err(|e| FileError {
kind: FileErrorKind::InvalidContentType(e),
})?,
),
];
let response = self
.builder_with_header(Method::PUT, url, canonicalized, headers)?
.body(content);
response.send_adjust_error().map_err(FileError::from)
}
fn get_object<Num, R>(&self, path: Path, range: R) -> Result<Vec<u8>, FileError>
where
R: Into<ContentRange<Num>>,
ContentRange<Num>: Into<HeaderValue>,
{
let (url, canonicalized) = self.get_std_with_path(path).ok_or(FileError {
kind: FileErrorKind::NotFoundCanonicalizedResource,
})?;
let headers: Vec<(_, HeaderValue)> = vec![(
{
#[allow(clippy::unwrap_used)]
RANGE.parse().unwrap()
},
range.into().into(),
)];
Ok(self
.builder_with_header(Method::GET, url, canonicalized, headers)?
.send_adjust_error()?
.text()?
.into_bytes())
}
fn delete_object(&self, path: Path) -> Result<(), FileError> {
let (url, canonicalized) = self.get_std_with_path(path).ok_or(FileError {
kind: FileErrorKind::NotFoundCanonicalizedResource,
})?;
self.builder(Method::DELETE, url, canonicalized)?
.send_adjust_error()?;
Ok(())
}
}
impl<P, T: AlignBuilder + GetStdWithPath<P>> Files<P> for T {}
pub trait AlignBuilder {
#[inline]
fn builder(
&self,
method: Method,
url: Url,
resource: CanonicalizedResource,
) -> Result<RequestBuilder, BuilderError> {
self.builder_with_header(method, url, resource, [])
}
fn builder_with_header<H: IntoIterator<Item = (HeaderName, HeaderValue)>>(
&self,
method: Method,
url: Url,
resource: CanonicalizedResource,
headers: H,
) -> Result<RequestBuilder, BuilderError>;
}
impl AlignBuilder for Bucket<RcPointer> {
fn builder_with_header<H: IntoIterator<Item = (HeaderName, HeaderValue)>>(
&self,
method: Method,
url: Url,
resource: CanonicalizedResource,
headers: H,
) -> Result<RequestBuilder, BuilderError> {
self.client()
.builder_with_header(method, url, resource, headers)
}
}
impl AlignBuilder for ObjectList<RcPointer> {
fn builder_with_header<H: IntoIterator<Item = (HeaderName, HeaderValue)>>(
&self,
method: Method,
url: Url,
resource: CanonicalizedResource,
headers: H,
) -> Result<RequestBuilder, BuilderError> {
self.client()
.builder_with_header(method, url, resource, headers)
}
}
}