Skip to main content

objectstore_client/
get.rs

1use std::{fmt, io};
2
3use async_compression::tokio::bufread::{GzipDecoder, ZstdDecoder};
4use bytes::BytesMut;
5use futures_util::{StreamExt, TryStreamExt};
6use objectstore_types::metadata::{Compression, Metadata};
7use reqwest::StatusCode;
8use tokio_util::io::{ReaderStream, StreamReader};
9
10use crate::response::ResponseExt as _;
11use crate::{ClientStream, ObjectKey, Session};
12
13/// The result from a successful [`get()`](Session::get) call.
14///
15/// This carries the response as a stream, plus the compression algorithm of the data.
16pub struct GetResponse {
17    /// The metadata attached to this object, including the compression algorithm used for the payload.
18    pub metadata: Metadata,
19    /// The response stream.
20    pub stream: ClientStream,
21}
22
23impl GetResponse {
24    /// Loads the object payload fully into memory.
25    pub async fn payload(self) -> crate::Result<bytes::Bytes> {
26        let bytes: BytesMut = self.stream.try_collect().await?;
27        Ok(bytes.freeze())
28    }
29
30    /// Loads the object payload fully into memory and interprets it as UTF-8 text.
31    pub async fn text(self) -> crate::Result<String> {
32        let bytes = self.payload().await?;
33        Ok(String::from_utf8(bytes.to_vec())?)
34    }
35}
36
37impl fmt::Debug for GetResponse {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        f.debug_struct("GetResponse")
40            .field("metadata", &self.metadata)
41            .field("stream", &format_args!("[Stream]"))
42            .finish()
43    }
44}
45
46impl Session {
47    /// Retrieves the object with the given `key`.
48    pub fn get(&self, key: &str) -> GetBuilder {
49        GetBuilder {
50            session: self.clone(),
51            key: key.to_owned(),
52            decompress: true,
53            accept_encoding: vec![],
54        }
55    }
56}
57
58/// A [`get`](Session::get) request builder.
59#[derive(Debug)]
60pub struct GetBuilder {
61    pub(crate) session: Session,
62    pub(crate) key: ObjectKey,
63    pub(crate) decompress: bool,
64    pub(crate) accept_encoding: Vec<Compression>,
65}
66
67impl GetBuilder {
68    /// Indicates whether the request should automatically handle decompression of known algorithms,
69    /// or rather return the payload as it is stored, along with the compression algorithm it is stored in.
70    ///
71    /// By default, automatic decompression is enabled.
72    pub fn decompress(mut self, decompress: bool) -> Self {
73        self.decompress = decompress;
74        self
75    }
76
77    /// Specifies compression encodings the caller can handle natively.
78    ///
79    /// When the stored object's compression matches one of these, the payload
80    /// is returned still compressed and `metadata.compression` is preserved.
81    /// An empty list (the default) means the client does not accept any
82    /// compressed encoding, so automatic decompression applies as usual.
83    pub fn accept_encoding(mut self, encodings: impl IntoIterator<Item = Compression>) -> Self {
84        self.accept_encoding = encodings.into_iter().collect();
85        self
86    }
87
88    /// Sends the get request.
89    pub async fn send(self) -> crate::Result<Option<GetResponse>> {
90        let response = self
91            .session
92            .request(reqwest::Method::GET, &self.key)?
93            .send()
94            .await?;
95        if response.status() == StatusCode::NOT_FOUND {
96            response.drain_body().await;
97            return Ok(None);
98        }
99        let response = response.error_for_status_and_drain().await?;
100
101        let mut metadata = match Metadata::from_headers(response.headers(), "") {
102            Ok(metadata) => metadata,
103            Err(error) => {
104                response.drain_body().await;
105                return Err(error.into());
106            }
107        };
108
109        let stream = response.bytes_stream().map_err(io::Error::other).boxed();
110        let stream = maybe_decompress(
111            stream,
112            &mut metadata,
113            self.decompress,
114            &self.accept_encoding,
115        );
116
117        Ok(Some(GetResponse { metadata, stream }))
118    }
119}
120
121/// Wraps a stream in a decompression layer.
122///
123/// Decompresses if the metadata indicates compression, `decompress` is `true`,
124/// and the stored encoding is not listed in `accept_encoding`. When the stored encoding
125/// is in `accept_encoding`, the payload is returned compressed and `metadata.compression`
126/// is preserved. Clears `metadata.compression` when decompression is applied.
127pub(crate) fn maybe_decompress(
128    stream: ClientStream,
129    metadata: &mut Metadata,
130    decompress: bool,
131    accept_encoding: &[Compression],
132) -> ClientStream {
133    let encoding_accepted = metadata
134        .compression
135        .is_some_and(|c| accept_encoding.contains(&c));
136    match (metadata.compression, decompress && !encoding_accepted) {
137        (Some(Compression::Zstd), true) => {
138            metadata.compression = None;
139            let mut decoder = ZstdDecoder::new(StreamReader::new(stream));
140            // Zstd streams may contain concatenated frames (RFC 8878 §3.1).
141            decoder.multiple_members(true);
142            ReaderStream::new(decoder).boxed()
143        }
144        (Some(Compression::Gzip), true) => {
145            metadata.compression = None;
146            let mut decoder = GzipDecoder::new(StreamReader::new(stream));
147            // Gzip streams may contain concatenated members (RFC 1952 §2.2).
148            decoder.multiple_members(true);
149            ReaderStream::new(decoder).boxed()
150        }
151        (Some(_), false) | (None, _) => stream,
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use futures_util::{StreamExt as _, TryStreamExt as _};
158    use objectstore_types::metadata::{Compression, Metadata};
159
160    use super::maybe_decompress;
161    use crate::ClientStream;
162
163    fn compressed_zstd_stream(data: &[u8]) -> ClientStream {
164        let mut encoder = zstd::Encoder::new(vec![], 0).unwrap();
165        std::io::copy(&mut std::io::Cursor::new(data), &mut encoder).unwrap();
166        let compressed = encoder.finish().unwrap();
167        futures_util::stream::once(async move {
168            Ok::<_, std::io::Error>(bytes::Bytes::from(compressed))
169        })
170        .boxed()
171    }
172
173    fn raw_stream(data: &[u8]) -> ClientStream {
174        let bytes = bytes::Bytes::copy_from_slice(data);
175        futures_util::stream::once(async move { Ok::<_, std::io::Error>(bytes) }).boxed()
176    }
177
178    async fn collect(stream: ClientStream) -> Vec<u8> {
179        let chunks: bytes::BytesMut = stream.try_collect().await.unwrap();
180        chunks.to_vec()
181    }
182
183    fn zstd_metadata() -> Metadata {
184        Metadata {
185            compression: Some(Compression::Zstd),
186            ..Default::default()
187        }
188    }
189
190    fn no_compression_metadata() -> Metadata {
191        Metadata::default()
192    }
193
194    #[tokio::test]
195    async fn empty_accept_decompress_true_decompresses() {
196        let payload = b"hello world";
197        let stream = compressed_zstd_stream(payload);
198        let mut metadata = zstd_metadata();
199
200        let out = maybe_decompress(stream, &mut metadata, true, &[]);
201        assert_eq!(collect(out).await, payload);
202        assert_eq!(metadata.compression, None);
203    }
204
205    #[tokio::test]
206    async fn empty_accept_decompress_false_returns_compressed() {
207        let payload = b"hello world";
208        let compressed_bytes = collect(compressed_zstd_stream(payload)).await;
209        let stream = compressed_zstd_stream(payload);
210
211        let mut metadata = zstd_metadata();
212        let out = maybe_decompress(stream, &mut metadata, false, &[]);
213        assert_eq!(collect(out).await, compressed_bytes);
214        assert_eq!(metadata.compression, Some(Compression::Zstd));
215    }
216
217    #[tokio::test]
218    async fn zstd_accept_decompress_true_skips_decompression() {
219        let payload = b"hello world";
220        let compressed_bytes = collect(compressed_zstd_stream(payload)).await;
221        let stream = compressed_zstd_stream(payload);
222
223        let mut metadata = zstd_metadata();
224        let out = maybe_decompress(stream, &mut metadata, true, &[Compression::Zstd]);
225        assert_eq!(collect(out).await, compressed_bytes);
226        assert_eq!(metadata.compression, Some(Compression::Zstd));
227    }
228
229    #[tokio::test]
230    async fn zstd_accept_decompress_false_returns_compressed() {
231        let payload = b"hello world";
232        let compressed_bytes = collect(compressed_zstd_stream(payload)).await;
233        let stream = compressed_zstd_stream(payload);
234
235        let mut metadata = zstd_metadata();
236        let out = maybe_decompress(stream, &mut metadata, false, &[Compression::Zstd]);
237        assert_eq!(collect(out).await, compressed_bytes);
238        assert_eq!(metadata.compression, Some(Compression::Zstd));
239    }
240
241    #[tokio::test]
242    async fn no_compression_returns_raw_regardless_of_accept() {
243        let payload = b"hello world";
244        let stream = raw_stream(payload);
245
246        let mut metadata = no_compression_metadata();
247        let out = maybe_decompress(stream, &mut metadata, true, &[Compression::Zstd]);
248        assert_eq!(collect(out).await, payload);
249        assert_eq!(metadata.compression, None);
250    }
251
252    fn gzip_compress(data: &[u8]) -> Vec<u8> {
253        use std::io::Write as _;
254        let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
255        encoder.write_all(data).unwrap();
256        encoder.finish().unwrap()
257    }
258
259    fn gzip_metadata() -> Metadata {
260        Metadata {
261            compression: Some(Compression::Gzip),
262            ..Default::default()
263        }
264    }
265
266    #[tokio::test]
267    async fn gzip_decompression() {
268        let payload = b"hello world";
269        let stream = raw_stream(&gzip_compress(payload));
270        let mut metadata = gzip_metadata();
271
272        let out = maybe_decompress(stream, &mut metadata, true, &[]);
273        assert_eq!(collect(out).await, payload);
274        assert_eq!(metadata.compression, None);
275    }
276
277    #[tokio::test]
278    async fn gzip_accept_encoding() {
279        let compressed = gzip_compress(b"hello world");
280        let stream = raw_stream(&compressed);
281        let mut metadata = gzip_metadata();
282
283        let out = maybe_decompress(stream, &mut metadata, true, &[Compression::Gzip]);
284        assert_eq!(collect(out).await, compressed);
285        assert_eq!(metadata.compression, Some(Compression::Gzip));
286    }
287
288    #[tokio::test]
289    async fn gzip_concatenated_members() {
290        let mut compressed = gzip_compress(b"hello ");
291        compressed.extend(gzip_compress(b"world"));
292        let stream = raw_stream(&compressed);
293        let mut metadata = gzip_metadata();
294
295        let out = maybe_decompress(stream, &mut metadata, true, &[]);
296        assert_eq!(collect(out).await, b"hello world");
297        assert_eq!(metadata.compression, None);
298    }
299
300    #[tokio::test]
301    async fn zstd_concatenated_frames_decompress() {
302        let payload1 = b"hello ";
303        let payload2 = b"world";
304        let compressed1 = collect(compressed_zstd_stream(payload1)).await;
305        let compressed2 = collect(compressed_zstd_stream(payload2)).await;
306        let stream = futures_util::stream::iter([
307            Ok::<_, std::io::Error>(bytes::Bytes::from(compressed1)),
308            Ok::<_, std::io::Error>(bytes::Bytes::from(compressed2)),
309        ])
310        .boxed();
311
312        let mut metadata = zstd_metadata();
313        let out = maybe_decompress(stream, &mut metadata, true, &[]);
314        assert_eq!(collect(out).await, b"hello world");
315        assert_eq!(metadata.compression, None);
316    }
317}