1use std::fmt;
2use std::io::{self, Cursor};
3use std::path::PathBuf;
4use std::{borrow::Cow, collections::BTreeMap};
5
6use async_compression::tokio::bufread::{GzipEncoder, ZstdEncoder};
7use bytes::Bytes;
8use futures_util::StreamExt;
9use objectstore_types::metadata::Metadata;
10use reqwest::Body;
11use serde::Deserialize;
12use tokio::fs::File;
13use tokio::io::{AsyncBufRead, AsyncRead, BufReader};
14use tokio_util::io::{ReaderStream, StreamReader};
15
16pub use objectstore_types::metadata::{Compression, ExpirationPolicy};
17
18use crate::response::ResponseExt as _;
19use crate::{ClientStream, ObjectKey, Session};
20
21#[derive(Debug, Deserialize)]
23pub struct PutResponse {
24 pub key: ObjectKey,
26}
27
28pub(crate) enum PutBody {
29 Buffer(Bytes),
30 Stream(ClientStream),
31 File(File),
32 Path(PathBuf),
33}
34
35impl fmt::Debug for PutBody {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 f.debug_tuple("PutBody").finish_non_exhaustive()
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub(crate) enum CompressionMode {
47 Compress(Compression),
49 Precompressed(Compression),
51}
52
53impl CompressionMode {
54 pub fn compression(self) -> Compression {
56 match self {
57 Self::Compress(compression) | Self::Precompressed(compression) => compression,
58 }
59 }
60}
61
62impl Session {
63 fn put_body(&self, body: PutBody) -> PutBuilder {
64 let metadata = Metadata {
65 expiration_policy: self.scope.usecase().expiration_policy(),
66 ..Default::default()
67 };
68
69 PutBuilder {
70 session: self.clone(),
71 metadata,
72 compression: self
73 .scope
74 .usecase()
75 .compression()
76 .map(CompressionMode::Compress),
77 key: None,
78 body,
79 }
80 }
81
82 pub fn put(&self, body: impl Into<Bytes>) -> PutBuilder {
84 self.put_body(PutBody::Buffer(body.into()))
85 }
86
87 pub fn put_stream(&self, body: ClientStream) -> PutBuilder {
89 self.put_body(PutBody::Stream(body))
90 }
91
92 pub fn put_read<R>(&self, body: R) -> PutBuilder
94 where
95 R: AsyncRead + Send + Sync + 'static,
96 {
97 let stream = ReaderStream::new(body).boxed();
98 self.put_body(PutBody::Stream(stream))
99 }
100
101 pub fn put_file(&self, file: File) -> PutBuilder {
109 self.put_body(PutBody::File(file))
110 }
111
112 pub fn put_path(&self, path: impl Into<PathBuf>) -> PutBuilder {
123 self.put_body(PutBody::Path(path.into()))
124 }
125}
126
127#[derive(Debug)]
129pub struct PutBuilder {
130 pub(crate) session: Session,
131 pub(crate) metadata: Metadata,
132 pub(crate) compression: Option<CompressionMode>,
133 pub(crate) key: Option<ObjectKey>,
134 pub(crate) body: PutBody,
135}
136
137impl PutBuilder {
138 pub fn key(mut self, key: impl Into<ObjectKey>) -> Self {
143 self.key = Some(key.into()).filter(|k| !k.is_empty());
144 self
145 }
146
147 pub fn compress(mut self, compression: impl Into<Option<Compression>>) -> Self {
172 self.compression = compression.into().map(CompressionMode::Compress);
173 self
174 }
175
176 pub fn precompressed(mut self, compression: Compression) -> Self {
198 self.compression = Some(CompressionMode::Precompressed(compression));
199 self
200 }
201
202 pub fn expiration_policy(mut self, expiration_policy: ExpirationPolicy) -> Self {
206 self.metadata.expiration_policy = expiration_policy;
207 self
208 }
209
210 pub fn content_type(mut self, content_type: impl Into<Cow<'static, str>>) -> Self {
215 self.metadata.content_type = content_type.into();
216 self
217 }
218
219 pub fn origin(mut self, origin: impl Into<String>) -> Self {
237 self.metadata.origin = Some(origin.into());
238 self
239 }
240
241 pub fn filename(mut self, filename: impl Into<String>) -> Self {
247 self.metadata.filename = Some(filename.into());
248 self
249 }
250
251 pub fn set_metadata(mut self, metadata: impl Into<BTreeMap<String, String>>) -> Self {
255 self.metadata.custom = metadata.into();
256 self
257 }
258
259 pub fn append_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
261 self.metadata.custom.insert(key.into(), value.into());
262 self
263 }
264}
265
266pub(crate) async fn encode_body(body: PutBody, mode: Option<CompressionMode>) -> io::Result<Body> {
270 let compression = match mode {
271 Some(CompressionMode::Compress(compression)) => Some(compression),
272 Some(CompressionMode::Precompressed(_)) | None => None,
274 };
275
276 Ok(match compression {
277 None => match body {
278 PutBody::Buffer(bytes) => bytes.into(),
279 PutBody::Stream(stream) => Body::wrap_stream(stream),
280 PutBody::File(file) => Body::wrap_stream(ReaderStream::new(file)),
281 PutBody::Path(path) => Body::wrap_stream(ReaderStream::new(File::open(path).await?)),
282 },
283 Some(compression) => match body {
284 PutBody::Buffer(bytes) => compress(Cursor::new(bytes), compression),
285 PutBody::Stream(stream) => compress(StreamReader::new(stream), compression),
286 PutBody::File(file) => compress(BufReader::new(file), compression),
287 PutBody::Path(path) => compress(BufReader::new(File::open(path).await?), compression),
288 },
289 })
290}
291
292fn compress(reader: impl AsyncBufRead + Send + 'static, compression: Compression) -> Body {
293 match compression {
294 Compression::Zstd => Body::wrap_stream(ReaderStream::new(ZstdEncoder::new(reader))),
295 Compression::Gzip => Body::wrap_stream(ReaderStream::new(GzipEncoder::new(reader))),
296 }
297}
298
299impl PutBuilder {
303 pub async fn send(mut self) -> crate::Result<PutResponse> {
305 let method = match self.key {
306 Some(_) => reqwest::Method::PUT,
307 None => reqwest::Method::POST,
308 };
309
310 let mut builder = self
311 .session
312 .request(method, self.key.as_deref().unwrap_or_default())?;
313
314 self.metadata.compression = self.compression.map(CompressionMode::compression);
315 let body = encode_body(self.body, self.compression).await?;
316
317 builder = builder.headers(self.metadata.to_headers("")?);
318
319 let response = builder.body(body).send().await?;
320 Ok(response.error_for_status_and_drain().await?.json().await?)
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use std::io::Read as _;
327
328 use futures_util::stream;
329 use http_body_util::BodyExt as _;
330
331 use super::*;
332
333 fn zstd_compress(data: &[u8]) -> Vec<u8> {
334 zstd::encode_all(Cursor::new(data), 0).expect("zstd encoding to succeed")
335 }
336
337 fn stream_body(chunks: Vec<&'static [u8]>) -> PutBody {
338 let chunks = chunks.into_iter().map(|c| Ok(Bytes::from_static(c)));
339 PutBody::Stream(stream::iter(chunks).boxed())
340 }
341
342 async fn collect(body: Body) -> Vec<u8> {
343 body.collect()
344 .await
345 .expect("body to be readable")
346 .to_bytes()
347 .to_vec()
348 }
349
350 #[tokio::test]
351 async fn compress_buffer_compresses() {
352 let body = PutBody::Buffer(Bytes::from_static(b"hello world"));
353 let mode = Some(CompressionMode::Compress(Compression::Zstd));
354
355 let encoded = collect(encode_body(body, mode).await.unwrap()).await;
356 assert_eq!(encoded, zstd_compress(b"hello world"));
357 }
358
359 #[tokio::test]
360 async fn compress_stream_compresses() {
361 let body = stream_body(vec![b"hello ", b"world"]);
362 let mode = Some(CompressionMode::Compress(Compression::Zstd));
363
364 let encoded = collect(encode_body(body, mode).await.unwrap()).await;
365 assert_eq!(
366 zstd::decode_all(Cursor::new(encoded)).unwrap(),
367 b"hello world"
368 );
369 }
370
371 #[tokio::test]
372 async fn gzip_body_kinds() {
373 let dir = tempfile::tempdir().unwrap();
374 let path = dir.path().join("payload");
375 std::fs::write(&path, b"hello world").unwrap();
376
377 let bodies = [
378 PutBody::Buffer(Bytes::from_static(b"hello world")),
379 stream_body(vec![b"hello ", b"world"]),
380 PutBody::File(File::open(&path).await.unwrap()),
381 PutBody::Path(path.clone()),
382 ];
383
384 for body in bodies {
385 let mode = Some(CompressionMode::Compress(Compression::Gzip));
386 let encoded = collect(encode_body(body, mode).await.unwrap()).await;
387
388 let mut decoded = Vec::new();
389 flate2::read::GzDecoder::new(&encoded[..])
390 .read_to_end(&mut decoded)
391 .unwrap();
392 assert_eq!(decoded, b"hello world");
393 }
394 }
395
396 #[tokio::test]
397 async fn precompressed_buffer_is_forwarded_verbatim() {
398 let compressed = zstd_compress(b"hello world");
399 let body = PutBody::Buffer(Bytes::from(compressed.clone()));
400 let mode = Some(CompressionMode::Precompressed(Compression::Zstd));
401
402 let encoded = collect(encode_body(body, mode).await.unwrap()).await;
403 assert_eq!(encoded, compressed);
404 }
405
406 #[tokio::test]
407 async fn precompressed_stream_is_forwarded_verbatim() {
408 let body = stream_body(vec![b"\x28\xb5\x2f\xfd", b"trailing"]);
409 let mode = Some(CompressionMode::Precompressed(Compression::Zstd));
410
411 let encoded = collect(encode_body(body, mode).await.unwrap()).await;
412 assert_eq!(encoded, b"\x28\xb5\x2f\xfdtrailing");
413 }
414
415 #[tokio::test]
416 async fn without_compression_is_forwarded_verbatim() {
417 let body = PutBody::Buffer(Bytes::from_static(b"hello world"));
418
419 let encoded = collect(encode_body(body, None).await.unwrap()).await;
420 assert_eq!(encoded, b"hello world");
421 }
422}