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#[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 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 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 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 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 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 pub fn mime(mut self, mime: impl Into<Cow<'static, str>>) -> Self {
130 self.mime = Some(mime.into());
131 self
132 }
133
134 pub fn name(&self) -> Option<&str> {
137 self.file_name.as_deref()
138 }
139
140 pub fn media_type(&self) -> Option<&str> {
142 self.mime.as_deref()
143 }
144
145 pub fn len(mut self, len: u64) -> Self {
148 self.total = Some(len);
149 self
150 }
151
152 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 pub fn buffer_for_retry(mut self, limit: usize) -> Self {
183 self.buffer_limit = Some(limit);
184 self
185 }
186
187 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 pub fn is_replayable(&self) -> bool {
199 !matches!(self.source, Source::Once(_))
200 }
201
202 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 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 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 pub async fn file(path: impl Into<std::path::PathBuf>) -> Result<Self, Error> {
284 Self::path(path).resolve().await
285 }
286
287 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#[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#[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#[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
365fn 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
394fn 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}