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::{ClientStream, ObjectKey, Session};
19
20#[derive(Debug, Deserialize)]
22pub struct PutResponse {
23 pub key: ObjectKey,
25}
26
27pub(crate) enum PutBody {
28 Buffer(Bytes),
29 Stream(ClientStream),
30 File(File),
31 Path(PathBuf),
32}
33
34impl fmt::Debug for PutBody {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 f.debug_tuple("PutBody").finish_non_exhaustive()
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub(crate) enum CompressionMode {
46 Compress(Compression),
48 Precompressed(Compression),
50}
51
52impl CompressionMode {
53 pub fn compression(self) -> Compression {
55 match self {
56 Self::Compress(compression) | Self::Precompressed(compression) => compression,
57 }
58 }
59}
60
61impl Session {
62 fn put_body(&self, body: PutBody) -> PutBuilder {
63 let metadata = Metadata {
64 expiration_policy: self.scope.usecase().expiration_policy(),
65 ..Default::default()
66 };
67
68 PutBuilder {
69 session: self.clone(),
70 metadata,
71 compression: self
72 .scope
73 .usecase()
74 .compression()
75 .map(CompressionMode::Compress),
76 key: None,
77 body,
78 }
79 }
80
81 pub fn put(&self, body: impl Into<Bytes>) -> PutBuilder {
83 self.put_body(PutBody::Buffer(body.into()))
84 }
85
86 pub fn put_stream(&self, body: ClientStream) -> PutBuilder {
88 self.put_body(PutBody::Stream(body))
89 }
90
91 pub fn put_read<R>(&self, body: R) -> PutBuilder
93 where
94 R: AsyncRead + Send + Sync + 'static,
95 {
96 let stream = ReaderStream::new(body).boxed();
97 self.put_body(PutBody::Stream(stream))
98 }
99
100 pub fn put_file(&self, file: File) -> PutBuilder {
108 self.put_body(PutBody::File(file))
109 }
110
111 pub fn put_path(&self, path: impl Into<PathBuf>) -> PutBuilder {
122 self.put_body(PutBody::Path(path.into()))
123 }
124}
125
126#[derive(Debug)]
128pub struct PutBuilder {
129 pub(crate) session: Session,
130 pub(crate) metadata: Metadata,
131 pub(crate) compression: Option<CompressionMode>,
132 pub(crate) key: Option<ObjectKey>,
133 pub(crate) body: PutBody,
134}
135
136impl PutBuilder {
137 pub fn key(mut self, key: impl Into<ObjectKey>) -> Self {
142 self.key = Some(key.into()).filter(|k| !k.is_empty());
143 self
144 }
145
146 pub fn compress(mut self, compression: impl Into<Option<Compression>>) -> Self {
171 self.compression = compression.into().map(CompressionMode::Compress);
172 self
173 }
174
175 #[deprecated(since = "0.3.0", note = "renamed to `compress`")]
177 pub fn compression(self, compression: impl Into<Option<Compression>>) -> Self {
178 self.compress(compression)
179 }
180
181 pub fn precompressed(mut self, compression: Compression) -> Self {
203 self.compression = Some(CompressionMode::Precompressed(compression));
204 self
205 }
206
207 pub fn expiration_policy(mut self, expiration_policy: ExpirationPolicy) -> Self {
211 self.metadata.expiration_policy = expiration_policy;
212 self
213 }
214
215 pub fn content_type(mut self, content_type: impl Into<Cow<'static, str>>) -> Self {
220 self.metadata.content_type = content_type.into();
221 self
222 }
223
224 pub fn origin(mut self, origin: impl Into<String>) -> Self {
242 self.metadata.origin = Some(origin.into());
243 self
244 }
245
246 pub fn filename(mut self, filename: impl Into<String>) -> Self {
252 self.metadata.filename = Some(filename.into());
253 self
254 }
255
256 pub fn set_metadata(mut self, metadata: impl Into<BTreeMap<String, String>>) -> Self {
260 self.metadata.custom = metadata.into();
261 self
262 }
263
264 pub fn append_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
266 self.metadata.custom.insert(key.into(), value.into());
267 self
268 }
269}
270
271pub(crate) async fn encode_body(body: PutBody, mode: Option<CompressionMode>) -> io::Result<Body> {
275 let compression = match mode {
276 Some(CompressionMode::Compress(compression)) => Some(compression),
277 Some(CompressionMode::Precompressed(_)) | None => None,
279 };
280
281 Ok(match (compression, body) {
282 (Some(Compression::Zstd), PutBody::Buffer(bytes)) => {
283 let cursor = Cursor::new(bytes);
284 let encoder = ZstdEncoder::new(cursor);
285 let stream = ReaderStream::new(encoder);
286 Body::wrap_stream(stream)
287 }
288 (Some(Compression::Zstd), PutBody::Stream(stream)) => {
289 let stream = StreamReader::new(stream);
290 let encoder = ZstdEncoder::new(stream);
291 let stream = ReaderStream::new(encoder);
292 Body::wrap_stream(stream)
293 }
294 (Some(Compression::Zstd), PutBody::File(file)) => {
295 let reader = BufReader::new(file);
296 let encoder = ZstdEncoder::new(reader);
297 let stream = ReaderStream::new(encoder);
298 Body::wrap_stream(stream)
299 }
300 (Some(Compression::Zstd), PutBody::Path(file)) => {
301 let file = File::open(file).await?;
302 let reader = BufReader::new(file);
303 let encoder = ZstdEncoder::new(reader);
304 let stream = ReaderStream::new(encoder);
305 Body::wrap_stream(stream)
306 }
307 (None, PutBody::Buffer(bytes)) => bytes.into(),
308 (None, PutBody::Stream(stream)) => Body::wrap_stream(stream),
309 (None, PutBody::File(file)) => {
310 let stream = ReaderStream::new(file);
311 Body::wrap_stream(stream)
312 }
313 (None, PutBody::Path(path)) => {
314 let stream = ReaderStream::new(File::open(path).await?);
315 Body::wrap_stream(stream)
316 }
317 })
318}
319
320impl PutBuilder {
324 pub async fn send(mut self) -> crate::Result<PutResponse> {
326 let method = match self.key {
327 Some(_) => reqwest::Method::PUT,
328 None => reqwest::Method::POST,
329 };
330
331 let mut builder = self
332 .session
333 .request(method, self.key.as_deref().unwrap_or_default())?;
334
335 self.metadata.compression = self.compression.map(CompressionMode::compression);
336 let body = encode_body(self.body, self.compression).await?;
337
338 builder = builder.headers(self.metadata.to_headers("")?);
339
340 let response = builder.body(body).send().await?;
341 Ok(response.error_for_status()?.json().await?)
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use futures_util::stream;
348 use http_body_util::BodyExt as _;
349
350 use super::*;
351
352 fn zstd_compress(data: &[u8]) -> Vec<u8> {
353 zstd::encode_all(Cursor::new(data), 0).expect("zstd encoding to succeed")
354 }
355
356 fn stream_body(chunks: Vec<&'static [u8]>) -> PutBody {
357 let chunks = chunks.into_iter().map(|c| Ok(Bytes::from_static(c)));
358 PutBody::Stream(stream::iter(chunks).boxed())
359 }
360
361 async fn collect(body: Body) -> Vec<u8> {
362 body.collect()
363 .await
364 .expect("body to be readable")
365 .to_bytes()
366 .to_vec()
367 }
368
369 #[tokio::test]
370 async fn compress_buffer_compresses() {
371 let body = PutBody::Buffer(Bytes::from_static(b"hello world"));
372 let mode = Some(CompressionMode::Compress(Compression::Zstd));
373
374 let encoded = collect(encode_body(body, mode).await.unwrap()).await;
375 assert_eq!(encoded, zstd_compress(b"hello world"));
376 }
377
378 #[tokio::test]
379 async fn compress_stream_compresses() {
380 let body = stream_body(vec![b"hello ", b"world"]);
381 let mode = Some(CompressionMode::Compress(Compression::Zstd));
382
383 let encoded = collect(encode_body(body, mode).await.unwrap()).await;
384 assert_eq!(
385 zstd::decode_all(Cursor::new(encoded)).unwrap(),
386 b"hello world"
387 );
388 }
389
390 #[tokio::test]
391 async fn precompressed_buffer_is_forwarded_verbatim() {
392 let compressed = zstd_compress(b"hello world");
393 let body = PutBody::Buffer(Bytes::from(compressed.clone()));
394 let mode = Some(CompressionMode::Precompressed(Compression::Zstd));
395
396 let encoded = collect(encode_body(body, mode).await.unwrap()).await;
397 assert_eq!(encoded, compressed);
398 }
399
400 #[tokio::test]
401 async fn precompressed_stream_is_forwarded_verbatim() {
402 let body = stream_body(vec![b"\x28\xb5\x2f\xfd", b"trailing"]);
403 let mode = Some(CompressionMode::Precompressed(Compression::Zstd));
404
405 let encoded = collect(encode_body(body, mode).await.unwrap()).await;
406 assert_eq!(encoded, b"\x28\xb5\x2f\xfdtrailing");
407 }
408
409 #[tokio::test]
410 async fn without_compression_is_forwarded_verbatim() {
411 let body = PutBody::Buffer(Bytes::from_static(b"hello world"));
412
413 let encoded = collect(encode_body(body, None).await.unwrap()).await;
414 assert_eq!(encoded, b"hello world");
415 }
416}