Skip to main content

loonfs_client/
payload.rs

1//! Where a streaming put reads its bytes, and how it cuts them into parts.
2//!
3//! A [`PayloadSource`] is read exactly once, forward, in pieces. That is the
4//! whole contract: it is what lets one put serve a file on disk and a pipe
5//! with no length equally well, and what keeps the memory a large put costs
6//! independent of how large it is.
7
8use bytes::{Bytes, BytesMut};
9use futures::stream::{BoxStream, StreamExt};
10use std::io;
11use std::path::Path;
12use tokio::io::{AsyncRead, AsyncReadExt};
13
14/// Bytes read from a source in one go before they are handed on.
15///
16/// Sized like an HTTP body's chunk rather than a transfer part: this is the
17/// slack a bounded uploader carries on top of the parts it holds, so it
18/// should stay small next to a part.
19const SOURCE_CHUNK_BYTES: usize = 64 * 1024;
20
21/// A payload delivered in pieces, for a put that must not hold it whole.
22///
23/// Chunk boundaries carry no meaning — an uploader regroups them into
24/// whatever units its transport wants. A chunk error ends the upload.
25pub type PayloadStream = BoxStream<'static, io::Result<Bytes>>;
26
27/// Where one put reads its payload, and what it knows about it up front.
28///
29/// The length is a hint and never a promise: a source that knows it lets the
30/// put pick the cheaper transport and declare a `Content-Length`, and a
31/// source that does not know it — a pipe, a socket, standard input — takes
32/// exactly the same path and discovers the length as it goes.
33pub struct PayloadSource {
34    stream: PayloadStream,
35    size_bytes: Option<u64>,
36}
37
38impl std::fmt::Debug for PayloadSource {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.debug_struct("PayloadSource")
41            .field("size_bytes", &self.size_bytes)
42            .finish_non_exhaustive()
43    }
44}
45
46impl PayloadSource {
47    /// A source of unknown length.
48    pub fn stream(stream: PayloadStream) -> Self {
49        Self {
50            stream,
51            size_bytes: None,
52        }
53    }
54
55    /// A source whose complete length the caller already knows.
56    ///
57    /// The length is used to choose a transport and to frame the request; a
58    /// source that delivers a different number of bytes is still uploaded
59    /// faithfully, and it is the bytes that decide what is stored.
60    pub fn sized_stream(stream: PayloadStream, size_bytes: u64) -> Self {
61        Self {
62            stream,
63            size_bytes: Some(size_bytes),
64        }
65    }
66
67    /// A source of unknown length reading from anything asynchronous —
68    /// standard input, a socket, a decompressor.
69    pub fn reader<R>(reader: R) -> Self
70    where
71        R: AsyncRead + Send + Unpin + 'static,
72    {
73        Self::stream(read_in_chunks(reader))
74    }
75
76    /// A source reading one local file, whose length is known before the
77    /// first byte moves.
78    pub async fn open_file(path: impl AsRef<Path>) -> io::Result<Self> {
79        let file = tokio::fs::File::open(path.as_ref()).await?;
80        let size_bytes = file.metadata().await?.len();
81        Ok(Self::sized_stream(read_in_chunks(file), size_bytes))
82    }
83
84    /// Complete length when the source knows it.
85    pub fn size_bytes(&self) -> Option<u64> {
86        self.size_bytes
87    }
88
89    /// Splits the source into the stream to read and the length it
90    /// declared, for a caller that has to restate it in another crate's
91    /// terms.
92    pub fn into_stream(self) -> (PayloadStream, Option<u64>) {
93        (self.stream, self.size_bytes)
94    }
95}
96
97/// Reads an asynchronous source into modest chunks until it ends.
98fn read_in_chunks<R>(reader: R) -> PayloadStream
99where
100    R: AsyncRead + Send + Unpin + 'static,
101{
102    futures::stream::unfold(Some(reader), |reader| async move {
103        let mut reader = reader?;
104        let mut buffer = BytesMut::with_capacity(SOURCE_CHUNK_BYTES);
105        match reader.read_buf(&mut buffer).await {
106            // A read of zero is the end of the source, and the only thing
107            // that ends it: a short read is just a short read.
108            Ok(0) => None,
109            Ok(_) => Some((Ok(buffer.freeze()), Some(reader))),
110            Err(error) => Some((Err(error), None)),
111        }
112    })
113    .boxed()
114}
115
116/// Cuts a source into fixed-size parts, holding one at a time.
117///
118/// Chunk boundaries in the source carry no meaning, so a chunk that overruns
119/// the part being cut is split and its tail carried into the next one. The
120/// last part is whatever is left when the source ends, and a source that
121/// ends exactly on a boundary produces no final part.
122pub(crate) struct PartReader {
123    stream: PayloadStream,
124    /// The tail of a chunk that overran the part being cut.
125    carry: Option<Bytes>,
126    part_bytes: usize,
127    exhausted: bool,
128}
129
130impl PartReader {
131    pub(crate) fn new(stream: PayloadStream, part_bytes: usize) -> Self {
132        Self {
133            stream,
134            carry: None,
135            part_bytes: part_bytes.max(1),
136            exhausted: false,
137        }
138    }
139
140    /// Cuts the next part: exactly `part_bytes`, or whatever is left when
141    /// the source ends. `None` once nothing is left.
142    ///
143    /// A full part is returned without reading further, so a caller cannot
144    /// conclude from a full part that more is coming — only a short part
145    /// proves the source ended.
146    pub(crate) async fn next_part(&mut self) -> io::Result<Option<Bytes>> {
147        let mut buffer: Option<BytesMut> = None;
148        loop {
149            let filled = buffer.as_ref().map_or(0, BytesMut::len);
150            if filled >= self.part_bytes {
151                break;
152            }
153            let mut chunk = match self.carry.take() {
154                Some(chunk) => chunk,
155                None if self.exhausted => break,
156                None => match self.stream.next().await {
157                    Some(chunk) => chunk?,
158                    None => {
159                        self.exhausted = true;
160                        break;
161                    }
162                },
163            };
164            let take = (self.part_bytes - filled).min(chunk.len());
165            let taken = chunk.split_to(take);
166            if !chunk.is_empty() {
167                self.carry = Some(chunk);
168            }
169            match &mut buffer {
170                Some(buffer) => buffer.extend_from_slice(&taken),
171                // A chunk that fills a part on its own is handed straight
172                // through. Nothing is copied, and the part is a view of the
173                // source's own buffer rather than a second copy of it.
174                None if taken.len() == self.part_bytes => return Ok(Some(taken)),
175                None => {
176                    let mut fresh = BytesMut::with_capacity(self.part_bytes);
177                    fresh.extend_from_slice(&taken);
178                    buffer = Some(fresh);
179                }
180            }
181        }
182        Ok(buffer
183            .filter(|buffer| !buffer.is_empty())
184            .map(BytesMut::freeze))
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    fn source(chunks: Vec<&'static [u8]>) -> PayloadStream {
193        futures::stream::iter(chunks.into_iter().map(|chunk| Ok(Bytes::from(chunk)))).boxed()
194    }
195
196    async fn cut(reader: &mut PartReader) -> Vec<Vec<u8>> {
197        let mut parts = Vec::new();
198        while let Some(part) = reader.next_part().await.expect("cut a part") {
199            parts.push(part.to_vec());
200        }
201        parts
202    }
203
204    /// Source chunks and part boundaries are unrelated: a chunk that
205    /// straddles a boundary is split, and its tail opens the next part.
206    #[tokio::test]
207    async fn parts_are_cut_regardless_of_how_the_source_chunked_them() {
208        let mut reader = PartReader::new(source(vec![b"abcde", b"fg", b"hijkl"]), 4);
209        assert_eq!(
210            cut(&mut reader).await,
211            vec![b"abcd".to_vec(), b"efgh".to_vec(), b"ijkl".to_vec()]
212        );
213    }
214
215    /// A source that ends exactly on a boundary produces no trailing empty
216    /// part, and one that does not ends with a short one.
217    #[tokio::test]
218    async fn the_last_part_is_whatever_is_left() {
219        let mut reader = PartReader::new(source(vec![b"abcdef"]), 4);
220        assert_eq!(
221            cut(&mut reader).await,
222            vec![b"abcd".to_vec(), b"ef".to_vec()]
223        );
224
225        let mut reader = PartReader::new(source(vec![b"abcd"]), 4);
226        assert_eq!(cut(&mut reader).await, vec![b"abcd".to_vec()]);
227    }
228
229    /// An empty source produces no parts at all, which is what tells a
230    /// one-pass uploader that it has nothing to assemble.
231    #[tokio::test]
232    async fn an_empty_source_produces_no_parts() {
233        let mut reader = PartReader::new(source(vec![]), 4);
234        assert!(reader.next_part().await.expect("cut a part").is_none());
235    }
236
237    #[tokio::test]
238    async fn a_file_source_knows_its_length() {
239        let directory = tempfile::tempdir().expect("tempdir");
240        let path = directory.path().join("payload.bin");
241        std::fs::write(&path, vec![7u8; 5_000]).expect("write payload");
242
243        let source = PayloadSource::open_file(&path).await.expect("open payload");
244        assert_eq!(source.size_bytes(), Some(5_000));
245
246        let (stream, _) = source.into_stream();
247        let mut reader = PartReader::new(stream, 4_096);
248        let first = reader.next_part().await.expect("cut").expect("first part");
249        let second = reader.next_part().await.expect("cut").expect("second part");
250        assert_eq!(first.len(), 4_096);
251        assert_eq!(second.len(), 904);
252        assert!(reader.next_part().await.expect("cut").is_none());
253    }
254
255    /// A reader-backed source declares no length, which is exactly the
256    /// case a pipe presents.
257    #[tokio::test]
258    async fn a_reader_source_declares_no_length() {
259        let source = PayloadSource::reader(std::io::Cursor::new(vec![1u8; 100]));
260        assert_eq!(source.size_bytes(), None);
261        let (stream, _) = source.into_stream();
262        let mut reader = PartReader::new(stream, 64);
263        assert_eq!(
264            reader.next_part().await.expect("cut").expect("part").len(),
265            64
266        );
267        assert_eq!(
268            reader.next_part().await.expect("cut").expect("part").len(),
269            36
270        );
271        assert!(reader.next_part().await.expect("cut").is_none());
272    }
273}