Skip to main content

timvw_rust_runtime/
upload.rs

1use std::borrow::Cow;
2use std::fmt;
3use std::ops::{Bound, RangeBounds};
4use std::sync::{Arc, Mutex, PoisonError};
5
6use bytes::{Bytes, BytesMut};
7use futures_core::Stream;
8use futures_util::{future, stream, StreamExt, TryStreamExt};
9
10use crate::maybe::BoxStream;
11use crate::{Body, BoxError, Error, MaybeSend, MaybeSync};
12
13type Chunks = BoxStream<'static, Result<Bytes, BoxError>>;
14type Once = Arc<Mutex<Option<Chunks>>>;
15#[cfg(not(target_family = "wasm"))]
16type Factory = Arc<dyn Fn() -> Chunks + Send + Sync>;
17#[cfg(target_family = "wasm")]
18type Factory = Arc<dyn Fn() -> Chunks>;
19
20/// Something to upload: a file, bytes, a reader or a stream, with a file name and a media type.
21///
22/// An `Upload` becomes a request body (`RequestBuilder::body(upload)`) or a multipart part
23/// ([`Part::upload`](crate::multipart::Part::upload)). Generated clients take one for every file parameter, and a
24/// `PathBuf`, `&Path`, `Vec<u8>` or `Bytes` converts into one.
25///
26/// | Source | Length | Retried |
27/// |---|---|---|
28/// | [`Upload::bytes`] (`Bytes`, `Vec<u8>`, `&'static [u8]`) | known | yes, without a copy |
29/// | [`Upload::path`], [`Upload::file`] | from the file's metadata | yes: the file is opened again |
30/// | [`Upload::from_fn`] | [`Upload::len`] | yes: the function makes a fresh stream |
31/// | [`Upload::reader`], [`Upload::stream`] | [`Upload::len`] | no, unless [`Upload::buffer_for_retry`] holds it all |
32///
33/// When the length is known, the body (or the whole multipart form) is sent with a `Content-Length`, not chunked.
34/// A path's length is read by [`Upload::resolve`], which generated clients call; [`Upload::file`] is
35/// `Upload::path(path).resolve()`.
36///
37/// ```no_run
38/// # #[cfg(all(feature = "fs", not(target_family = "wasm")))]
39/// # async fn demo() -> Result<(), timvw_rust_runtime::Error> {
40/// use timvw_rust_runtime::Upload;
41///
42/// // Streamed from disk, named `photo.png`, `image/png` guessed from the extension.
43/// let file = Upload::file("data/photo.png").await?;
44/// // Bytes held in memory, with a name and a media type of your own.
45/// let image = Upload::bytes(vec![0x89, b'P', b'N', b'G']).file_name("cat.png").mime("image/png");
46/// // The second 5 MiB of a file, for an API that takes a large file in parts.
47/// let part = Upload::path("data/big.bin").slice(5 << 20..10 << 20).resolve().await?;
48/// # Ok(())
49/// # }
50/// ```
51#[derive(Clone)]
52pub struct Upload {
53    source: Source,
54    pub(crate) file_name: Option<Cow<'static, str>>,
55    pub(crate) mime: Option<Cow<'static, str>>,
56    /// The length of the whole source, before `start`/`end`, when known.
57    total: Option<u64>,
58    start: u64,
59    end: Option<u64>,
60    buffer_limit: Option<usize>,
61}
62
63#[derive(Clone)]
64enum Source {
65    Bytes(Bytes),
66    #[cfg(feature = "fs")]
67    File(Arc<std::path::Path>),
68    Once(Once),
69    Factory(Factory),
70}
71
72impl Upload {
73    /// Bytes held in memory: their length is known, and a retry sends them again without a copy.
74    pub fn bytes(bytes: impl Into<Bytes>) -> Self {
75        let bytes = bytes.into();
76        let total = Some(bytes.len() as u64);
77        Self::new(Source::Bytes(bytes), total)
78    }
79
80    /// A stream of chunks, read once: a retry cannot send it again unless [`Upload::buffer_for_retry`] holds it.
81    /// Its length is unknown (the body is chunked) unless set with [`Upload::len`]. The stream must be `Send`, except
82    /// on WebAssembly ([`MaybeSend`]).
83    pub fn stream<S, B, E>(stream: S) -> Self
84    where
85        S: Stream<Item = Result<B, E>> + MaybeSend + 'static,
86        B: Into<Bytes> + 'static,
87        E: Into<BoxError> + 'static,
88    {
89        let chunks: Chunks = Box::pin(stream.map_ok(Into::into).map_err(Into::into));
90        Self::new(Source::Once(Arc::new(Mutex::new(Some(chunks)))), None)
91    }
92
93    /// A stream that `make` starts afresh for every attempt, so a retry sends it again. Its length is unknown (the
94    /// body is chunked) unless set with [`Upload::len`]; every stream `make` returns must then have that length.
95    /// `make` must be `Send` and `Sync`, and its streams `Send`, except on WebAssembly ([`MaybeSend`], [`MaybeSync`]).
96    pub fn from_fn<F, S, B, E>(make: F) -> Self
97    where
98        F: Fn() -> S + MaybeSend + MaybeSync + 'static,
99        S: Stream<Item = Result<B, E>> + MaybeSend + 'static,
100        B: Into<Bytes> + 'static,
101        E: Into<BoxError> + 'static,
102    {
103        let factory: Factory =
104            Arc::new(move || Box::pin(make().map_ok(Into::into).map_err(Into::into)));
105        Self::new(Source::Factory(factory), None)
106    }
107
108    fn new(source: Source, total: Option<u64>) -> Self {
109        Self {
110            source,
111            file_name: None,
112            mime: None,
113            total,
114            start: 0,
115            end: None,
116            buffer_limit: None,
117        }
118    }
119
120    /// Sets the file name (multipart `filename`). A path's is its last component; a multipart part of an upload
121    /// without one is named `file`.
122    pub fn file_name(mut self, file_name: impl Into<Cow<'static, str>>) -> Self {
123        self.file_name = Some(file_name.into());
124        self
125    }
126
127    /// Sets the media type (the `Content-Type` of a multipart part), such as `image/png`. A path's is guessed from its
128    /// extension. A value that is not a valid header value is sent as `application/octet-stream`.
129    pub fn mime(mut self, mime: impl Into<Cow<'static, str>>) -> Self {
130        self.mime = Some(mime.into());
131        self
132    }
133
134    /// The file name: the one set with [`Upload::file_name`], or a path's last component. `None` for bytes, readers
135    /// and streams without one.
136    pub fn name(&self) -> Option<&str> {
137        self.file_name.as_deref()
138    }
139
140    /// The media type: the one set with [`Upload::mime`], or guessed from a path's extension.
141    pub fn media_type(&self) -> Option<&str> {
142        self.mime.as_deref()
143    }
144
145    /// Sets the length of the source in bytes, for a stream or a reader: the body is then sent with a
146    /// `Content-Length`, and the stream must have exactly that many bytes.
147    pub fn len(mut self, len: u64) -> Self {
148        self.total = Some(len);
149        self
150    }
151
152    /// Only the bytes in `range` of the source (of the current slice, when sliced again): a file is read from the
153    /// start of the range (a seek, no copy), bytes are sliced without a copy, a stream skips to it. For APIs that take
154    /// a large file in parts.
155    pub fn slice(mut self, range: impl RangeBounds<u64>) -> Self {
156        let from = match range.start_bound() {
157            Bound::Included(&start) => start,
158            Bound::Excluded(&start) => start.saturating_add(1),
159            Bound::Unbounded => 0,
160        };
161        let to = match range.end_bound() {
162            Bound::Included(&end) => Some(end.saturating_add(1)),
163            Bound::Excluded(&end) => Some(end),
164            Bound::Unbounded => None,
165        };
166        let start = self.start.saturating_add(from);
167        let end = to.map(|to| self.start.saturating_add(to));
168        self.end = match (self.end, end) {
169            (Some(old), Some(new)) => Some(old.min(new)),
170            (old, new) => old.or(new),
171        };
172        self.start = match self.end {
173            Some(end) => start.min(end),
174            None => start,
175        };
176        self
177    }
178
179    /// Lets a retry send a stream or a reader again: [`Upload::resolve`] reads it into memory, up to `limit` bytes.
180    /// A stream that ends within the limit becomes bytes (replayable, with a known length); a longer one is sent once,
181    /// as if this were not set. Without `resolve` (which generated clients call) this has no effect.
182    pub fn buffer_for_retry(mut self, limit: usize) -> Self {
183        self.buffer_limit = Some(limit);
184        self
185    }
186
187    /// The number of bytes the upload sends, when known: bytes, a resolved path, or a stream given a
188    /// [`len`](Upload::len).
189    pub fn content_length(&self) -> Option<u64> {
190        self.total.map(|total| {
191            let end = self.end.unwrap_or(total).min(total);
192            end.saturating_sub(self.start.min(total))
193        })
194    }
195
196    /// Whether a retry can send the upload again: bytes, a path and [`Upload::from_fn`] can, a stream or a reader
197    /// cannot (unless [`Upload::buffer_for_retry`] held it all when it was resolved).
198    pub fn is_replayable(&self) -> bool {
199        !matches!(self.source, Source::Once(_))
200    }
201
202    /// Gets the upload ready to send: reads a path's length from its metadata (an error when the file does not
203    /// exist), and reads a stream with [`Upload::buffer_for_retry`] into memory. Other uploads are returned as they
204    /// are.
205    pub async fn resolve(mut self) -> Result<Self, Error> {
206        #[cfg(all(feature = "fs", target_family = "wasm"))]
207        if let Source::File(path) = &self.source {
208            return Err(Error::io(no_file_system(path)));
209        }
210        #[cfg(all(feature = "fs", not(target_family = "wasm")))]
211        if let Source::File(path) = &self.source {
212            if self.total.is_none() {
213                let metadata = tokio::fs::metadata(&**path)
214                    .await
215                    .map_err(|error| Error::io(with_path(error, path)))?;
216                if !metadata.is_file() {
217                    return Err(Error::io(std::io::Error::other(format!(
218                        "{}: not a file",
219                        path.display()
220                    ))));
221                }
222                self.total = Some(metadata.len());
223            }
224            return Ok(self);
225        }
226        let Some(limit) = self.buffer_limit else {
227            return Ok(self);
228        };
229        let Source::Once(once) = &self.source else {
230            return Ok(self);
231        };
232        let taken = once.lock().unwrap_or_else(PoisonError::into_inner).take();
233        let Some(mut chunks) = taken else {
234            return Ok(self);
235        };
236        let mut buffer = BytesMut::new();
237        while let Some(chunk) = chunks.next().await {
238            let chunk = chunk.map_err(Error::io)?;
239            if buffer.len() + chunk.len() > limit {
240                // Too long to hold: sent once, starting with what was read.
241                let read = stream::iter([Ok(buffer.freeze()), Ok(chunk)]).chain(chunks);
242                let chunks: Chunks = Box::pin(read);
243                self.source = Source::Once(Arc::new(Mutex::new(Some(chunks))));
244                return Ok(self);
245            }
246            buffer.extend_from_slice(&chunk);
247        }
248        self.total = Some(buffer.len() as u64);
249        self.source = Source::Bytes(buffer.freeze());
250        Ok(self)
251    }
252}
253
254#[cfg(feature = "fs")]
255impl Upload {
256    /// A file, streamed from disk with tokio and opened again for each attempt, so a retry sends it again.
257    ///
258    /// The file name is the path's last component, the media type is guessed from its extension. Nothing is read
259    /// until the request is sent; [`Upload::resolve`] (or [`Upload::file`]) reads the length, so the body is sent with
260    /// a `Content-Length`, and fails early when the file does not exist.
261    ///
262    /// On WebAssembly there is no file system: the upload fails ([`Upload::resolve`], or the request) with an I/O
263    /// error of kind `Unsupported`. The function is there so that code taking a `PathBuf` (a generated model with a
264    /// file variant) compiles; send bytes or a stream instead.
265    pub fn path(path: impl Into<std::path::PathBuf>) -> Self {
266        let path: Arc<std::path::Path> = Arc::from(path.into());
267        let file_name = path
268            .file_name()
269            .map(|name| Cow::Owned(name.to_string_lossy().into_owned()));
270        let mime = mime_guess::from_path(&path)
271            .first()
272            .map(|mime| Cow::Owned(mime.essence_str().to_owned()));
273        let mut upload = Self::new(Source::File(path), None);
274        upload.file_name = file_name;
275        upload.mime = mime;
276        upload
277    }
278}
279
280#[cfg(all(feature = "fs", not(target_family = "wasm")))]
281impl Upload {
282    /// [`Upload::path`], resolved: its length read from the file's metadata, an error when there is no such file.
283    pub async fn file(path: impl Into<std::path::PathBuf>) -> Result<Self, Error> {
284        Self::path(path).resolve().await
285    }
286
287    /// A tokio reader, read once in chunks: a retry cannot send it again unless [`Upload::buffer_for_retry`] holds it.
288    /// Its length is unknown (the body is chunked) unless set with [`Upload::len`].
289    pub fn reader<R>(reader: R) -> Self
290    where
291        R: tokio::io::AsyncRead + Send + 'static,
292    {
293        Self::stream(read_chunks(Box::pin(reader), None))
294    }
295}
296
297/// The chunks of a reader, up to its end; fewer bytes than `expected` is an error.
298#[cfg(all(feature = "fs", not(target_family = "wasm")))]
299fn read_chunks<R>(
300    reader: R,
301    expected: Option<u64>,
302) -> impl Stream<Item = std::io::Result<Bytes>> + Send + 'static
303where
304    R: tokio::io::AsyncRead + Send + Unpin + 'static,
305{
306    use tokio::io::AsyncReadExt;
307    const CHUNK: usize = 64 * 1024;
308    stream::try_unfold((reader, 0u64), move |(mut reader, read)| async move {
309        let mut buffer = BytesMut::with_capacity(CHUNK);
310        let n = reader.read_buf(&mut buffer).await?;
311        if n == 0 {
312            return match expected {
313                Some(expected) if read < expected => Err(std::io::Error::new(
314                    std::io::ErrorKind::UnexpectedEof,
315                    format!("the upload ended after {read} of its {expected} bytes"),
316                )),
317                _ => Ok(None),
318            };
319        }
320        Ok(Some((buffer.freeze(), (reader, read + n as u64))))
321    })
322}
323
324/// The error of a file upload on WebAssembly, where there is no file system.
325#[cfg(all(feature = "fs", target_family = "wasm"))]
326fn no_file_system(path: &std::path::Path) -> std::io::Error {
327    std::io::Error::new(
328        std::io::ErrorKind::Unsupported,
329        format!(
330            "{}: no file system on WebAssembly: upload bytes or a stream instead",
331            path.display()
332        ),
333    )
334}
335
336#[cfg(all(feature = "fs", not(target_family = "wasm")))]
337fn with_path(error: std::io::Error, path: &std::path::Path) -> std::io::Error {
338    std::io::Error::new(error.kind(), format!("{}: {error}", path.display()))
339}
340
341/// The bytes of a file from `start` to `end`, `len` of them when known.
342#[cfg(all(feature = "fs", not(target_family = "wasm")))]
343fn file_chunks(
344    path: Arc<std::path::Path>,
345    start: u64,
346    end: Option<u64>,
347    len: Option<u64>,
348) -> Chunks {
349    use tokio::io::{AsyncReadExt, AsyncSeekExt};
350    let open = async move {
351        let mut file = tokio::fs::File::open(&*path)
352            .await
353            .map_err(|error| with_path(error, &path))?;
354        if start > 0 {
355            file.seek(std::io::SeekFrom::Start(start)).await?;
356        }
357        let limit = len
358            .or_else(|| end.map(|end| end.saturating_sub(start)))
359            .unwrap_or(u64::MAX);
360        Ok::<_, std::io::Error>(read_chunks(file.take(limit), len))
361    };
362    Box::pin(stream::once(open).try_flatten().map_err(BoxError::from))
363}
364
365/// The bytes of a stream from `start` to `end`.
366fn range(chunks: Chunks, start: u64, end: Option<u64>) -> Chunks {
367    if start == 0 && end.is_none() {
368        return chunks;
369    }
370    Box::pin(stream::unfold(
371        (chunks, 0u64),
372        move |(mut chunks, mut position)| async move {
373            loop {
374                if end.is_some_and(|end| position >= end) {
375                    return None;
376                }
377                let chunk = match chunks.next().await? {
378                    Ok(chunk) => chunk,
379                    Err(error) => return Some((Err(error), (chunks, position))),
380                };
381                let first = position;
382                position += chunk.len() as u64;
383                let from = start.max(first);
384                let to = end.map_or(position, |end| end.min(position));
385                if from < to {
386                    let slice = chunk.slice((from - first) as usize..(to - first) as usize);
387                    return Some((Ok(slice), (chunks, position)));
388                }
389            }
390        },
391    ))
392}
393
394/// The stream of a one-shot source, taken when it is first read; an error when it was taken before.
395fn take_once(once: Once) -> Chunks {
396    let taken = stream::once(future::lazy(move |_| {
397        once.lock().unwrap_or_else(PoisonError::into_inner).take()
398    }));
399    Box::pin(taken.flat_map(|chunks| match chunks {
400        Some(chunks) => chunks,
401        None => Box::pin(stream::once(future::ready(Err(BoxError::from(
402            "this upload is a stream that was sent already: a stream is read once",
403        ))))) as Chunks,
404    }))
405}
406
407impl From<Upload> for Body {
408    fn from(upload: Upload) -> Self {
409        let len = upload.content_length();
410        let (start, end) = (upload.start, upload.end);
411        match upload.source {
412            Source::Bytes(bytes) => {
413                let total = bytes.len() as u64;
414                let to = end.unwrap_or(total).min(total);
415                let from = start.min(to);
416                Body::from(bytes.slice(from as usize..to as usize))
417            }
418            #[cfg(all(feature = "fs", target_family = "wasm"))]
419            Source::File(path) => Body::wrap_stream(stream::once(future::ready(Err::<Bytes, _>(
420                no_file_system(&path),
421            )))),
422            #[cfg(all(feature = "fs", not(target_family = "wasm")))]
423            Source::File(path) => Body::replay(
424                Arc::new(move || {
425                    Body::sized_stream(file_chunks(path.clone(), start, end, len), len)
426                }),
427                len,
428            ),
429            Source::Factory(make) => Body::replay(
430                Arc::new(move || Body::sized_stream(range(make(), start, end), len)),
431                len,
432            ),
433            Source::Once(once) => Body::sized_stream(range(take_once(once), start, end), len),
434        }
435    }
436}
437
438impl From<Bytes> for Upload {
439    fn from(bytes: Bytes) -> Self {
440        Self::bytes(bytes)
441    }
442}
443
444impl From<Vec<u8>> for Upload {
445    fn from(bytes: Vec<u8>) -> Self {
446        Self::bytes(bytes)
447    }
448}
449
450impl From<&'static [u8]> for Upload {
451    fn from(bytes: &'static [u8]) -> Self {
452        Self::bytes(Bytes::from_static(bytes))
453    }
454}
455
456impl From<&Upload> for Upload {
457    fn from(upload: &Upload) -> Self {
458        upload.clone()
459    }
460}
461
462#[cfg(feature = "fs")]
463impl From<std::path::PathBuf> for Upload {
464    fn from(path: std::path::PathBuf) -> Self {
465        Self::path(path)
466    }
467}
468
469#[cfg(feature = "fs")]
470impl From<&std::path::PathBuf> for Upload {
471    fn from(path: &std::path::PathBuf) -> Self {
472        Self::path(path.clone())
473    }
474}
475
476#[cfg(feature = "fs")]
477impl From<&std::path::Path> for Upload {
478    fn from(path: &std::path::Path) -> Self {
479        Self::path(path)
480    }
481}
482
483impl fmt::Debug for Upload {
484    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
485        let mut debug = f.debug_struct("Upload");
486        match &self.source {
487            Source::Bytes(bytes) => debug.field("bytes", &bytes.len()),
488            #[cfg(feature = "fs")]
489            Source::File(path) => debug.field("path", path),
490            Source::Once(_) => debug.field("stream", &true),
491            Source::Factory(_) => debug.field("from_fn", &true),
492        };
493        debug
494            .field("file_name", &self.file_name)
495            .field("mime", &self.mime)
496            .field("content_length", &self.content_length())
497            .finish_non_exhaustive()
498    }
499}
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504
505    async fn read(upload: Upload) -> Bytes {
506        Body::from(upload).collect_bytes().await.unwrap()
507    }
508
509    fn chunks(
510        parts: &[&'static str],
511    ) -> impl Stream<Item = Result<Bytes, std::io::Error>> + Send + 'static {
512        stream::iter(
513            parts
514                .iter()
515                .map(|part| Ok(Bytes::from_static(part.as_bytes())))
516                .collect::<Vec<_>>(),
517        )
518    }
519
520    #[tokio::test]
521    async fn bytes_are_replayable_and_sliced_without_a_copy() {
522        let upload = Upload::bytes("hello world").slice(6..);
523        assert_eq!(upload.content_length(), Some(5));
524        let body = Body::from(upload);
525        assert_eq!(http_body::Body::size_hint(&body).exact(), Some(5));
526        let copy = body.try_clone().unwrap();
527        assert_eq!(body.collect_bytes().await.unwrap(), "world");
528        assert_eq!(copy.collect_bytes().await.unwrap(), "world");
529        assert_eq!(
530            read(Upload::bytes("hello").slice(1..=2).slice(1..)).await,
531            "l"
532        );
533        assert_eq!(read(Upload::bytes("hello").slice(10..)).await, "");
534    }
535
536    #[tokio::test]
537    async fn a_stream_is_read_once() {
538        let upload = Upload::stream(chunks(&["ab", "cd"]));
539        assert!(!upload.is_replayable());
540        let copy = upload.clone();
541        let body = Body::from(upload);
542        assert!(body.try_clone().is_none());
543        assert_eq!(body.collect_bytes().await.unwrap(), "abcd");
544        assert!(Body::from(copy).collect_bytes().await.is_err());
545    }
546
547    #[tokio::test]
548    async fn a_stream_is_sliced_across_chunks() {
549        assert_eq!(
550            read(Upload::stream(chunks(&["ab", "cd", "ef"])).slice(1..5)).await,
551            "bcde"
552        );
553        let sized = Upload::stream(chunks(&["ab", "cd"])).len(4).slice(1..);
554        assert_eq!(sized.content_length(), Some(3));
555        assert_eq!(
556            http_body::Body::size_hint(&Body::from(sized)).exact(),
557            Some(3)
558        );
559    }
560
561    #[tokio::test]
562    async fn from_fn_is_replayed() {
563        let upload = Upload::from_fn(|| chunks(&["x", "y"])).len(2);
564        assert!(upload.is_replayable());
565        let body = Body::from(upload);
566        let copy = body.try_clone().unwrap();
567        assert_eq!(body.collect_bytes().await.unwrap(), "xy");
568        assert_eq!(copy.collect_bytes().await.unwrap(), "xy");
569    }
570
571    #[tokio::test]
572    async fn buffer_for_retry_holds_a_short_stream() {
573        let short = Upload::stream(chunks(&["ab", "cd"]))
574            .buffer_for_retry(4)
575            .resolve()
576            .await
577            .unwrap();
578        assert!(short.is_replayable());
579        assert_eq!(short.content_length(), Some(4));
580        assert_eq!(read(short).await, "abcd");
581
582        let long = Upload::stream(chunks(&["ab", "cd", "e"]))
583            .buffer_for_retry(4)
584            .resolve()
585            .await
586            .unwrap();
587        assert!(!long.is_replayable());
588        assert_eq!(long.content_length(), None);
589        assert_eq!(read(long).await, "abcde");
590    }
591
592    #[cfg(all(feature = "fs", not(target_family = "wasm")))]
593    #[tokio::test]
594    async fn a_file_is_resolved_reopened_and_sliced() {
595        let dir = tempfile::tempdir().unwrap();
596        let path = dir.path().join("notes.json");
597        std::fs::write(&path, "0123456789").unwrap();
598
599        let upload = Upload::path(&path);
600        assert_eq!(upload.file_name.as_deref(), Some("notes.json"));
601        assert_eq!(upload.mime.as_deref(), Some("application/json"));
602        assert_eq!(upload.content_length(), None);
603        let upload = upload.resolve().await.unwrap();
604        assert_eq!(upload.content_length(), Some(10));
605
606        let body = Body::from(upload.clone());
607        assert_eq!(http_body::Body::size_hint(&body).exact(), Some(10));
608        let copy = body.try_clone().unwrap();
609        assert_eq!(body.collect_bytes().await.unwrap(), "0123456789");
610        assert_eq!(copy.collect_bytes().await.unwrap(), "0123456789");
611
612        let slice = upload.slice(2..5);
613        assert_eq!(slice.content_length(), Some(3));
614        assert_eq!(read(slice.clone()).await, "234");
615        assert_eq!(read(slice.slice(1..)).await, "34");
616        let tail = Upload::file(&path).await.unwrap().slice(8..20);
617        assert_eq!(tail.content_length(), Some(2));
618        assert_eq!(read(tail).await, "89");
619    }
620
621    #[cfg(all(feature = "fs", not(target_family = "wasm")))]
622    #[tokio::test]
623    async fn a_missing_file_fails_to_resolve() {
624        let error = Upload::file("/no/such/file.bin").await.unwrap_err();
625        assert!(error.is_io(), "{error:?}");
626        assert!(error.to_string().contains("/no/such/file.bin"), "{error}");
627    }
628
629    #[cfg(all(feature = "fs", not(target_family = "wasm")))]
630    #[tokio::test]
631    async fn a_file_that_shrank_is_an_error() {
632        let dir = tempfile::tempdir().unwrap();
633        let path = dir.path().join("data.bin");
634        std::fs::write(&path, "0123456789").unwrap();
635        let upload = Upload::file(&path).await.unwrap();
636        std::fs::write(&path, "0123").unwrap();
637        assert!(Body::from(upload).collect_bytes().await.is_err());
638    }
639
640    #[cfg(all(feature = "fs", not(target_family = "wasm")))]
641    #[tokio::test]
642    async fn a_reader_is_read_once() {
643        let upload = Upload::reader(&b"from a reader"[..]);
644        assert!(!upload.is_replayable());
645        assert_eq!(read(upload).await, "from a reader");
646    }
647}