objectstore_client/
put.rs1use std::fmt;
2use std::io::{self, Cursor};
3use std::path::PathBuf;
4use std::{borrow::Cow, collections::BTreeMap};
5
6use async_compression::tokio::bufread::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::{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 #[deprecated(since = "0.3.0", note = "renamed to `compress`")]
178 pub fn compression(self, compression: impl Into<Option<Compression>>) -> Self {
179 self.compress(compression)
180 }
181
182 pub fn precompressed(mut self, compression: Compression) -> Self {
204 self.compression = Some(CompressionMode::Precompressed(compression));
205 self
206 }
207
208 pub fn expiration_policy(mut self, expiration_policy: ExpirationPolicy) -> Self {
212 self.metadata.expiration_policy = expiration_policy;
213 self
214 }
215
216 pub fn content_type(mut self, content_type: impl Into<Cow<'static, str>>) -> Self {
221 self.metadata.content_type = content_type.into();
222 self
223 }
224
225 pub fn origin(mut self, origin: impl Into<String>) -> Self {
243 self.metadata.origin = Some(origin.into());
244 self
245 }
246
247 pub fn filename(mut self, filename: impl Into<String>) -> Self {
253 self.metadata.filename = Some(filename.into());
254 self
255 }
256
257 pub fn set_metadata(mut self, metadata: impl Into<BTreeMap<String, String>>) -> Self {
261 self.metadata.custom = metadata.into();
262 self
263 }
264
265 pub fn append_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
267 self.metadata.custom.insert(key.into(), value.into());
268 self
269 }
270}
271
272pub(crate) async fn encode_body(body: PutBody, mode: Option<CompressionMode>) -> io::Result<Body> {
276 let compression = match mode {
277 Some(CompressionMode::Compress(compression)) => Some(compression),
278 Some(CompressionMode::Precompressed(_)) | None => None,
280 };
281
282 Ok(match (compression, body) {
283 (Some(Compression::Zstd), PutBody::Buffer(bytes)) => {
284 let cursor = Cursor::new(bytes);
285 let encoder = ZstdEncoder::new(cursor);
286 let stream = ReaderStream::new(encoder);
287 Body::wrap_stream(stream)
288 }
289 (Some(Compression::Zstd), PutBody::Stream(stream)) => {
290 let stream = StreamReader::new(stream);
291 let encoder = ZstdEncoder::new(stream);
292 let stream = ReaderStream::new(encoder);
293 Body::wrap_stream(stream)
294 }
295 (Some(Compression::Zstd), PutBody::File(file)) => {
296 let reader = BufReader::new(file);
297 let encoder = ZstdEncoder::new(reader);
298 let stream = ReaderStream::new(encoder);
299 Body::wrap_stream(stream)
300 }
301 (Some(Compression::Zstd), PutBody::Path(file)) => {
302 let file = File::open(file).await?;
303 let reader = BufReader::new(file);
304 let encoder = ZstdEncoder::new(reader);
305 let stream = ReaderStream::new(encoder);
306 Body::wrap_stream(stream)
307 }
308 (None, PutBody::Buffer(bytes)) => bytes.into(),
309 (None, PutBody::Stream(stream)) => Body::wrap_stream(stream),
310 (None, PutBody::File(file)) => {
311 let stream = ReaderStream::new(file);
312 Body::wrap_stream(stream)
313 }
314 (None, PutBody::Path(path)) => {
315 let stream = ReaderStream::new(File::open(path).await?);
316 Body::wrap_stream(stream)
317 }
318 })
319}
320
321impl PutBuilder {
325 pub async fn send(mut self) -> crate::Result<PutResponse> {
327 let method = match self.key {
328 Some(_) => reqwest::Method::PUT,
329 None => reqwest::Method::POST,
330 };
331
332 let mut builder = self
333 .session
334 .request(method, self.key.as_deref().unwrap_or_default())?;
335
336 self.metadata.compression = self.compression.map(CompressionMode::compression);
337 let body = encode_body(self.body, self.compression).await?;
338
339 builder = builder.headers(self.metadata.to_headers("")?);
340
341 let response = builder.body(body).send().await?;
342 Ok(response.error_for_status_and_drain().await?.json().await?)
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use futures_util::stream;
349 use http_body_util::BodyExt as _;
350
351 use super::*;
352
353 fn zstd_compress(data: &[u8]) -> Vec<u8> {
354 zstd::encode_all(Cursor::new(data), 0).expect("zstd encoding to succeed")
355 }
356
357 fn stream_body(chunks: Vec<&'static [u8]>) -> PutBody {
358 let chunks = chunks.into_iter().map(|c| Ok(Bytes::from_static(c)));
359 PutBody::Stream(stream::iter(chunks).boxed())
360 }
361
362 async fn collect(body: Body) -> Vec<u8> {
363 body.collect()
364 .await
365 .expect("body to be readable")
366 .to_bytes()
367 .to_vec()
368 }
369
370 #[tokio::test]
371 async fn compress_buffer_compresses() {
372 let body = PutBody::Buffer(Bytes::from_static(b"hello world"));
373 let mode = Some(CompressionMode::Compress(Compression::Zstd));
374
375 let encoded = collect(encode_body(body, mode).await.unwrap()).await;
376 assert_eq!(encoded, zstd_compress(b"hello world"));
377 }
378
379 #[tokio::test]
380 async fn compress_stream_compresses() {
381 let body = stream_body(vec![b"hello ", b"world"]);
382 let mode = Some(CompressionMode::Compress(Compression::Zstd));
383
384 let encoded = collect(encode_body(body, mode).await.unwrap()).await;
385 assert_eq!(
386 zstd::decode_all(Cursor::new(encoded)).unwrap(),
387 b"hello world"
388 );
389 }
390
391 #[tokio::test]
392 async fn precompressed_buffer_is_forwarded_verbatim() {
393 let compressed = zstd_compress(b"hello world");
394 let body = PutBody::Buffer(Bytes::from(compressed.clone()));
395 let mode = Some(CompressionMode::Precompressed(Compression::Zstd));
396
397 let encoded = collect(encode_body(body, mode).await.unwrap()).await;
398 assert_eq!(encoded, compressed);
399 }
400
401 #[tokio::test]
402 async fn precompressed_stream_is_forwarded_verbatim() {
403 let body = stream_body(vec![b"\x28\xb5\x2f\xfd", b"trailing"]);
404 let mode = Some(CompressionMode::Precompressed(Compression::Zstd));
405
406 let encoded = collect(encode_body(body, mode).await.unwrap()).await;
407 assert_eq!(encoded, b"\x28\xb5\x2f\xfdtrailing");
408 }
409
410 #[tokio::test]
411 async fn without_compression_is_forwarded_verbatim() {
412 let body = PutBody::Buffer(Bytes::from_static(b"hello world"));
413
414 let encoded = collect(encode_body(body, None).await.unwrap()).await;
415 assert_eq!(encoded, b"hello world");
416 }
417}