1use color_eyre::Result;
8use color_eyre::eyre::eyre;
9use std::path::Path;
10use std::sync::Arc;
11
12use crate::loading::unfinished::{Claim, Writer};
13
14#[derive(Debug, Clone)]
18pub struct TempDownload(Arc<Held>);
19
20#[derive(Debug)]
23struct Held {
24 path: tempfile::TempPath,
25 _claim: Option<Claim>,
26 #[cfg(unix)]
30 _lock: Option<std::fs::File>,
31}
32
33#[cfg(unix)]
36pub(crate) fn held_elsewhere(path: &Path) -> bool {
37 use fs2::FileExt;
38 match std::fs::File::open(path) {
39 Ok(file) => file.try_lock_exclusive().is_err(),
40 Err(_) => true,
41 }
42}
43
44impl TempDownload {
45 pub fn create(dir: Option<&Path>, extension: Option<&str>) -> Result<tempfile::NamedTempFile> {
48 let dir = dir
49 .map(Path::to_path_buf)
50 .unwrap_or_else(std::env::temp_dir);
51 let suffix = extension
52 .map(|e| format!(".{e}"))
53 .unwrap_or_else(|| ".tmp".to_string());
54 tempfile::Builder::new()
55 .suffix(&suffix)
56 .tempfile_in(&dir)
57 .map_err(|_| eyre!("Could not create a temporary file."))
58 }
59
60 #[cfg(test)]
62 pub fn keep(file: tempfile::NamedTempFile) -> TempDownload {
63 Self::held(file, None)
64 }
65
66 pub(crate) fn held(file: tempfile::NamedTempFile, claim: Option<Claim>) -> TempDownload {
67 #[cfg(unix)]
68 let lock = file.as_file().try_clone().ok().filter(|lock| {
69 use fs2::FileExt;
70 FileExt::try_lock_shared(lock).is_ok()
71 });
72 TempDownload(Arc::new(Held {
73 path: file.into_temp_path(),
74 _claim: claim,
75 #[cfg(unix)]
76 _lock: lock,
77 }))
78 }
79
80 pub fn path(&self) -> &Path {
81 &self.0.path
82 }
83}
84
85pub const QUEUED_CHUNKS: usize = 4;
87
88const STALL_CHECK: std::time::Duration = std::time::Duration::from_millis(100);
90
91pub type Opened<S> = std::result::Result<(S, Option<u64>), String>;
94
95#[derive(Debug)]
97pub enum StreamError {
98 Open(String),
100 Read(String),
102 Write(color_eyre::Report),
104 Short { expected: u64, got: u64 },
106 Cut,
108}
109
110enum Piece<B> {
112 Opened(Option<u64>),
114 Refused(String),
115 Chunk(B),
116 Failed(String),
117 End,
118}
119
120fn receive<B: AsRef<[u8]>>(
123 stop: &impl Fn() -> bool,
124 mut next: impl FnMut() -> Option<Piece<B>>,
125 mut write: impl FnMut(&[u8]) -> Result<()>,
126) -> std::result::Result<u64, StreamError> {
127 let mut expected = None;
128 let mut written = 0u64;
129 loop {
130 if stop() {
131 return Err(StreamError::Cut);
132 }
133 match next() {
134 Some(Piece::Opened(len)) => expected = len,
135 Some(Piece::Refused(error)) => return Err(StreamError::Open(error)),
136 Some(Piece::Chunk(chunk)) => {
137 let chunk = chunk.as_ref();
138 write(chunk).map_err(StreamError::Write)?;
139 written += chunk.len() as u64;
140 }
141 Some(Piece::Failed(error)) => return Err(StreamError::Read(error)),
142 Some(Piece::End) => break,
143 None => return Err(StreamError::Cut),
144 }
145 }
146 match expected {
147 Some(expected) if expected != written => Err(StreamError::Short {
148 expected,
149 got: written,
150 }),
151 _ => Ok(written),
152 }
153}
154
155pub(crate) fn fill_file(
158 file: &mut impl std::io::Write,
159 unwritable: impl Fn(std::io::Error) -> color_eyre::Report,
160 fill: impl FnOnce(&mut dyn FnMut(&[u8]) -> Result<()>) -> std::result::Result<u64, StreamError>,
161) -> std::result::Result<u64, StreamError> {
162 let written = fill(&mut |chunk| file.write_all(chunk).map_err(&unwritable))?;
163 file.flush()
164 .map_err(|e| StreamError::Write(unwritable(e)))?;
165 Ok(written)
166}
167
168fn fill_temp(
173 dir: Option<&Path>,
174 extension: Option<&str>,
175 writer: &Writer,
176 fill: impl FnOnce(&mut dyn FnMut(&[u8]) -> Result<()>) -> std::result::Result<u64, StreamError>,
177) -> std::result::Result<TempDownload, StreamError> {
178 let Some((mut file, claim)) = writer
179 .create(|| TempDownload::create(dir, extension))
180 .map_err(StreamError::Write)?
181 else {
182 return Err(StreamError::Cut);
183 };
184 let unwritable = |e: std::io::Error| eyre!("Could not write the downloaded file: {e}");
185 match fill_file(&mut file, unwritable, fill) {
186 Ok(_) => Ok(TempDownload::held(file, Some(claim))),
187 Err(e) => {
188 drop(file);
190 drop(claim);
191 Err(e)
192 }
193 }
194}
195
196#[cfg(feature = "cloud")]
202pub fn stream_into<O, S, B, E>(
203 runtime: &tokio::runtime::Handle,
204 open: O,
205 stop: impl Fn() -> bool + Clone + Send + Sync + 'static,
206 write: impl FnMut(&[u8]) -> Result<()>,
207) -> std::result::Result<u64, StreamError>
208where
209 O: std::future::Future<Output = Opened<S>> + Send + 'static,
210 S: futures::Stream<Item = std::result::Result<B, E>> + Send + 'static,
211 B: AsRef<[u8]> + Send + 'static,
212 E: std::fmt::Display,
213{
214 let (tx, mut rx) = tokio::sync::mpsc::channel(QUEUED_CHUNKS);
215 let stopped = stop.clone();
216 runtime.spawn(async move {
220 let gone = || stopped() || tx.is_closed();
223 let (stream, len) = match until_stopped(open, &gone).await {
224 Some(Ok(opened)) => opened,
225 Some(Err(error)) => {
226 let _ = tx.send(Piece::Refused(error)).await;
227 return;
228 }
229 None => return,
230 };
231 if tx.send(Piece::Opened(len)).await.is_err() {
232 return;
233 }
234 let mut stream = std::pin::pin!(stream);
235 loop {
236 let next = futures::StreamExt::next(&mut stream);
237 let piece = match until_stopped(next, &gone).await {
238 None => return,
239 Some(Some(Ok(chunk))) => Piece::Chunk(chunk),
240 Some(Some(Err(error))) => Piece::Failed(error.to_string()),
241 Some(None) => Piece::End,
242 };
243 let last = !matches!(piece, Piece::Chunk(_));
244 if tx.send(piece).await.is_err() || last {
247 return;
248 }
249 }
250 });
251 receive(&stop, || rx.blocking_recv(), write)
252}
253
254#[cfg(feature = "cloud")]
256async fn until_stopped<F: std::future::Future>(
257 future: F,
258 stop: &impl Fn() -> bool,
259) -> Option<F::Output> {
260 let mut future = std::pin::pin!(future);
261 loop {
262 if stop() {
263 return None;
264 }
265 if let Ok(output) = tokio::time::timeout(STALL_CHECK, future.as_mut()).await {
266 return Some(output);
267 }
268 }
269}
270
271#[cfg(feature = "cloud")]
275pub(crate) fn stream_to_temp<O, S, B, E>(
276 runtime: &tokio::runtime::Handle,
277 dir: Option<&Path>,
278 extension: Option<&str>,
279 open: O,
280 writer: &Writer,
281) -> std::result::Result<TempDownload, StreamError>
282where
283 O: std::future::Future<Output = Opened<S>> + Send + 'static,
284 S: futures::Stream<Item = std::result::Result<B, E>> + Send + 'static,
285 B: AsRef<[u8]> + Send + 'static,
286 E: std::fmt::Display,
287{
288 let stop = {
289 let writer = writer.clone();
290 move || writer.stopped()
291 };
292 fill_temp(dir, extension, writer, |write| {
293 stream_into(runtime, open, stop, write)
294 })
295}
296
297const READ_CHUNK: usize = 64 * 1024;
299
300pub fn read_into<R: std::io::Read>(
305 open: impl FnOnce() -> Opened<R> + Send + 'static,
306 stop: impl Fn() -> bool,
307 write: impl FnMut(&[u8]) -> Result<()>,
308) -> std::result::Result<u64, StreamError> {
309 use std::sync::mpsc::RecvTimeoutError;
310
311 let (tx, rx) = std::sync::mpsc::sync_channel(QUEUED_CHUNKS);
312 std::thread::Builder::new()
313 .name("datui-download".to_string())
314 .spawn(move || {
315 let mut reader = match open() {
316 Ok((reader, len)) => {
317 if tx.send(Piece::Opened(len)).is_err() {
318 return;
319 }
320 reader
321 }
322 Err(error) => {
323 let _ = tx.send(Piece::Refused(error));
324 return;
325 }
326 };
327 loop {
328 let mut chunk = vec![0; READ_CHUNK];
329 let piece = match reader.read(&mut chunk) {
330 Ok(0) => Piece::End,
331 Ok(read) => {
332 chunk.truncate(read);
333 Piece::Chunk(chunk)
334 }
335 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
336 Err(error) => Piece::Failed(error.to_string()),
337 };
338 let last = !matches!(piece, Piece::Chunk(_));
339 if tx.send(piece).is_err() || last {
340 return;
341 }
342 }
343 })
344 .map_err(|e| StreamError::Open(e.to_string()))?;
345 let next = || loop {
346 match rx.recv_timeout(STALL_CHECK) {
347 Ok(piece) => return Some(piece),
348 Err(RecvTimeoutError::Timeout) if !stop() => {}
349 Err(RecvTimeoutError::Timeout) => return None,
350 Err(RecvTimeoutError::Disconnected) => {
352 return Some(Piece::Failed("the download stopped".to_string()));
353 }
354 }
355 };
356 receive(&stop, next, write)
357}
358
359#[cfg(feature = "http")]
363pub(crate) fn read_to_temp<R: std::io::Read>(
364 dir: Option<&Path>,
365 extension: Option<&str>,
366 open: impl FnOnce() -> Opened<R> + Send + 'static,
367 writer: &Writer,
368 limit: Option<u64>,
369) -> std::result::Result<TempDownload, StreamError> {
370 let mut arrived = 0u64;
371 fill_temp(dir, extension, writer, |write| {
372 read_into(
373 open,
374 || writer.stopped(),
375 |chunk| {
376 arrived += chunk.len() as u64;
377 if limit.is_some_and(|limit| arrived > limit) {
378 return Err(color_eyre::Report::new(PastLimit));
379 }
380 write(chunk)
381 },
382 )
383 })
384}
385
386#[cfg(any(feature = "http", feature = "cloud"))]
388#[derive(Debug)]
389pub(crate) struct PastLimit;
390
391#[cfg(any(feature = "http", feature = "cloud"))]
392impl std::fmt::Display for PastLimit {
393 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394 f.write_str("The download passed the size it may fetch without asking.")
395 }
396}
397
398#[cfg(any(feature = "http", feature = "cloud"))]
399impl std::error::Error for PastLimit {}
400
401pub(crate) fn spool_to_temp<R: std::io::Read>(
405 dir: Option<&Path>,
406 open: impl FnOnce() -> Opened<R> + Send + 'static,
407 writer: &Writer,
408 read: &std::sync::atomic::AtomicU64,
409) -> std::result::Result<TempDownload, StreamError> {
410 use std::sync::atomic::Ordering;
411 fill_temp(dir, None, writer, |write| {
412 read_into(
413 open,
414 || writer.stopped(),
415 |chunk| {
416 write(chunk)?;
417 read.fetch_add(chunk.len() as u64, Ordering::Relaxed);
418 Ok(())
419 },
420 )
421 })
422}
423
424#[cfg(all(test, feature = "cloud"))]
425mod tests {
426 use super::*;
427 use futures::StreamExt;
428 use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
429
430 const CHUNK: usize = 64 * 1024;
431
432 fn runtime() -> tokio::runtime::Runtime {
433 tokio::runtime::Builder::new_multi_thread()
434 .worker_threads(1)
435 .enable_all()
436 .build()
437 .expect("runtime")
438 }
439
440 fn chunk(i: usize, len: usize) -> Vec<u8> {
443 (0..len).map(|j| ((i * 31 + j * 7) % 251) as u8).collect()
444 }
445
446 #[derive(Default)]
449 struct Live {
450 now: AtomicU64,
451 peak: AtomicU64,
452 }
453
454 struct Tracked(Vec<u8>, Arc<Live>);
455
456 impl Tracked {
457 fn new(bytes: Vec<u8>, live: &Arc<Live>) -> Tracked {
458 let now = live.now.fetch_add(bytes.len() as u64, Ordering::SeqCst) + bytes.len() as u64;
459 live.peak.fetch_max(now, Ordering::SeqCst);
460 Tracked(bytes, live.clone())
461 }
462 }
463
464 impl Drop for Tracked {
465 fn drop(&mut self) {
466 self.1.now.fetch_sub(self.0.len() as u64, Ordering::SeqCst);
467 }
468 }
469
470 impl AsRef<[u8]> for Tracked {
471 fn as_ref(&self) -> &[u8] {
472 &self.0
473 }
474 }
475
476 struct DropFlag(Arc<AtomicBool>);
478
479 impl Drop for DropFlag {
480 fn drop(&mut self) {
481 self.0.store(true, Ordering::SeqCst);
482 }
483 }
484
485 async fn opened<S>(stream: S, len: Option<u64>) -> Opened<S> {
486 Ok((stream, len))
487 }
488
489 fn never() -> impl Fn() -> bool + Clone + Send + Sync + 'static {
490 || false
491 }
492
493 fn unstopped() -> Writer {
495 Writer::default()
496 }
497
498 fn files_in(dir: &Path) -> usize {
499 std::fs::read_dir(dir).unwrap().count()
500 }
501
502 #[test]
504 fn a_stream_lands_whole_in_order() {
505 let rt = runtime();
506 let dir = tempfile::tempdir().unwrap();
507 let chunks = (0..40).map(|i| chunk(i, 1000 + i * 13)).collect::<Vec<_>>();
508 let whole = chunks.concat();
509 let stream = futures::stream::iter(chunks.into_iter().map(Ok::<_, String>));
510 let file = stream_to_temp(
511 rt.handle(),
512 Some(dir.path()),
513 Some("csv.gz"),
514 opened(stream, Some(whole.len() as u64)),
515 &unstopped(),
516 )
517 .unwrap();
518 assert_eq!(std::fs::read(file.path()).unwrap(), whole);
519 assert!(file.path().to_string_lossy().ends_with(".csv.gz"));
520 assert_eq!(file.path().parent(), Some(dir.path()));
521 let held = file.clone();
522 drop(file);
523 assert!(held.path().exists(), "a holder keeps it");
524 let path = held.path().to_path_buf();
525 drop(held);
526 assert!(!path.exists(), "the last holder removes it");
527 }
528
529 #[test]
533 fn a_download_is_claimed_for_as_long_as_it_lives() {
534 let rt = runtime();
535 let dir = tempfile::tempdir().unwrap();
536 let unfinished = crate::loading::unfinished::Unfinished::default();
537 let stop = Arc::new(AtomicBool::new(false));
538 let writer = unfinished.writer(stop.clone());
539 let stream = futures::stream::iter(vec![Ok::<_, String>(chunk(0, 10))]);
540 let file = stream_to_temp(
541 rt.handle(),
542 Some(dir.path()),
543 None,
544 opened(stream, None),
545 &writer,
546 )
547 .unwrap();
548 let held = file.clone();
549 drop(file);
550 assert!(unfinished.writing(), "a holder keeps the claim");
551 let path = held.path().to_path_buf();
552 drop(held);
553 assert!(!path.exists());
554 assert!(!unfinished.writing(), "the claim goes with the file");
555
556 stop.store(true, Ordering::SeqCst);
557 let stream = futures::stream::iter(vec![Ok::<_, String>(chunk(0, 10))]);
558 let error = stream_to_temp(
559 rt.handle(),
560 Some(dir.path()),
561 None,
562 opened(stream, None),
563 &writer,
564 )
565 .unwrap_err();
566 assert!(matches!(error, StreamError::Cut), "{error:?}");
567 assert_eq!(files_in(dir.path()), 0);
568 }
569
570 #[test]
574 fn a_slow_writer_holds_the_stream_back() {
575 let rt = runtime();
576 let live = Arc::new(Live::default());
577 let pulled = Arc::new(AtomicUsize::new(0));
578 let chunks = 64;
579 let stream = {
580 let (live, pulled) = (live.clone(), pulled.clone());
581 futures::stream::iter(0..chunks).map(move |i| {
582 pulled.fetch_add(1, Ordering::SeqCst);
583 Ok::<_, String>(Tracked::new(chunk(i, CHUNK), &live))
584 })
585 };
586 let mut written = 0usize;
587 let mut ahead = 0usize;
588 let mut bytes = Vec::new();
589 let total = stream_into(rt.handle(), opened(stream, None), never(), |piece| {
590 std::thread::sleep(std::time::Duration::from_millis(2));
591 written += 1;
592 ahead = ahead.max(pulled.load(Ordering::SeqCst) - written);
593 bytes.extend_from_slice(piece);
594 Ok(())
595 })
596 .unwrap();
597 assert_eq!(total, (chunks * CHUNK) as u64);
598 assert_eq!(
599 bytes,
600 (0..chunks)
601 .flat_map(|i| chunk(i, CHUNK))
602 .collect::<Vec<_>>()
603 );
604 let bound = QUEUED_CHUNKS + 2;
606 assert!(ahead <= bound, "the store ran {ahead} chunks ahead");
607 let peak = live.peak.load(Ordering::SeqCst);
608 assert!(
609 peak <= (bound * CHUNK) as u64,
610 "{peak} bytes held at once, of {} streamed",
611 chunks * CHUNK
612 );
613 assert_eq!(live.now.load(Ordering::SeqCst), 0, "every chunk was let go");
614 }
615
616 #[test]
618 fn a_failure_mid_stream_leaves_no_file() {
619 let rt = runtime();
620 let dir = tempfile::tempdir().unwrap();
621 let stream = futures::stream::iter(vec![
622 Ok(chunk(0, CHUNK)),
623 Ok(chunk(1, CHUNK)),
624 Err("connection reset".to_string()),
625 Ok(chunk(3, CHUNK)),
626 ]);
627 let error = stream_to_temp(
628 rt.handle(),
629 Some(dir.path()),
630 None,
631 opened(stream, None),
632 &unstopped(),
633 )
634 .unwrap_err();
635 assert!(
636 matches!(&error, StreamError::Read(e) if e == "connection reset"),
637 "{error:?}"
638 );
639 assert_eq!(files_in(dir.path()), 0);
640
641 let refused = async {
642 Err::<(futures::stream::Empty<Result<Vec<u8>, String>>, _), _>("403".to_string())
643 };
644 let error =
645 stream_to_temp(rt.handle(), Some(dir.path()), None, refused, &unstopped()).unwrap_err();
646 assert!(
647 matches!(&error, StreamError::Open(e) if e == "403"),
648 "{error:?}"
649 );
650 assert_eq!(files_in(dir.path()), 0);
651
652 let stream = futures::stream::iter(vec![Ok::<_, String>(chunk(0, 10))]);
654 let error = stream_to_temp(
655 rt.handle(),
656 Some(dir.path()),
657 None,
658 opened(stream, Some(20)),
659 &unstopped(),
660 )
661 .unwrap_err();
662 assert!(
663 matches!(
664 error,
665 StreamError::Short {
666 expected: 20,
667 got: 10
668 }
669 ),
670 "{error:?}"
671 );
672 assert_eq!(files_in(dir.path()), 0);
673 }
674
675 #[test]
678 fn a_refused_write_stops_the_stream() {
679 let rt = runtime();
680 let pulled = Arc::new(AtomicUsize::new(0));
681 let ended = Arc::new(AtomicBool::new(false));
682 let stream = {
683 let pulled = pulled.clone();
684 let guard = DropFlag(ended.clone());
685 futures::stream::iter(0..1000).map(move |i| {
686 let _ = &guard;
687 pulled.fetch_add(1, Ordering::SeqCst);
688 Ok::<_, String>(chunk(i, 1024))
689 })
690 };
691 let mut writes = 0;
692 let error = stream_into(rt.handle(), opened(stream, None), never(), |_| {
693 writes += 1;
694 if writes == 3 {
695 return Err(eyre!("No space left on device"));
696 }
697 Ok(())
698 })
699 .unwrap_err();
700 assert!(matches!(&error, StreamError::Write(e) if e.to_string().contains("No space")));
701 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
703 while !ended.load(Ordering::SeqCst) {
704 assert!(
705 std::time::Instant::now() < deadline,
706 "the stream is still read"
707 );
708 std::thread::sleep(std::time::Duration::from_millis(5));
709 }
710 let pulled = pulled.load(Ordering::SeqCst);
711 assert!(pulled <= 3 + QUEUED_CHUNKS + 2, "{pulled} chunks read");
712
713 let dropped = Arc::new(AtomicBool::new(false));
716 let stream = {
717 let guard = DropFlag(dropped.clone());
718 futures::stream::iter(vec![Ok::<_, String>(chunk(0, 1024))])
719 .chain(futures::stream::pending())
720 .map(move |chunk| {
721 let _ = &guard;
722 chunk
723 })
724 };
725 let error = stream_into(rt.handle(), opened(stream, None), never(), |_| {
726 Err(eyre!("No space left on device"))
727 })
728 .unwrap_err();
729 assert!(matches!(error, StreamError::Write(_)));
730 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
731 while !dropped.load(Ordering::SeqCst) {
732 assert!(
733 std::time::Instant::now() < deadline,
734 "the request is still open"
735 );
736 std::thread::sleep(std::time::Duration::from_millis(10));
737 }
738
739 let missing = tempfile::tempdir().unwrap().path().join("gone");
741 let asked = Arc::new(AtomicBool::new(false));
742 let open = {
743 let asked = asked.clone();
744 async move {
745 asked.store(true, Ordering::SeqCst);
746 Ok((futures::stream::empty::<Result<Vec<u8>, String>>(), None))
747 }
748 };
749 let error =
750 stream_to_temp(rt.handle(), Some(&missing), None, open, &unstopped()).unwrap_err();
751 assert!(matches!(error, StreamError::Write(_)));
752 assert!(!asked.load(Ordering::SeqCst));
753 }
754
755 #[test]
758 fn a_stop_ends_the_download_and_removes_the_file() {
759 let rt = runtime();
760 let dir = tempfile::tempdir().unwrap();
761 let stop = Arc::new(AtomicBool::new(false));
762 let stopped = crate::loading::unfinished::Unfinished::default().writer(stop.clone());
763
764 let stream = {
765 let stop = stop.clone();
766 futures::stream::iter(0..100).map(move |i| {
767 if i == 3 {
768 stop.store(true, Ordering::SeqCst);
769 }
770 Ok::<_, String>(chunk(i, CHUNK))
771 })
772 };
773 let error = stream_to_temp(
774 rt.handle(),
775 Some(dir.path()),
776 None,
777 opened(stream, None),
778 &stopped,
779 )
780 .unwrap_err();
781 assert!(matches!(error, StreamError::Cut), "{error:?}");
782 assert_eq!(files_in(dir.path()), 0);
783
784 stop.store(false, Ordering::SeqCst);
786 let stream = futures::stream::iter(vec![Ok::<_, String>(chunk(0, CHUNK))])
787 .chain(futures::stream::pending());
788 let stopper = {
790 let (stop, dir) = (stop.clone(), dir.path().to_path_buf());
791 std::thread::spawn(move || {
792 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
793 while !std::fs::read_dir(&dir).unwrap().any(|f| {
794 std::fs::metadata(f.unwrap().path()).map_or(0, |m| m.len()) == CHUNK as u64
795 }) && std::time::Instant::now() < deadline
796 {
797 std::thread::sleep(std::time::Duration::from_millis(5));
798 }
799 stop.store(true, Ordering::SeqCst);
800 })
801 };
802 let began = std::time::Instant::now();
803 let error = stream_to_temp(
804 rt.handle(),
805 Some(dir.path()),
806 None,
807 opened(stream, None),
808 &stopped,
809 )
810 .unwrap_err();
811 stopper.join().unwrap();
812 assert!(matches!(error, StreamError::Cut), "{error:?}");
813 assert!(began.elapsed() < std::time::Duration::from_secs(5));
814 assert_eq!(files_in(dir.path()), 0);
815 }
816
817 #[test]
821 fn a_shutdown_mid_transfer_cuts_it_off() {
822 let rt = runtime();
823 let dir = tempfile::tempdir().unwrap();
824 let stream = futures::stream::iter(vec![Ok::<_, String>(chunk(0, CHUNK))])
825 .chain(futures::stream::pending());
826 let handle = rt.handle().clone();
827 let path = dir.path().to_path_buf();
828 let waiter = std::thread::spawn(move || {
829 stream_to_temp(
830 &handle,
831 Some(&path),
832 None,
833 opened(stream, None),
834 &unstopped(),
835 )
836 });
837 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
841 while !std::fs::read_dir(dir.path())
842 .unwrap()
843 .any(|f| std::fs::metadata(f.unwrap().path()).map_or(0, |m| m.len()) == CHUNK as u64)
844 {
845 assert!(
846 std::time::Instant::now() < deadline,
847 "the first chunk never landed"
848 );
849 std::thread::sleep(std::time::Duration::from_millis(5));
850 }
851 rt.shutdown_background();
852 let error = waiter.join().expect("no panic").unwrap_err();
853 assert!(matches!(error, StreamError::Cut), "{error:?}");
854 assert_eq!(files_in(dir.path()), 0);
855 }
856}
857
858#[cfg(all(test, feature = "http"))]
859mod read_tests {
860 use super::*;
861 use std::io::Read;
862 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
863 use std::time::{Duration, Instant};
864
865 struct Source {
868 chunks: std::vec::IntoIter<Vec<u8>>,
869 release: Option<std::sync::mpsc::Receiver<()>>,
870 reads: Arc<AtomicUsize>,
871 dropped: Arc<AtomicBool>,
872 }
873
874 impl Read for Source {
875 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
876 self.reads.fetch_add(1, Ordering::SeqCst);
877 if let Some(chunk) = self.chunks.next() {
878 buf[..chunk.len()].copy_from_slice(&chunk);
879 return Ok(chunk.len());
880 }
881 if let Some(release) = self.release.take() {
882 let _ = release.recv();
883 }
884 Ok(0)
885 }
886 }
887
888 impl Drop for Source {
889 fn drop(&mut self) {
890 self.dropped.store(true, Ordering::SeqCst);
891 }
892 }
893
894 fn source(chunks: Vec<Vec<u8>>, release: Option<std::sync::mpsc::Receiver<()>>) -> Source {
895 Source {
896 chunks: chunks.into_iter(),
897 release,
898 reads: Arc::default(),
899 dropped: Arc::default(),
900 }
901 }
902
903 fn files_in(dir: &Path) -> usize {
904 std::fs::read_dir(dir).unwrap().count()
905 }
906
907 #[track_caller]
908 fn wait_until(what: &str, done: impl Fn() -> bool) {
909 let deadline = Instant::now() + Duration::from_secs(5);
910 while !done() {
911 assert!(Instant::now() < deadline, "{what}");
912 std::thread::sleep(Duration::from_millis(5));
913 }
914 }
915
916 #[test]
918 fn a_reader_lands_whole_in_order() {
919 let dir = tempfile::tempdir().unwrap();
920 let chunks = (0..40u8)
921 .map(|i| vec![i; 1000 + usize::from(i) * 13])
922 .collect::<Vec<_>>();
923 let whole = chunks.concat();
924 let len = whole.len() as u64;
925 let reader = source(chunks.clone(), None);
926 let file = read_to_temp(
927 Some(dir.path()),
928 Some("csv"),
929 move || Ok((reader, Some(len))),
930 &Writer::default(),
931 None,
932 )
933 .unwrap();
934 assert_eq!(std::fs::read(file.path()).unwrap(), whole);
935 assert!(file.path().to_string_lossy().ends_with(".csv"));
936
937 let reader = source(chunks, None);
938 let error = read_to_temp(
939 Some(dir.path()),
940 None,
941 move || Ok((reader, Some(len + 1))),
942 &Writer::default(),
943 None,
944 )
945 .unwrap_err();
946 assert!(matches!(error, StreamError::Short { .. }), "{error:?}");
947 drop(file);
948 assert_eq!(files_in(dir.path()), 0);
949 }
950
951 #[test]
954 fn a_download_past_its_limit_stops_and_leaves_no_file() {
955 let dir = tempfile::tempdir().unwrap();
956 let chunks = (0..8u8).map(|i| vec![i; 1000]).collect::<Vec<_>>();
957 let reader = source(chunks.clone(), None);
958 let error = read_to_temp(
959 Some(dir.path()),
960 None,
961 move || Ok((reader, None)),
962 &Writer::default(),
963 Some(7_999),
964 )
965 .unwrap_err();
966 assert!(
967 matches!(&error, StreamError::Write(report) if report.downcast_ref::<PastLimit>().is_some()),
968 "{error:?}"
969 );
970 assert_eq!(files_in(dir.path()), 0);
971
972 let reader = source(chunks, None);
973 let file = read_to_temp(
974 Some(dir.path()),
975 None,
976 move || Ok((reader, None)),
977 &Writer::default(),
978 Some(8_000),
979 )
980 .unwrap();
981 assert_eq!(std::fs::metadata(file.path()).unwrap().len(), 8_000);
982 }
983
984 #[test]
986 fn a_refusal_or_failed_read_leaves_no_file() {
987 struct Reset(bool);
988 impl Read for Reset {
989 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
990 if std::mem::replace(&mut self.0, true) {
991 return Err(std::io::ErrorKind::ConnectionReset.into());
992 }
993 buf[..3].copy_from_slice(b"a,b");
994 Ok(3)
995 }
996 }
997
998 let dir = tempfile::tempdir().unwrap();
999 let error = read_to_temp(
1000 Some(dir.path()),
1001 None,
1002 || Err::<(Reset, _), _>("Server returned 404 Not Found.".to_string()),
1003 &Writer::default(),
1004 None,
1005 )
1006 .unwrap_err();
1007 assert!(
1008 matches!(&error, StreamError::Open(e) if e.contains("404")),
1009 "{error:?}"
1010 );
1011 let error = read_to_temp(
1012 Some(dir.path()),
1013 None,
1014 || Ok((Reset(false), None)),
1015 &Writer::default(),
1016 None,
1017 )
1018 .unwrap_err();
1019 assert!(matches!(error, StreamError::Read(_)), "{error:?}");
1020 assert_eq!(files_in(dir.path()), 0);
1021 }
1022
1023 #[test]
1025 fn a_slow_writer_holds_the_reads_back() {
1026 let reader = source((0..64).map(|i| vec![i; 1024]).collect(), None);
1027 let reads = reader.reads.clone();
1028 let mut written = 0usize;
1029 let mut ahead = 0usize;
1030 let total = read_into(
1031 move || Ok((reader, None)),
1032 || false,
1033 |_| {
1034 std::thread::sleep(Duration::from_millis(2));
1035 written += 1;
1036 ahead = ahead.max(reads.load(Ordering::SeqCst) - written);
1037 Ok(())
1038 },
1039 )
1040 .unwrap();
1041 assert_eq!(total, 64 * 1024);
1042 assert!(ahead <= QUEUED_CHUNKS + 2, "read {ahead} chunks ahead");
1044 }
1045
1046 #[test]
1049 fn a_stop_while_the_server_is_silent_ends_it() {
1050 let dir = tempfile::tempdir().unwrap();
1051 let (release, held) = std::sync::mpsc::channel();
1052 let reader = source(vec![vec![1; 1024]], Some(held));
1053 let dropped = reader.dropped.clone();
1054 let stop = Arc::new(AtomicBool::new(false));
1055 let stopper = {
1056 let stop = stop.clone();
1057 let dir = dir.path().to_path_buf();
1058 std::thread::spawn(move || {
1059 let deadline = Instant::now() + Duration::from_secs(5);
1063 let landed = loop {
1064 let sizes = std::fs::read_dir(&dir)
1065 .unwrap()
1066 .filter_map(|f| std::fs::metadata(f.unwrap().path()).ok())
1067 .map(|m| m.len());
1068 if sizes.into_iter().any(|len| len == 1024) {
1069 break true;
1070 }
1071 if Instant::now() > deadline {
1072 break false;
1073 }
1074 std::thread::sleep(Duration::from_millis(5));
1075 };
1076 stop.store(true, Ordering::SeqCst);
1079 (landed, Instant::now())
1080 })
1081 };
1082 let error = read_to_temp(
1083 Some(dir.path()),
1084 None,
1085 move || Ok((reader, None)),
1086 &crate::loading::unfinished::Unfinished::default().writer(stop),
1087 None,
1088 )
1089 .unwrap_err();
1090 let (landed, stopped_at) = stopper.join().unwrap();
1091 assert!(landed, "the first chunk landed");
1092 assert!(matches!(error, StreamError::Cut), "{error:?}");
1093 assert!(stopped_at.elapsed() < Duration::from_secs(2));
1094 assert_eq!(files_in(dir.path()), 0);
1095
1096 assert!(
1097 !dropped.load(Ordering::SeqCst),
1098 "still waiting on the server"
1099 );
1100 release.send(()).unwrap();
1101 wait_until("the reader was let go", || dropped.load(Ordering::SeqCst));
1102 }
1103}