use std::fmt;
use std::io::{self, Cursor};
use std::path::PathBuf;
use std::{borrow::Cow, collections::BTreeMap};
use async_compression::tokio::bufread::ZstdEncoder;
use bytes::Bytes;
use futures_util::StreamExt;
use objectstore_types::metadata::Metadata;
use reqwest::Body;
use serde::Deserialize;
use tokio::fs::File;
use tokio::io::{AsyncRead, BufReader};
use tokio_util::io::{ReaderStream, StreamReader};
pub use objectstore_types::metadata::{Compression, ExpirationPolicy};
use crate::{ClientStream, ObjectKey, Session};
#[derive(Debug, Deserialize)]
pub struct PutResponse {
pub key: ObjectKey,
}
pub(crate) enum PutBody {
Buffer(Bytes),
Stream(ClientStream),
File(File),
Path(PathBuf),
}
impl fmt::Debug for PutBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("PutBody").finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CompressionMode {
Compress(Compression),
Precompressed(Compression),
}
impl CompressionMode {
pub fn compression(self) -> Compression {
match self {
Self::Compress(compression) | Self::Precompressed(compression) => compression,
}
}
}
impl Session {
fn put_body(&self, body: PutBody) -> PutBuilder {
let metadata = Metadata {
expiration_policy: self.scope.usecase().expiration_policy(),
..Default::default()
};
PutBuilder {
session: self.clone(),
metadata,
compression: self
.scope
.usecase()
.compression()
.map(CompressionMode::Compress),
key: None,
body,
}
}
pub fn put(&self, body: impl Into<Bytes>) -> PutBuilder {
self.put_body(PutBody::Buffer(body.into()))
}
pub fn put_stream(&self, body: ClientStream) -> PutBuilder {
self.put_body(PutBody::Stream(body))
}
pub fn put_read<R>(&self, body: R) -> PutBuilder
where
R: AsyncRead + Send + Sync + 'static,
{
let stream = ReaderStream::new(body).boxed();
self.put_body(PutBody::Stream(stream))
}
pub fn put_file(&self, file: File) -> PutBuilder {
self.put_body(PutBody::File(file))
}
pub fn put_path(&self, path: impl Into<PathBuf>) -> PutBuilder {
self.put_body(PutBody::Path(path.into()))
}
}
#[derive(Debug)]
pub struct PutBuilder {
pub(crate) session: Session,
pub(crate) metadata: Metadata,
pub(crate) compression: Option<CompressionMode>,
pub(crate) key: Option<ObjectKey>,
pub(crate) body: PutBody,
}
impl PutBuilder {
pub fn key(mut self, key: impl Into<ObjectKey>) -> Self {
self.key = Some(key.into()).filter(|k| !k.is_empty());
self
}
pub fn compress(mut self, compression: impl Into<Option<Compression>>) -> Self {
self.compression = compression.into().map(CompressionMode::Compress);
self
}
#[deprecated(since = "0.3.0", note = "renamed to `compress`")]
pub fn compression(self, compression: impl Into<Option<Compression>>) -> Self {
self.compress(compression)
}
pub fn precompressed(mut self, compression: Compression) -> Self {
self.compression = Some(CompressionMode::Precompressed(compression));
self
}
pub fn expiration_policy(mut self, expiration_policy: ExpirationPolicy) -> Self {
self.metadata.expiration_policy = expiration_policy;
self
}
pub fn content_type(mut self, content_type: impl Into<Cow<'static, str>>) -> Self {
self.metadata.content_type = content_type.into();
self
}
pub fn origin(mut self, origin: impl Into<String>) -> Self {
self.metadata.origin = Some(origin.into());
self
}
pub fn filename(mut self, filename: impl Into<String>) -> Self {
self.metadata.filename = Some(filename.into());
self
}
pub fn set_metadata(mut self, metadata: impl Into<BTreeMap<String, String>>) -> Self {
self.metadata.custom = metadata.into();
self
}
pub fn append_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.custom.insert(key.into(), value.into());
self
}
}
pub(crate) async fn encode_body(body: PutBody, mode: Option<CompressionMode>) -> io::Result<Body> {
let compression = match mode {
Some(CompressionMode::Compress(compression)) => Some(compression),
Some(CompressionMode::Precompressed(_)) | None => None,
};
Ok(match (compression, body) {
(Some(Compression::Zstd), PutBody::Buffer(bytes)) => {
let cursor = Cursor::new(bytes);
let encoder = ZstdEncoder::new(cursor);
let stream = ReaderStream::new(encoder);
Body::wrap_stream(stream)
}
(Some(Compression::Zstd), PutBody::Stream(stream)) => {
let stream = StreamReader::new(stream);
let encoder = ZstdEncoder::new(stream);
let stream = ReaderStream::new(encoder);
Body::wrap_stream(stream)
}
(Some(Compression::Zstd), PutBody::File(file)) => {
let reader = BufReader::new(file);
let encoder = ZstdEncoder::new(reader);
let stream = ReaderStream::new(encoder);
Body::wrap_stream(stream)
}
(Some(Compression::Zstd), PutBody::Path(file)) => {
let file = File::open(file).await?;
let reader = BufReader::new(file);
let encoder = ZstdEncoder::new(reader);
let stream = ReaderStream::new(encoder);
Body::wrap_stream(stream)
}
(None, PutBody::Buffer(bytes)) => bytes.into(),
(None, PutBody::Stream(stream)) => Body::wrap_stream(stream),
(None, PutBody::File(file)) => {
let stream = ReaderStream::new(file);
Body::wrap_stream(stream)
}
(None, PutBody::Path(path)) => {
let stream = ReaderStream::new(File::open(path).await?);
Body::wrap_stream(stream)
}
})
}
impl PutBuilder {
pub async fn send(mut self) -> crate::Result<PutResponse> {
let method = match self.key {
Some(_) => reqwest::Method::PUT,
None => reqwest::Method::POST,
};
let mut builder = self
.session
.request(method, self.key.as_deref().unwrap_or_default())?;
self.metadata.compression = self.compression.map(CompressionMode::compression);
let body = encode_body(self.body, self.compression).await?;
builder = builder.headers(self.metadata.to_headers("")?);
let response = builder.body(body).send().await?;
Ok(response.error_for_status()?.json().await?)
}
}
#[cfg(test)]
mod tests {
use futures_util::stream;
use http_body_util::BodyExt as _;
use super::*;
fn zstd_compress(data: &[u8]) -> Vec<u8> {
zstd::encode_all(Cursor::new(data), 0).expect("zstd encoding to succeed")
}
fn stream_body(chunks: Vec<&'static [u8]>) -> PutBody {
let chunks = chunks.into_iter().map(|c| Ok(Bytes::from_static(c)));
PutBody::Stream(stream::iter(chunks).boxed())
}
async fn collect(body: Body) -> Vec<u8> {
body.collect()
.await
.expect("body to be readable")
.to_bytes()
.to_vec()
}
#[tokio::test]
async fn compress_buffer_compresses() {
let body = PutBody::Buffer(Bytes::from_static(b"hello world"));
let mode = Some(CompressionMode::Compress(Compression::Zstd));
let encoded = collect(encode_body(body, mode).await.unwrap()).await;
assert_eq!(encoded, zstd_compress(b"hello world"));
}
#[tokio::test]
async fn compress_stream_compresses() {
let body = stream_body(vec![b"hello ", b"world"]);
let mode = Some(CompressionMode::Compress(Compression::Zstd));
let encoded = collect(encode_body(body, mode).await.unwrap()).await;
assert_eq!(
zstd::decode_all(Cursor::new(encoded)).unwrap(),
b"hello world"
);
}
#[tokio::test]
async fn precompressed_buffer_is_forwarded_verbatim() {
let compressed = zstd_compress(b"hello world");
let body = PutBody::Buffer(Bytes::from(compressed.clone()));
let mode = Some(CompressionMode::Precompressed(Compression::Zstd));
let encoded = collect(encode_body(body, mode).await.unwrap()).await;
assert_eq!(encoded, compressed);
}
#[tokio::test]
async fn precompressed_stream_is_forwarded_verbatim() {
let body = stream_body(vec![b"\x28\xb5\x2f\xfd", b"trailing"]);
let mode = Some(CompressionMode::Precompressed(Compression::Zstd));
let encoded = collect(encode_body(body, mode).await.unwrap()).await;
assert_eq!(encoded, b"\x28\xb5\x2f\xfdtrailing");
}
#[tokio::test]
async fn without_compression_is_forwarded_verbatim() {
let body = PutBody::Buffer(Bytes::from_static(b"hello world"));
let encoded = collect(encode_body(body, None).await.unwrap()).await;
assert_eq!(encoded, b"hello world");
}
}