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
13pub struct GetResponse {
17 pub metadata: Metadata,
19 pub stream: ClientStream,
21}
22
23impl GetResponse {
24 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 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 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#[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 pub fn decompress(mut self, decompress: bool) -> Self {
73 self.decompress = decompress;
74 self
75 }
76
77 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 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
121pub(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 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 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}