1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::io::Write;
4use std::ops::{Bound, Range, RangeBounds};
5#[cfg(not(target_family = "wasm"))]
6use std::path::{Path, PathBuf};
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::{Arc, Mutex};
9
10#[cfg(not(target_family = "wasm"))]
11use tokio::task::JoinHandle;
12use tracing::instrument;
13use xet_client::cas_client::Client;
14use xet_client::cas_types::FileRange;
15use xet_client::chunk_cache::ChunkCache;
16use xet_runtime::core::XetContext;
17use xet_runtime::utils::UniqueId;
18
19use super::XetFileInfo;
20use super::configurations::TranslatorConfig;
21use super::remote_client_interface::create_remote_client;
22use crate::error::{DataError, Result};
23use crate::file_reconstruction::{DownloadStream, FileReconstructor, UnorderedDownloadStream};
24use crate::progress_tracking::{GroupProgress, ItemProgressUpdater};
25
26pub struct FileDownloadSession {
31 ctx: XetContext,
32 client: Arc<dyn Client>,
33 chunk_cache: Option<Arc<dyn ChunkCache>>,
34 progress: Arc<GroupProgress>,
35 active_stream_abort_callbacks: Mutex<HashMap<UniqueId, Box<dyn Fn() + Send + Sync>>>,
36 finalized: AtomicBool,
37}
38
39impl FileDownloadSession {
40 pub async fn new(config: Arc<TranslatorConfig>, chunk_cache: Option<Arc<dyn ChunkCache>>) -> Result<Arc<Self>> {
41 let session_id = config
42 .session
43 .session_id
44 .as_ref()
45 .map(Cow::Borrowed)
46 .unwrap_or_else(|| Cow::Owned(UniqueId::new().to_string()));
47
48 let ctx = config.ctx.clone();
49 let client = create_remote_client(&config, &session_id, false).await?;
50 let progress = Arc::new(GroupProgress::with_speed_config(
51 ctx.config.data.progress_update_speed_sampling_window,
52 ctx.config.data.progress_update_speed_min_observations,
53 ));
54
55 Ok(Arc::new(Self {
56 ctx,
57 client,
58 chunk_cache,
59 progress,
60 active_stream_abort_callbacks: Mutex::new(HashMap::new()),
61 finalized: AtomicBool::new(false),
62 }))
63 }
64
65 pub fn from_client(
71 ctx: &XetContext,
72 client: Arc<dyn Client>,
73 chunk_cache: Option<Arc<dyn ChunkCache>>,
74 ) -> Arc<Self> {
75 let progress = Arc::new(GroupProgress::new());
76 Arc::new(Self {
77 ctx: ctx.clone(),
78 client,
79 chunk_cache,
80 progress,
81 active_stream_abort_callbacks: Mutex::new(HashMap::new()),
82 finalized: AtomicBool::new(false),
83 })
84 }
85
86 pub fn report(&self) -> crate::progress_tracking::GroupProgressReport {
87 self.progress.report()
88 }
89
90 pub fn item_report(&self, id: UniqueId) -> Option<crate::progress_tracking::ItemProgressReport> {
91 self.progress.item_report(id)
92 }
93
94 pub fn item_reports(&self) -> HashMap<UniqueId, crate::progress_tracking::ItemProgressReport> {
95 self.progress.item_reports()
96 }
97
98 fn register_stream_abort_callback(&self, id: UniqueId, callback: Box<dyn Fn() + Send + Sync>) {
99 self.active_stream_abort_callbacks.lock().unwrap().insert(id, callback);
100 }
101
102 pub fn unregister_stream_abort_callback(&self, id: UniqueId) {
103 self.active_stream_abort_callbacks.lock().unwrap().remove(&id);
104 }
105
106 pub fn abort_active_streams(&self) {
107 let callbacks = self.active_stream_abort_callbacks.lock().unwrap();
108 for callback in callbacks.values() {
109 callback();
110 }
111 }
112
113 #[instrument(skip_all, name = "FileDownloadSession::download_stream", fields(hash = file_info.hash()))]
126 pub async fn download_stream(
127 &self,
128 file_info: &XetFileInfo,
129 source_range: Option<Range<u64>>,
130 ) -> Result<(UniqueId, DownloadStream)> {
131 self.check_not_finalized()?;
132 let id = UniqueId::new();
133 let progress_updater = self.progress.new_item(id, "stream");
134 let range = source_range.map(|r| FileRange::new(r.start, r.end));
135 let reconstructor = self.setup_reconstructor(file_info, range, Some(progress_updater))?;
136 let stream = reconstructor.reconstruct_to_stream();
137 self.register_stream_abort_callback(id, stream.abort_callback());
138 Ok((id, stream))
139 }
140
141 #[instrument(skip_all, name = "FileDownloadSession::download_unordered_stream", fields(hash = file_info.hash()))]
154 pub async fn download_unordered_stream(
155 &self,
156 file_info: &XetFileInfo,
157 source_range: Option<Range<u64>>,
158 ) -> Result<(UniqueId, UnorderedDownloadStream)> {
159 self.check_not_finalized()?;
160 let id = UniqueId::new();
161 let progress_updater = self.progress.new_item(id, "unordered_stream");
162 let range = source_range.map(|r| FileRange::new(r.start, r.end));
163 let reconstructor = self.setup_reconstructor(file_info, range, Some(progress_updater))?;
164 let stream = reconstructor.reconstruct_to_unordered_stream();
165 self.register_stream_abort_callback(id, stream.abort_callback());
166 Ok((id, stream))
167 }
168
169 #[instrument(skip_all, name = "FileDownloadSession::download_stream_range", fields(hash = file_info.hash()))]
175 pub async fn download_stream_range(
176 &self,
177 file_info: &XetFileInfo,
178 range: impl RangeBounds<u64>,
179 ) -> Result<(UniqueId, DownloadStream)> {
180 self.check_not_finalized()?;
181 let file_range = range_bounds_to_file_range(&range)?;
182 let id = UniqueId::new();
183 let progress_updater = self.progress.new_item(id, "stream");
184 let reconstructor = self.setup_reconstructor(file_info, file_range, Some(progress_updater))?;
185 let stream = reconstructor.reconstruct_to_stream();
186 self.register_stream_abort_callback(id, stream.abort_callback());
187 Ok((id, stream))
188 }
189 fn check_not_finalized(&self) -> Result<()> {
190 if self.finalized.load(Ordering::Acquire) {
191 return Err(DataError::InvalidOperation("FileDownloadSession already finalized".to_string()));
192 }
193 Ok(())
194 }
195
196 pub async fn finalize(&self) -> Result<()> {
198 if self.finalized.swap(true, Ordering::AcqRel) {
199 return Err(DataError::InvalidOperation("FileDownloadSession already finalized".to_string()));
200 }
201 #[cfg(debug_assertions)]
202 self.progress.assert_complete();
203 Ok(())
204 }
205
206 fn setup_reconstructor(
207 &self,
208 file_info: &XetFileInfo,
209 range: Option<FileRange>,
210 progress_updater: Option<Arc<ItemProgressUpdater>>,
211 ) -> Result<FileReconstructor> {
212 let file_id = file_info.merkle_hash()?;
213
214 let mut reconstructor = FileReconstructor::new(&self.ctx, &self.client, file_id);
215
216 match range {
217 Some(range) if range.end < u64::MAX => {
218 let size = range.end - range.start;
220 if let Some(ref updater) = progress_updater {
221 updater.update_item_size(size, true);
222 }
223 reconstructor = reconstructor.with_byte_range(range);
224 },
225 Some(range) => {
226 reconstructor = reconstructor.with_byte_range(range);
230 },
231 None if file_info.file_size().is_some() => {
232 if let Some(ref updater) = progress_updater {
236 updater.update_item_size(file_info.file_size().unwrap(), true);
237 }
238 },
239 None => {
240 },
244 }
245
246 if let Some(updater) = progress_updater {
247 reconstructor = reconstructor.with_progress_updater(updater);
248 }
249
250 if let Some(ref cache) = self.chunk_cache {
251 reconstructor = reconstructor.with_chunk_cache(cache.clone());
252 }
253
254 Ok(reconstructor)
255 }
256}
257
258#[cfg(not(target_family = "wasm"))]
261impl FileDownloadSession {
262 pub async fn download_file_background(
267 self: &Arc<Self>,
268 file_info: XetFileInfo,
269 write_path: PathBuf,
270 ) -> Result<(UniqueId, JoinHandle<Result<u64>>)> {
271 self.check_not_finalized()?;
272 let id = UniqueId::new();
273 let session = self.clone();
274 let runtime = self.ctx.runtime.clone();
275 let semaphore = self.ctx.common.file_download_semaphore.clone();
276 let handle = runtime.spawn(async move {
277 let _permit = semaphore.acquire().await?;
278 session.download_file_with_id(&file_info, &write_path, id).await
279 });
280 Ok((id, handle))
281 }
282
283 #[instrument(skip_all, name = "FileDownloadSession::download_file", fields(hash = file_info.hash()))]
285 pub async fn download_file(&self, file_info: &XetFileInfo, write_path: &Path) -> Result<(UniqueId, u64)> {
286 self.check_not_finalized()?;
287 let id = UniqueId::new();
288 let n_bytes = self.download_file_with_id(file_info, write_path, id).await?;
289 Ok((id, n_bytes))
290 }
291
292 async fn download_file_with_id(&self, file_info: &XetFileInfo, write_path: &Path, id: UniqueId) -> Result<u64> {
293 let name = Arc::from(write_path.to_string_lossy().as_ref());
294 let progress_updater = self.progress.new_item(id, name);
295 let reconstructor = self.setup_reconstructor(file_info, None, Some(progress_updater))?;
296 let n_bytes = reconstructor.reconstruct_to_file(write_path, None, true).await?;
297 if let Some(expected_size) = file_info.file_size()
300 && n_bytes != expected_size
301 {
302 return Err(DataError::SizeMismatch {
303 expected: expected_size,
304 actual: n_bytes,
305 });
306 }
307 Ok(n_bytes)
308 }
309}
310
311impl FileDownloadSession {
316 #[instrument(skip_all, name = "FileDownloadSession::download_to_writer",
324 fields(hash = file_info.hash(), range_start = tracing::field::Empty, range_end = tracing::field::Empty))]
325 pub async fn download_to_writer<W: Write + Send + 'static>(
326 &self,
327 file_info: &XetFileInfo,
328 source_range: impl RangeBounds<u64>,
329 writer: W,
330 ) -> Result<(UniqueId, u64)> {
331 self.check_not_finalized()?;
332 let range = range_bounds_to_file_range(&source_range)?;
333 if let Some(ref r) = range {
334 let span = tracing::Span::current();
335 span.record("range_start", r.start);
336 span.record("range_end", r.end);
337 }
338 let id = UniqueId::new();
339 let name = Arc::from("");
340 let progress_updater = self.progress.new_item(id, name);
341 let reconstructor = self.setup_reconstructor(file_info, range, Some(progress_updater))?;
342 let n_bytes = reconstructor.reconstruct_to_writer(writer).await?;
343
344 let expected_size = match range {
345 Some(r) if r.end < u64::MAX => Some(r.end - r.start),
346 None => file_info.file_size(),
347 _ => None,
348 };
349 if let Some(expected) = expected_size
350 && n_bytes != expected
351 {
352 return Err(DataError::SizeMismatch {
353 expected,
354 actual: n_bytes,
355 });
356 }
357
358 Ok((id, n_bytes))
359 }
360}
361
362fn range_bounds_to_file_range(range: &impl RangeBounds<u64>) -> Result<Option<FileRange>> {
370 let start = match range.start_bound() {
371 Bound::Included(&s) => s,
372 Bound::Excluded(&s) => s.saturating_add(1),
373 Bound::Unbounded => 0,
374 };
375 let end = match range.end_bound() {
376 Bound::Included(&e) => e.saturating_add(1),
377 Bound::Excluded(&e) => e,
378 Bound::Unbounded => u64::MAX,
379 };
380 if start > end {
381 return Err(DataError::InvalidOperation(format!("Invalid range: start ({start}) > end ({end})")));
382 }
383 if start == 0 && end == u64::MAX {
384 Ok(None)
385 } else {
386 Ok(Some(FileRange::new(start, end)))
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use std::fs::{read, write};
393 use std::io::{Seek, SeekFrom};
394 use std::sync::{Arc, OnceLock};
395
396 use tempfile::tempdir;
397 use xet_runtime::core::XetContext;
398
399 use super::*;
400 use crate::processing::configurations::TranslatorConfig;
401 use crate::processing::file_cleaner::Sha256Policy;
402 use crate::processing::{FileUploadSession, XetFileInfo};
403
404 fn get_runtime() -> Arc<xet_runtime::core::XetRuntime> {
405 static THREADPOOL: OnceLock<Arc<xet_runtime::core::XetRuntime>> = OnceLock::new();
406 THREADPOOL
407 .get_or_init(|| {
408 XetContext::default()
409 .expect("Error starting multithreaded runtime.")
410 .runtime
411 .clone()
412 })
413 .clone()
414 }
415
416 async fn upload_data(cas_path: &Path, data: &[u8]) -> XetFileInfo {
417 let ctx = XetContext::default().unwrap();
418 let upload_session = FileUploadSession::new(TranslatorConfig::local_config(&ctx, cas_path).unwrap().into())
419 .await
420 .unwrap();
421
422 let (_id, mut cleaner) = upload_session
423 .start_clean(Some("test".into()), Some(data.len() as u64), Sha256Policy::Compute)
424 .unwrap();
425 cleaner.add_data(data).await.unwrap();
426 let (xfi, _metrics) = cleaner.finish().await.unwrap();
427 upload_session.finalize().await.unwrap();
428 xfi
429 }
430
431 #[test]
432 fn test_download_file() {
433 let runtime = get_runtime();
434 runtime
435 .bridge_sync(async {
436 let temp = tempdir().unwrap();
437 let cas_path = temp.path().join("cas");
438 let original_data = b"Hello, download session!";
439
440 let xfi = upload_data(&cas_path, original_data).await;
441
442 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
443 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
444
445 let out_path = temp.path().join("output.txt");
446 let (_id, n_bytes) = session.download_file(&xfi, &out_path).await.unwrap();
447
448 assert_eq!(n_bytes, original_data.len() as u64);
449 assert_eq!(read(&out_path).unwrap(), original_data);
450 })
451 .unwrap();
452 }
453
454 #[test]
455 fn test_download_file_creates_parent_dirs() {
456 let runtime = get_runtime();
457 runtime
458 .bridge_sync(async {
459 let temp = tempdir().unwrap();
460 let cas_path = temp.path().join("cas");
461 let original_data = b"nested directory test";
462
463 let xfi = upload_data(&cas_path, original_data).await;
464
465 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
466 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
467
468 let out_path = temp.path().join("deep").join("nested").join("dir").join("output.txt");
469 assert!(!out_path.parent().unwrap().exists());
470
471 session.download_file(&xfi, &out_path).await.unwrap();
472
473 assert_eq!(read(&out_path).unwrap(), original_data);
474 })
475 .unwrap();
476 }
477
478 #[test]
479 fn test_download_to_writer() {
480 let runtime = get_runtime();
481 runtime
482 .bridge_sync(async {
483 let temp = tempdir().unwrap();
484 let cas_path = temp.path().join("cas");
485 let original_data = b"0123456789abcdef";
486
487 let xfi = upload_data(&cas_path, original_data).await;
488
489 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
490 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
491
492 let out_path = temp.path().join("partial_writer.txt");
493 write(&out_path, vec![0u8; original_data.len()]).unwrap();
494
495 let mut file = std::fs::OpenOptions::new().write(true).open(&out_path).unwrap();
496 file.seek(SeekFrom::Start(4)).unwrap();
497
498 let (_id, n_bytes) = session.download_to_writer(&xfi, 4..12, file).await.unwrap();
499
500 assert_eq!(n_bytes, 8);
501 let result = read(&out_path).unwrap();
502 assert_eq!(&result[4..12], &original_data[4..12]);
503 })
504 .unwrap();
505 }
506
507 #[test]
508 fn test_download_to_writer_parallel_partitioned_file() {
509 let runtime = get_runtime();
510 runtime
511 .bridge_sync(async {
512 let temp = tempdir().unwrap();
513 let cas_path = temp.path().join("cas");
514 let original_data = b"abcdefghijklmnopqrstuvwxyz0123456789";
515
516 let xfi = upload_data(&cas_path, original_data).await;
517 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
518 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
519
520 let out_path = temp.path().join("partitioned.txt");
521 write(&out_path, vec![0u8; original_data.len()]).unwrap();
522
523 let n_parts = 5u64;
524 let total = original_data.len() as u64;
525 let mut tasks = Vec::new();
526
527 for idx in 0..n_parts {
528 let start = (idx * total) / n_parts;
529 let end = ((idx + 1) * total) / n_parts;
530 if start == end {
531 continue;
532 }
533
534 let session = session.clone();
535 let xfi = xfi.clone();
536 let out_path = out_path.clone();
537 tasks.push(tokio::spawn(async move {
538 let mut writer = std::fs::OpenOptions::new().write(true).open(out_path).unwrap();
539 writer.seek(SeekFrom::Start(start)).unwrap();
540 session.download_to_writer(&xfi, start..end, writer).await
541 }));
542 }
543
544 for task in tasks {
545 task.await.unwrap().unwrap();
546 }
547
548 let result = read(&out_path).unwrap();
549 assert_eq!(result, original_data);
550 })
551 .unwrap();
552 }
553
554 #[test]
555 fn test_download_multiple_files_concurrent() {
556 let runtime = get_runtime();
557 runtime
558 .bridge_sync(async {
559 let temp = tempdir().unwrap();
560 let cas_path = temp.path().join("cas");
561
562 let data_a = b"File A content for concurrent test";
563 let data_b = b"File B content for concurrent test - different";
564
565 let xfi_a = upload_data(&cas_path, data_a).await;
566 let xfi_b = upload_data(&cas_path, data_b).await;
567
568 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
569 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
570
571 let out_a = temp.path().join("out_a.txt");
572 let out_b = temp.path().join("out_b.txt");
573
574 let session_a = session.clone();
575 let xfi_a_clone = xfi_a.clone();
576 let out_a_clone = out_a.clone();
577 let task_a = tokio::spawn(async move { session_a.download_file(&xfi_a_clone, &out_a_clone).await });
578
579 let session_b = session.clone();
580 let xfi_b_clone = xfi_b.clone();
581 let out_b_clone = out_b.clone();
582 let task_b = tokio::spawn(async move { session_b.download_file(&xfi_b_clone, &out_b_clone).await });
583
584 task_a.await.unwrap().unwrap();
585 task_b.await.unwrap().unwrap();
586
587 assert_eq!(read(&out_a).unwrap(), data_a);
588 assert_eq!(read(&out_b).unwrap(), data_b);
589 })
590 .unwrap();
591 }
592
593 #[test]
596 fn test_download_stream_async() {
597 let runtime = get_runtime();
598 runtime
599 .bridge_sync(async {
600 let temp = tempdir().unwrap();
601 let cas_path = temp.path().join("cas");
602 let original_data = b"Hello, streaming download!";
603
604 let xfi = upload_data(&cas_path, original_data).await;
605
606 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
607 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
608
609 let (_id, mut stream) = session.download_stream(&xfi, None).await.unwrap();
610
611 let mut collected = Vec::new();
612 while let Some(chunk) = stream.next().await.unwrap() {
613 collected.extend_from_slice(&chunk);
614 }
615
616 assert_eq!(collected, original_data);
617 })
618 .unwrap();
619 }
620
621 #[test]
622 fn test_download_stream_blocking() {
623 let runtime = get_runtime();
624 runtime
625 .bridge_sync(async {
626 let temp = tempdir().unwrap();
627 let cas_path = temp.path().join("cas");
628 let original_data = b"Blocking stream test data";
629
630 let xfi = upload_data(&cas_path, original_data).await;
631
632 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
633 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
634
635 let (_id, stream) = session.download_stream(&xfi, None).await.unwrap();
636
637 let collected = tokio::task::spawn_blocking(move || {
638 let mut stream = stream;
639 let mut buf = Vec::new();
640 while let Some(chunk) = stream.blocking_next().unwrap() {
641 buf.extend_from_slice(&chunk);
642 }
643 buf
644 })
645 .await
646 .unwrap();
647
648 assert_eq!(collected, original_data);
649 })
650 .unwrap();
651 }
652
653 #[test]
654 fn test_download_stream_returns_none_after_finish() {
655 let runtime = get_runtime();
656 runtime
657 .bridge_sync(async {
658 let temp = tempdir().unwrap();
659 let cas_path = temp.path().join("cas");
660 let original_data = b"Extra none calls";
661
662 let xfi = upload_data(&cas_path, original_data).await;
663
664 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
665 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
666
667 let (_id, mut stream) = session.download_stream(&xfi, None).await.unwrap();
668
669 while stream.next().await.unwrap().is_some() {}
670
671 assert!(stream.next().await.unwrap().is_none());
673 assert!(stream.next().await.unwrap().is_none());
674 })
675 .unwrap();
676 }
677
678 #[test]
679 fn test_download_stream_multiple_concurrent() {
680 let runtime = get_runtime();
681 runtime
682 .bridge_sync(async {
683 let temp = tempdir().unwrap();
684 let cas_path = temp.path().join("cas");
685
686 let data_a = b"Stream A for concurrent download";
687 let data_b = b"Stream B for concurrent download - different";
688
689 let xfi_a = upload_data(&cas_path, data_a).await;
690 let xfi_b = upload_data(&cas_path, data_b).await;
691
692 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
693 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
694
695 let (_id_a, mut stream_a) = session.download_stream(&xfi_a, None).await.unwrap();
696 let (_id_b, mut stream_b) = session.download_stream(&xfi_b, None).await.unwrap();
697
698 let task_a = tokio::spawn(async move {
699 let mut buf = Vec::new();
700 while let Some(chunk) = stream_a.next().await.unwrap() {
701 buf.extend_from_slice(&chunk);
702 }
703 buf
704 });
705
706 let task_b = tokio::spawn(async move {
707 let mut buf = Vec::new();
708 while let Some(chunk) = stream_b.next().await.unwrap() {
709 buf.extend_from_slice(&chunk);
710 }
711 buf
712 });
713
714 let result_a = task_a.await.unwrap();
715 let result_b = task_b.await.unwrap();
716
717 assert_eq!(result_a, data_a);
718 assert_eq!(result_b, data_b);
719 })
720 .unwrap();
721 }
722
723 #[test]
724 fn test_drop_stream_without_reading() {
725 let runtime = get_runtime();
726 runtime
727 .bridge_sync(async {
728 let temp = tempdir().unwrap();
729 let cas_path = temp.path().join("cas");
730 let original_data = b"Drop-without-reading cleanup test";
731
732 let xfi = upload_data(&cas_path, original_data).await;
733
734 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
735 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
736
737 let (_id, stream) = session.download_stream(&xfi, None).await.unwrap();
738 drop(stream);
739 tokio::task::yield_now().await;
740
741 let out_path = temp.path().join("after_drop.txt");
742 session.download_file(&xfi, &out_path).await.unwrap();
743 assert_eq!(read(&out_path).unwrap(), original_data);
744 })
745 .unwrap();
746 }
747
748 #[test]
749 fn test_drop_stream_multiple_cycles_then_download() {
750 let runtime = get_runtime();
751 runtime
752 .bridge_sync(async {
753 let temp = tempdir().unwrap();
754 let cas_path = temp.path().join("cas");
755 let original_data = b"Multi-cycle drop cleanup test";
756
757 let xfi = upload_data(&cas_path, original_data).await;
758
759 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
760 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
761
762 for i in 0..5u32 {
763 let (_id, mut stream) = session.download_stream(&xfi, None).await.unwrap();
764 if i % 3 == 0 {
765 let _ = stream.next().await;
766 }
767 drop(stream);
768 tokio::task::yield_now().await;
769 }
770
771 let out_path = temp.path().join("after_cycles.txt");
772 session.download_file(&xfi, &out_path).await.unwrap();
773 assert_eq!(read(&out_path).unwrap(), original_data);
774 })
775 .unwrap();
776 }
777
778 #[test]
779 fn test_drop_stream_blocking_mid_read_then_download() {
780 let runtime = get_runtime();
781 runtime
782 .bridge_sync(async {
783 let temp = tempdir().unwrap();
784 let cas_path = temp.path().join("cas");
785 let original_data = b"Blocking drop cleanup test data";
786
787 let xfi = upload_data(&cas_path, original_data).await;
788
789 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
790 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
791
792 let (_id, stream) = session.download_stream(&xfi, None).await.unwrap();
793
794 tokio::task::spawn_blocking(move || {
795 let mut stream = stream;
796 let _chunk = stream.blocking_next().unwrap();
797 })
798 .await
799 .unwrap();
800
801 tokio::task::yield_now().await;
802
803 let out_path = temp.path().join("after_blocking_drop.txt");
804 session.download_file(&xfi, &out_path).await.unwrap();
805 assert_eq!(read(&out_path).unwrap(), original_data);
806 })
807 .unwrap();
808 }
809
810 #[test]
811 fn test_cancel_stream_before_start_returns_none() {
812 let runtime = get_runtime();
813 runtime
814 .bridge_sync(async {
815 let temp = tempdir().unwrap();
816 let cas_path = temp.path().join("cas");
817 let original_data = b"Cancel-before-start stream test";
818
819 let xfi = upload_data(&cas_path, original_data).await;
820
821 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
822 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
823
824 let (_id, mut stream) = session.download_stream(&xfi, None).await.unwrap();
825 stream.cancel();
826 assert!(stream.next().await.unwrap().is_none());
827 assert!(stream.next().await.unwrap().is_none());
828 })
829 .unwrap();
830 }
831
832 #[test]
833 fn test_cancel_stream_after_first_chunk_returns_none() {
834 let runtime = get_runtime();
835 runtime
836 .bridge_sync(async {
837 let temp = tempdir().unwrap();
838 let cas_path = temp.path().join("cas");
839 let original_data = b"Cancel-after-first-chunk stream test data";
840
841 let xfi = upload_data(&cas_path, original_data).await;
842
843 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
844 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
845
846 let (_id, mut stream) = session.download_stream(&xfi, None).await.unwrap();
847 let _ = stream.next().await.unwrap();
848 stream.cancel();
849 assert!(stream.next().await.unwrap().is_none());
850 assert!(stream.next().await.unwrap().is_none());
851
852 let out_path = temp.path().join("after_cancel.txt");
853 session.download_file(&xfi, &out_path).await.unwrap();
854 assert_eq!(read(&out_path).unwrap(), original_data);
855 })
856 .unwrap();
857 }
858
859 #[test]
862 fn test_download_to_writer_range_from() {
863 let runtime = get_runtime();
864 runtime
865 .clone()
866 .external_run_async_task(async {
867 let temp = tempdir().unwrap();
868 let cas_path = temp.path().join("cas");
869 let original_data = b"0123456789abcdef";
870
871 let xfi = upload_data(&cas_path, original_data).await;
872
873 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
874 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
875
876 let out_path = temp.path().join("range_from.bin");
877 let file = std::fs::File::create(&out_path).unwrap();
878 let (_id, n_bytes) = session.download_to_writer(&xfi, 4.., file).await.unwrap();
879
880 assert_eq!(n_bytes, 12);
881 assert_eq!(read(&out_path).unwrap(), &original_data[4..]);
882 })
883 .unwrap();
884 }
885
886 #[test]
887 fn test_download_to_writer_range_to() {
888 let runtime = get_runtime();
889 runtime
890 .clone()
891 .external_run_async_task(async {
892 let temp = tempdir().unwrap();
893 let cas_path = temp.path().join("cas");
894 let original_data = b"0123456789abcdef";
895
896 let xfi = upload_data(&cas_path, original_data).await;
897
898 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
899 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
900
901 let out_path = temp.path().join("range_to.bin");
902 let file = std::fs::File::create(&out_path).unwrap();
903 let (_id, n_bytes) = session.download_to_writer(&xfi, ..8, file).await.unwrap();
904
905 assert_eq!(n_bytes, 8);
906 assert_eq!(read(&out_path).unwrap(), &original_data[..8]);
907 })
908 .unwrap();
909 }
910
911 #[test]
912 fn test_download_to_writer_full_range() {
913 let runtime = get_runtime();
914 runtime
915 .clone()
916 .external_run_async_task(async {
917 let temp = tempdir().unwrap();
918 let cas_path = temp.path().join("cas");
919 let original_data = b"0123456789abcdef";
920
921 let xfi = upload_data(&cas_path, original_data).await;
922
923 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
924 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
925
926 let out_path = temp.path().join("full_range.bin");
927 let file = std::fs::File::create(&out_path).unwrap();
928 let (_id, n_bytes) = session.download_to_writer(&xfi, .., file).await.unwrap();
929
930 assert_eq!(n_bytes, original_data.len() as u64);
931 assert_eq!(read(&out_path).unwrap(), original_data);
932 })
933 .unwrap();
934 }
935
936 #[test]
937 fn test_download_to_writer_range_inclusive() {
938 let runtime = get_runtime();
939 runtime
940 .clone()
941 .external_run_async_task(async {
942 let temp = tempdir().unwrap();
943 let cas_path = temp.path().join("cas");
944 let original_data = b"0123456789abcdef";
945
946 let xfi = upload_data(&cas_path, original_data).await;
947
948 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
949 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
950
951 let out_path = temp.path().join("range_incl.bin");
952 let file = std::fs::File::create(&out_path).unwrap();
953 let (_id, n_bytes) = session.download_to_writer(&xfi, 2..=5, file).await.unwrap();
954
955 assert_eq!(n_bytes, 4);
956 assert_eq!(read(&out_path).unwrap(), &original_data[2..=5]);
957 })
958 .unwrap();
959 }
960
961 #[test]
964 fn test_download_stream_range_bounded() {
965 let runtime = get_runtime();
966 runtime
967 .clone()
968 .external_run_async_task(async {
969 let temp = tempdir().unwrap();
970 let cas_path = temp.path().join("cas");
971 let original_data = b"0123456789abcdef";
972
973 let xfi = upload_data(&cas_path, original_data).await;
974
975 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
976 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
977
978 let (_id, mut stream) = session.download_stream_range(&xfi, 4..12).await.unwrap();
979
980 let mut collected = Vec::new();
981 while let Some(chunk) = stream.next().await.unwrap() {
982 collected.extend_from_slice(&chunk);
983 }
984
985 assert_eq!(collected, &original_data[4..12]);
986 })
987 .unwrap();
988 }
989
990 #[test]
991 fn test_download_stream_range_from() {
992 let runtime = get_runtime();
993 runtime
994 .clone()
995 .external_run_async_task(async {
996 let temp = tempdir().unwrap();
997 let cas_path = temp.path().join("cas");
998 let original_data = b"0123456789abcdef";
999
1000 let xfi = upload_data(&cas_path, original_data).await;
1001
1002 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
1003 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
1004
1005 let (_id, mut stream) = session.download_stream_range(&xfi, 10..).await.unwrap();
1006
1007 let mut collected = Vec::new();
1008 while let Some(chunk) = stream.next().await.unwrap() {
1009 collected.extend_from_slice(&chunk);
1010 }
1011
1012 assert_eq!(collected, &original_data[10..]);
1013 })
1014 .unwrap();
1015 }
1016
1017 #[test]
1018 fn test_download_stream_range_to() {
1019 let runtime = get_runtime();
1020 runtime
1021 .clone()
1022 .external_run_async_task(async {
1023 let temp = tempdir().unwrap();
1024 let cas_path = temp.path().join("cas");
1025 let original_data = b"0123456789abcdef";
1026
1027 let xfi = upload_data(&cas_path, original_data).await;
1028
1029 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
1030 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
1031
1032 let (_id, mut stream) = session.download_stream_range(&xfi, ..6).await.unwrap();
1033
1034 let mut collected = Vec::new();
1035 while let Some(chunk) = stream.next().await.unwrap() {
1036 collected.extend_from_slice(&chunk);
1037 }
1038
1039 assert_eq!(collected, &original_data[..6]);
1040 })
1041 .unwrap();
1042 }
1043
1044 #[test]
1047 fn test_download_file_unknown_size() {
1048 let runtime = get_runtime();
1049 runtime
1050 .clone()
1051 .external_run_async_task(async {
1052 let temp = tempdir().unwrap();
1053 let cas_path = temp.path().join("cas");
1054 let original_data = b"File with unknown size test";
1055
1056 let xfi = upload_data(&cas_path, original_data).await;
1057 let xfi_no_size = XetFileInfo::new_hash_only(xfi.hash().to_string());
1058
1059 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
1060 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
1061
1062 let out_path = temp.path().join("output_unknown.txt");
1063 let (_id, n_bytes) = session.download_file(&xfi_no_size, &out_path).await.unwrap();
1064
1065 assert_eq!(n_bytes, original_data.len() as u64);
1066 assert_eq!(read(&out_path).unwrap(), original_data);
1067 })
1068 .unwrap();
1069 }
1070
1071 #[test]
1072 fn test_download_stream_unknown_size() {
1073 let runtime = get_runtime();
1074 runtime
1075 .clone()
1076 .external_run_async_task(async {
1077 let temp = tempdir().unwrap();
1078 let cas_path = temp.path().join("cas");
1079 let original_data = b"Stream with unknown size test";
1080
1081 let xfi = upload_data(&cas_path, original_data).await;
1082 let xfi_no_size = XetFileInfo::new_hash_only(xfi.hash().to_string());
1083
1084 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
1085 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
1086
1087 let (_id, mut stream) = session.download_stream(&xfi_no_size, None).await.unwrap();
1088
1089 let mut collected = Vec::new();
1090 while let Some(chunk) = stream.next().await.unwrap() {
1091 collected.extend_from_slice(&chunk);
1092 }
1093
1094 assert_eq!(collected, original_data);
1095 })
1096 .unwrap();
1097 }
1098
1099 #[cfg(not(debug_assertions))]
1100 #[test]
1101 fn test_download_file_size_mismatch_error() {
1102 let runtime = get_runtime();
1103 runtime
1104 .clone()
1105 .external_run_async_task(async {
1106 let temp = tempdir().unwrap();
1107 let cas_path = temp.path().join("cas");
1108 let original_data = b"Size mismatch test data";
1109
1110 let xfi = upload_data(&cas_path, original_data).await;
1111 let wrong_size_xfi = XetFileInfo::new(xfi.hash().to_string(), 999);
1112
1113 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
1114 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
1115
1116 let out_path = temp.path().join("output_mismatch.txt");
1117 let err = session.download_file(&wrong_size_xfi, &out_path).await.unwrap_err();
1118
1119 assert!(
1120 matches!(err, DataError::SizeMismatch { expected: 999, .. }),
1121 "Expected SizeMismatch error, got: {err:?}"
1122 );
1123 })
1124 .unwrap();
1125 }
1126
1127 #[test]
1130 fn test_range_bounds_conversion() {
1131 use super::range_bounds_to_file_range;
1132
1133 assert_eq!(range_bounds_to_file_range(&(..)).unwrap(), None);
1134 assert_eq!(range_bounds_to_file_range(&(0..100)).unwrap(), Some(FileRange::new(0, 100)));
1135 assert_eq!(range_bounds_to_file_range(&(5..)).unwrap(), Some(FileRange::new(5, u64::MAX)));
1136 assert_eq!(range_bounds_to_file_range(&(..50)).unwrap(), Some(FileRange::new(0, 50)));
1137 assert_eq!(range_bounds_to_file_range(&(10..=19)).unwrap(), Some(FileRange::new(10, 20)));
1138 }
1139
1140 #[test]
1141 fn test_range_bounds_inverted_range_errors() {
1142 use super::range_bounds_to_file_range;
1143
1144 let result = range_bounds_to_file_range(&(10..5));
1145 assert!(result.is_err());
1146 }
1147
1148 #[test]
1149 fn test_download_to_writer_empty_range() {
1150 let runtime = get_runtime();
1151 runtime
1152 .clone()
1153 .external_run_async_task(async {
1154 let temp = tempdir().unwrap();
1155 let cas_path = temp.path().join("cas");
1156 let original_data = b"0123456789abcdef";
1157
1158 let xfi = upload_data(&cas_path, original_data).await;
1159
1160 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
1161 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
1162
1163 let out_path = temp.path().join("empty_range.bin");
1164 let file = std::fs::File::create(&out_path).unwrap();
1165 let (_id, n_bytes) = session.download_to_writer(&xfi, 5..5, file).await.unwrap();
1166
1167 assert_eq!(n_bytes, 0);
1168 assert_eq!(read(&out_path).unwrap(), &[] as &[u8]);
1169 })
1170 .unwrap();
1171 }
1172
1173 #[test]
1174 fn test_download_to_writer_inverted_range_errors() {
1175 let runtime = get_runtime();
1176 runtime
1177 .clone()
1178 .external_run_async_task(async {
1179 let temp = tempdir().unwrap();
1180 let cas_path = temp.path().join("cas");
1181 let original_data = b"0123456789abcdef";
1182
1183 let xfi = upload_data(&cas_path, original_data).await;
1184
1185 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
1186 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
1187
1188 let out_path = temp.path().join("inverted_range.bin");
1189 let file = std::fs::File::create(&out_path).unwrap();
1190 let result = session.download_to_writer(&xfi, 10..5, file).await;
1191
1192 assert!(result.is_err());
1193 })
1194 .unwrap();
1195 }
1196
1197 #[cfg(not(debug_assertions))]
1198 #[test]
1199 fn test_download_to_writer_range_start_beyond_file_size_errors() {
1200 let runtime = get_runtime();
1201 runtime
1202 .clone()
1203 .external_run_async_task(async {
1204 let temp = tempdir().unwrap();
1205 let cas_path = temp.path().join("cas");
1206 let original_data = b"0123456789abcdef";
1207
1208 let xfi = upload_data(&cas_path, original_data).await;
1209
1210 let config = TranslatorConfig::local_config(&XetContext::default().unwrap(), &cas_path).unwrap();
1211 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
1212
1213 let out_path = temp.path().join("beyond_size.bin");
1214 let file = std::fs::File::create(&out_path).unwrap();
1215 let result = session.download_to_writer(&xfi, 100000.., file).await;
1216
1217 assert!(result.is_err());
1218 })
1219 .unwrap();
1220 }
1221}