1use std::borrow::Cow;
2use std::collections::HashMap;
3#[cfg(not(target_family = "wasm"))]
4use std::fs::File;
5#[cfg(not(target_family = "wasm"))]
6use std::io::Read;
7use std::mem::{swap, take};
8#[cfg(not(target_family = "wasm"))]
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11use std::sync::atomic::{AtomicBool, Ordering};
12
13#[cfg(not(target_family = "wasm"))]
14use bytes::Bytes;
15use more_asserts::*;
16use tokio::sync::Mutex;
17use tokio::task::{JoinHandle, JoinSet};
18#[cfg(target_family = "wasm")]
19use tokio_with_wasm::alias as tokio;
20#[cfg(not(target_family = "wasm"))]
21use tracing::Span;
22use tracing::{Instrument, info_span, instrument};
23use xet_client::cas_client::{Client, ProgressCallback};
24use xet_core_structures::metadata_shard::file_structs::MDBFileInfo;
25use xet_core_structures::xorb_object::SerializedXorbObject;
26use xet_runtime::core::XetContext;
27use xet_runtime::utils::UniqueId;
28
29use super::XetFileInfo;
30use super::configurations::TranslatorConfig;
31use super::file_cleaner::{Sha256Policy, SingleFileCleaner};
32use super::remote_client_interface::create_remote_client;
33use super::shard_interface::SessionShardInterface;
34use crate::deduplication::constants::{MAX_XORB_BYTES, MAX_XORB_CHUNKS};
35use crate::deduplication::{DataAggregator, DeduplicationMetrics, RawXorbData};
36use crate::error::{DataError, Result};
37use crate::progress_tracking::upload_tracking::{CompletionTracker, FileXorbDependency};
38use crate::progress_tracking::{GroupProgressReport, ItemProgressReport, UploadGroupProgress};
39
40pub struct FileUploadSession {
47 pub(crate) ctx: XetContext,
48 pub(crate) client: Arc<dyn Client + Send + Sync>,
49 pub(crate) shard_interface: SessionShardInterface,
50
51 pub(crate) completion_tracker: Arc<CompletionTracker>,
53
54 current_session_data: Mutex<DataAggregator>,
56
57 deduplication_metrics: Mutex<DeduplicationMetrics>,
59
60 xorb_upload_tasks: Mutex<JoinSet<Result<()>>>,
62
63 finalized: AtomicBool,
65}
66
67impl FileUploadSession {
69 pub async fn new(config: Arc<TranslatorConfig>) -> Result<Arc<FileUploadSession>> {
70 FileUploadSession::new_impl(config, false).await
71 }
72
73 pub async fn dry_run(config: Arc<TranslatorConfig>) -> Result<Arc<FileUploadSession>> {
74 FileUploadSession::new_impl(config, true).await
75 }
76
77 async fn new_impl(config: Arc<TranslatorConfig>, dry_run: bool) -> Result<Arc<FileUploadSession>> {
78 let ctx = config.ctx.clone();
79 let session_id = config
80 .session
81 .session_id
82 .as_ref()
83 .map(Cow::Borrowed)
84 .unwrap_or_else(|| Cow::Owned(UniqueId::new().to_string()));
85
86 let progress = UploadGroupProgress::with_speed_config(
87 ctx.config.data.progress_update_speed_sampling_window,
88 ctx.config.data.progress_update_speed_min_observations,
89 );
90 let completion_tracker = Arc::new(CompletionTracker::new(progress));
91
92 let client = create_remote_client(&config, &session_id, dry_run).await?;
93
94 #[cfg(target_family = "wasm")]
95 let shard_interface = SessionShardInterface::new(&ctx, config.clone(), client.clone(), dry_run).await?;
96
97 #[cfg(not(target_family = "wasm"))]
98 let shard_interface =
99 SessionShardInterface::new(&ctx, config.clone(), client.clone(), completion_tracker.clone(), dry_run)
100 .await?;
101
102 Ok(Arc::new(Self {
103 ctx,
104 shard_interface,
105 client,
106 completion_tracker,
107 current_session_data: Mutex::new(DataAggregator::default()),
108 deduplication_metrics: Mutex::new(DeduplicationMetrics::default()),
109 xorb_upload_tasks: Mutex::new(JoinSet::new()),
110 finalized: AtomicBool::new(false),
111 }))
112 }
113
114 #[cfg(not(target_family = "wasm"))]
115 pub async fn upload_files(
116 self: &Arc<Self>,
117 files_and_sha256: impl IntoIterator<Item = (impl AsRef<Path>, Sha256Policy)> + Send,
118 ) -> Result<Vec<XetFileInfo>> {
119 self.check_not_finalized()?;
120 let mut cleaning_tasks: Vec<JoinHandle<_>> = vec![];
121
122 for (f, sha256) in files_and_sha256.into_iter() {
123 let file_path = f.as_ref().to_owned();
124 let file_name: Arc<str> = Arc::from(file_path.to_string_lossy());
125
126 let file_size = std::fs::metadata(&file_path)?.len();
127
128 let file_id =
129 self.completion_tracker
130 .register_new_file(UniqueId::new(), file_name.clone(), Some(file_size));
131
132 let ingestion_concurrency_limiter = self.ctx.common.file_ingestion_semaphore.clone();
133 let ingestion_block_size = *self.ctx.config.data.ingestion_block_size;
134 let session = self.clone();
135
136 cleaning_tasks.push(tokio::spawn(async move {
137 let span = info_span!(
139 "clean_file_task",
140 "file.name" = file_name.to_string(),
141 "file.len" = file_size,
142 "file.new_bytes" = tracing::field::Empty,
143 "file.deduped_bytes" = tracing::field::Empty,
144 "file.defrag_prevented_dedup_bytes" = tracing::field::Empty,
145 "file.new_chunks" = tracing::field::Empty,
146 "file.deduped_chunks" = tracing::field::Empty,
147 "file.defrag_prevented_dedup_chunks" = tracing::field::Empty,
148 );
149 let _processing_permit = ingestion_concurrency_limiter.acquire().await?;
151
152 async move {
153 let mut reader = File::open(&file_path)?;
154
155 let mut cleaner = SingleFileCleaner::new(Some(file_name), file_id, sha256, session);
157 let mut bytes_read = 0;
158
159 while bytes_read < file_size {
160 let bytes_left = file_size - bytes_read;
162 let n_bytes_read = ingestion_block_size.min(bytes_left) as usize;
163
164 let mut buffer = Vec::with_capacity(n_bytes_read);
173 #[allow(clippy::uninit_vec)]
174 unsafe {
175 buffer.set_len(n_bytes_read);
176 }
177
178 reader.read_exact(&mut buffer)?;
180
181 bytes_read += buffer.len() as u64;
182
183 cleaner.add_data_from_bytes(Bytes::from(buffer)).await?;
184 }
185
186 let (xfi, metrics) = cleaner.finish().await?;
188
189 let span = Span::current();
191 span.record("file.new_bytes", metrics.new_bytes);
192 span.record("file.deduped_bytes", metrics.deduped_bytes);
193 span.record("file.defrag_prevented_dedup_bytes", metrics.defrag_prevented_dedup_bytes);
194 span.record("file.new_chunks", metrics.new_chunks);
195 span.record("file.deduped_chunks", metrics.deduped_chunks);
196 span.record("file.defrag_prevented_dedup_chunks", metrics.defrag_prevented_dedup_chunks);
197
198 Result::Ok(xfi)
199 }
200 .instrument(span)
201 .await
202 }));
203 }
204
205 let mut ret = Vec::with_capacity(cleaning_tasks.len());
207
208 for task in cleaning_tasks {
209 ret.push(task.await??);
210 }
211
212 Ok(ret)
213 }
214
215 pub fn start_clean(
226 self: &Arc<Self>,
227 tracking_name: Option<Arc<str>>,
228 size: Option<u64>,
229 sha256: Sha256Policy,
230 ) -> Result<(UniqueId, SingleFileCleaner)> {
231 self.check_not_finalized()?;
232 let id = UniqueId::new();
233 let cleaner = self.start_clean_with_id(id, tracking_name, size, sha256);
234 Ok((id, cleaner))
235 }
236
237 fn start_clean_with_id(
238 self: &Arc<Self>,
239 id: UniqueId,
240 tracking_name: Option<Arc<str>>,
241 size: Option<u64>,
242 sha256: Sha256Policy,
243 ) -> SingleFileCleaner {
244 let file_id = self
245 .completion_tracker
246 .register_new_file(id, tracking_name.clone().unwrap_or_default(), size);
247 SingleFileCleaner::new(tracking_name, file_id, sha256, self.clone())
248 }
249
250 #[cfg(not(target_family = "wasm"))]
254 pub async fn spawn_upload_from_path(
255 self: &Arc<Self>,
256 file_path: PathBuf,
257 sha256: Sha256Policy,
258 ) -> Result<(UniqueId, JoinHandle<Result<(XetFileInfo, DeduplicationMetrics)>>)> {
259 self.check_not_finalized()?;
260 let file_size = std::fs::metadata(&file_path)?.len();
261 let tracking_name: Arc<str> = Arc::from(file_path.to_string_lossy().as_ref());
262 let (id, cleaner) = self.start_clean(Some(tracking_name), Some(file_size), sha256)?;
263
264 let session = self.clone();
265 let runtime = self.ctx.runtime.clone();
266 let semaphore = self.ctx.common.file_ingestion_semaphore.clone();
267 let handle = runtime.spawn(async move {
268 let _permit = semaphore.acquire().await?;
269 Self::feed_file_to_cleaner(&session, cleaner, &file_path).await
270 });
271
272 Ok((id, handle))
273 }
274
275 pub async fn spawn_upload_bytes(
279 self: &Arc<Self>,
280 bytes: Vec<u8>,
281 sha256: Sha256Policy,
282 tracking_name: Option<Arc<str>>,
283 ) -> Result<(UniqueId, JoinHandle<Result<(XetFileInfo, DeduplicationMetrics)>>)> {
284 self.check_not_finalized()?;
285 let (id, mut cleaner) = self.start_clean(tracking_name, Some(bytes.len() as u64), sha256)?;
286
287 let semaphore = self.ctx.common.file_ingestion_semaphore.clone();
288 #[cfg(not(target_family = "wasm"))]
293 let handle = self.ctx.runtime.spawn(async move {
294 let _permit = semaphore.acquire().await?;
295 cleaner.add_data(&bytes).await?;
296 cleaner.finish().await
297 });
298 #[cfg(target_family = "wasm")]
299 let handle = tokio::task::spawn(async move {
300 let _permit = semaphore.acquire().await?;
301 cleaner.add_data(&bytes).await?;
302 cleaner.finish().await
303 });
304
305 Ok((id, handle))
306 }
307
308 #[cfg(not(target_family = "wasm"))]
309 async fn feed_file_to_cleaner(
310 _session: &Arc<Self>,
311 mut cleaner: SingleFileCleaner,
312 file_path: &Path,
313 ) -> Result<(XetFileInfo, DeduplicationMetrics)> {
314 let mut reader = File::open(file_path)?;
315 let filesize = reader.metadata()?.len();
316 let mut buffer = vec![0u8; u64::min(filesize, *_session.ctx.config.data.ingestion_block_size) as usize];
317
318 loop {
319 let n = reader.read(&mut buffer)?;
320 if n == 0 {
321 break;
322 }
323 cleaner.add_data(&buffer[..n]).await?;
324 }
325 cleaner.finish().await
326 }
327
328 #[instrument(skip_all, name="FileUploadSession::register_new_xorb_for_upload", fields(xorb_len = xorb.num_bytes()))]
331 pub(crate) async fn register_new_xorb(
332 self: &Arc<Self>,
333 xorb: RawXorbData,
334 file_dependencies: &[FileXorbDependency],
335 ) -> Result<bool> {
336 {
338 let mut upload_tasks = self.xorb_upload_tasks.lock().await;
339 while let Some(result) = upload_tasks.try_join_next() {
340 result??;
341 }
342 }
343
344 let xorb_hash = xorb.hash();
345
346 let xorb_is_new = self.completion_tracker.register_new_xorb(xorb_hash, xorb.num_bytes() as u64);
352
353 self.completion_tracker.register_dependencies(file_dependencies);
356
357 if !xorb_is_new {
358 return Ok(false);
359 }
360
361 if xorb.num_bytes() == 0 {
364 self.completion_tracker.register_xorb_upload_completion(xorb_hash);
365 return Ok(true);
366 }
367
368 let xorb_info = Arc::new(xorb.xorb_info.clone());
372 self.shard_interface.add_xorb_block(xorb_info.clone()).await?;
373
374 let runtime = self.ctx.runtime.clone();
377 let compression_policy = self.ctx.config.xorb.compression_policy.clone();
378 let compression_scheme_retest_interval = self.ctx.config.xorb.compression_scheme_retest_interval;
379 let xorb_obj = runtime
380 .spawn_blocking(move || {
381 SerializedXorbObject::from_xorb(
382 xorb,
383 false,
384 compression_policy.as_str(),
385 compression_scheme_retest_interval,
386 )
387 })
388 .await??;
389
390 let session = self.clone();
391 let upload_permit = self.client.acquire_upload_permit().await?;
392 let cas_prefix = self.ctx.config.data.default_prefix.clone();
393 let completion_tracker = self.completion_tracker.clone();
394 let xorb_hash = xorb_obj.hash;
395 let raw_num_bytes = xorb_obj.raw_num_bytes;
396 let progress_callback: ProgressCallback = Arc::new(move |delta, _completed, total| {
397 let raw_delta = (delta * raw_num_bytes).checked_div(total).unwrap_or(0);
398 if raw_delta > 0 {
399 completion_tracker
400 .clone()
401 .register_xorb_upload_progress_background(xorb_hash, raw_delta);
402 }
403 });
404
405 self.xorb_upload_tasks.lock().await.spawn(
406 async move {
407 let n_bytes_transmitted = session
408 .client
409 .upload_xorb(&cas_prefix, xorb_obj, Some(progress_callback), upload_permit)
410 .await?;
411
412 session.completion_tracker.register_xorb_upload_completion(xorb_hash);
414
415 session.deduplication_metrics.lock().await.xorb_bytes_uploaded += n_bytes_transmitted;
417
418 session.shard_interface.add_uploaded_xorb_block(xorb_info).await?;
420
421 Ok(())
422 }
423 .instrument(info_span!("FileUploadSession::upload_xorb_task", xorb.hash = xorb_hash.hex())),
424 );
425
426 Ok(true)
427 }
428
429 #[instrument(skip_all, name="FileUploadSession::register_single_file_clean_completion", fields(num_bytes = file_data.num_bytes(), num_chunks = file_data.num_chunks()))]
431 pub(crate) async fn register_single_file_clean_completion(
432 self: &Arc<Self>,
433 mut file_data: DataAggregator,
434 dedup_metrics: &DeduplicationMetrics,
435 ) -> Result<()> {
436 {
438 let mut current_session_data = self.current_session_data.lock().await;
439
440 #[cfg(feature = "simulation")]
441 let xorb_cut_bytes = self
442 .ctx
443 .config
444 .xorb
445 .simulation_max_bytes
446 .map(|bs| bs.as_u64().min(*MAX_XORB_BYTES as u64) as usize)
447 .unwrap_or(*MAX_XORB_BYTES);
448 #[cfg(not(feature = "simulation"))]
449 let xorb_cut_bytes = *MAX_XORB_BYTES;
450 #[cfg(feature = "simulation")]
451 let xorb_cut_chunks = self
452 .ctx
453 .config
454 .xorb
455 .simulation_max_chunks
456 .unwrap_or(*MAX_XORB_CHUNKS)
457 .min(*MAX_XORB_CHUNKS);
458 #[cfg(not(feature = "simulation"))]
459 let xorb_cut_chunks = *MAX_XORB_CHUNKS;
460
461 if current_session_data.num_bytes() + file_data.num_bytes() > xorb_cut_bytes
463 || current_session_data.num_chunks() + file_data.num_chunks() > xorb_cut_chunks
464 {
465 if current_session_data.num_bytes() > file_data.num_bytes() {
467 swap(&mut *current_session_data, &mut file_data);
468 }
469
470 debug_assert_le!(current_session_data.num_bytes(), file_data.num_bytes());
472
473 drop(current_session_data);
475
476 self.process_aggregated_data_as_xorb(file_data).await?;
477 } else {
478 current_session_data.merge_in(file_data);
479 }
480 }
481
482 #[cfg(debug_assertions)]
483 {
484 let current_session_data = self.current_session_data.lock().await;
485 debug_assert_le!(current_session_data.num_bytes(), *MAX_XORB_BYTES);
486 debug_assert_le!(current_session_data.num_chunks(), *MAX_XORB_CHUNKS);
487 }
488
489 self.deduplication_metrics.lock().await.merge_in(dedup_metrics);
491
492 Ok(())
493 }
494
495 pub(crate) async fn register_single_file_clean_completion_detached(
499 self: &Arc<Self>,
500 file_data: DataAggregator,
501 dedup_metrics: &DeduplicationMetrics,
502 ) -> Result<MDBFileInfo> {
503 let file_infos = self.process_aggregated_data_as_xorb_detached(file_data).await?;
506
507 self.deduplication_metrics.lock().await.merge_in(dedup_metrics);
508
509 debug_assert_eq!(file_infos.len(), 1);
510 file_infos
511 .into_iter()
512 .next()
513 .ok_or_else(|| DataError::InternalError("detached completion produced no file info".into()))
514 }
515
516 async fn process_aggregated_data_as_xorb(self: &Arc<Self>, data_agg: DataAggregator) -> Result<()> {
518 self.process_aggregated_data_as_xorb_impl(data_agg, true).await.map(|_| ())
519 }
520
521 async fn process_aggregated_data_as_xorb_detached(
524 self: &Arc<Self>,
525 data_agg: DataAggregator,
526 ) -> Result<Vec<MDBFileInfo>> {
527 self.process_aggregated_data_as_xorb_impl(data_agg, false).await
528 }
529
530 async fn process_aggregated_data_as_xorb_impl(
531 self: &Arc<Self>,
532 data_agg: DataAggregator,
533 register_files: bool,
534 ) -> Result<Vec<MDBFileInfo>> {
535 let (xorb, new_files) = data_agg.finalize();
536 let xorb_hash = xorb.hash();
537
538 debug_assert_le!(xorb.num_bytes(), *MAX_XORB_BYTES);
539 debug_assert_le!(xorb.data.len(), *MAX_XORB_CHUNKS);
540
541 let mut new_dependencies = Vec::with_capacity(new_files.len());
542 let mut file_infos = Vec::with_capacity(new_files.len());
543
544 for (file_id, fi, bytes_in_xorb) in new_files {
545 new_dependencies.push(FileXorbDependency {
546 file_id,
547 xorb_hash,
548 n_bytes: bytes_in_xorb,
549 is_external: false,
550 });
551
552 if register_files {
553 self.shard_interface.add_file_reconstruction_info(fi).await?;
554 } else {
555 file_infos.push(fi);
556 }
557 }
558
559 self.register_new_xorb(xorb, &new_dependencies).await?;
561
562 Ok(file_infos)
563 }
564
565 pub(crate) fn register_xorb_dependencies(self: &Arc<Self>, xorb_dependencies: &[FileXorbDependency]) {
567 self.completion_tracker.register_dependencies(xorb_dependencies);
568 }
569
570 #[instrument(skip_all, name="FileUploadSession::finalize", fields(session.id))]
572 async fn finalize_impl(
573 self: Arc<Self>,
574 return_files: bool,
575 ) -> Result<(DeduplicationMetrics, Vec<MDBFileInfo>, GroupProgressReport)> {
576 if self.finalized.swap(true, Ordering::AcqRel) {
577 return Err(DataError::InvalidOperation("FileUploadSession already finalized".to_string()));
578 }
579
580 let data_agg = take(&mut *self.current_session_data.lock().await);
582 self.process_aggregated_data_as_xorb(data_agg).await?;
583
584 let mut upload_tasks = take(&mut *self.xorb_upload_tasks.lock().await);
593
594 while let Some(result) = upload_tasks.join_next().await {
595 result??;
596 }
597
598 let mut metrics = take(&mut *self.deduplication_metrics.lock().await);
599
600 let all_file_info = if return_files {
601 self.shard_interface.session_file_info_list().await?
602 } else {
603 Vec::new()
604 };
605
606 metrics.shard_bytes_uploaded = self.shard_interface.upload_and_register_session_shards().await?;
609 metrics.total_bytes_uploaded = metrics.shard_bytes_uploaded + metrics.xorb_bytes_uploaded;
610
611 #[cfg(debug_assertions)]
612 {
613 self.completion_tracker.assert_complete();
614 }
615
616 let report = self.report();
617 Ok((metrics, all_file_info, report))
618 }
619
620 pub async fn checkpoint(self: &Arc<Self>) -> Result<()> {
625 let data_agg = take(&mut *self.current_session_data.lock().await);
627 self.process_aggregated_data_as_xorb(data_agg).await?;
628
629 {
631 let mut upload_tasks = self.xorb_upload_tasks.lock().await;
632
633 while let Some(result) = upload_tasks.join_next().await {
634 result??;
635 }
636 }
637
638 Ok(())
639 }
640
641 pub async fn register_composed_file(self: &Arc<Self>, file_info: MDBFileInfo) -> Result<()> {
645 self.check_not_finalized()?;
646 self.shard_interface.add_file_reconstruction_info(file_info).await
647 }
648
649 fn check_not_finalized(&self) -> Result<()> {
650 if self.finalized.load(Ordering::Acquire) {
651 return Err(DataError::InvalidOperation("FileUploadSession already finalized".to_string()));
652 }
653 Ok(())
654 }
655
656 pub fn client(&self) -> Arc<dyn Client + Send + Sync> {
657 Arc::clone(&self.client)
658 }
659
660 pub fn progress(&self) -> &Arc<UploadGroupProgress> {
661 self.completion_tracker.progress()
662 }
663
664 pub fn report(&self) -> GroupProgressReport {
665 self.completion_tracker.progress().report()
666 }
667
668 pub fn item_report(&self, id: UniqueId) -> Option<ItemProgressReport> {
669 self.completion_tracker.progress().item_report(id)
670 }
671
672 pub fn item_reports(&self) -> HashMap<UniqueId, ItemProgressReport> {
673 self.completion_tracker.progress().item_reports()
674 }
675
676 pub async fn finalize(self: Arc<Self>) -> Result<DeduplicationMetrics> {
677 Ok(self.finalize_impl(false).await?.0)
678 }
679
680 pub async fn finalize_with_report(self: Arc<Self>) -> Result<(DeduplicationMetrics, GroupProgressReport)> {
681 let (metrics, _file_info, report) = self.finalize_impl(false).await?;
682 Ok((metrics, report))
683 }
684
685 pub async fn finalize_with_file_info(self: Arc<Self>) -> Result<(DeduplicationMetrics, Vec<MDBFileInfo>)> {
686 let (metrics, file_info, _report) = self.finalize_impl(true).await?;
687 Ok((metrics, file_info))
688 }
689}
690
691#[cfg(all(test, not(target_family = "wasm")))]
692mod tests {
693 use std::fs::{File, OpenOptions};
694 use std::io::{Read, Write};
695 use std::path::Path;
696
697 use xet_runtime::core::XetContext;
698
699 use crate::processing::{FileDownloadSession, FileUploadSession, XetFileInfo};
700
701 async fn test_clean_file(cas_path: &Path, input_path: &Path, output_path: &Path) {
706 let read_data = read(input_path).unwrap().to_vec();
707
708 let mut pf_out = Box::new(
709 OpenOptions::new()
710 .create(true)
711 .write(true)
712 .truncate(true)
713 .open(output_path)
714 .unwrap(),
715 );
716
717 let ctx = XetContext::default().unwrap();
718 let upload_session = FileUploadSession::new(TranslatorConfig::local_config(&ctx, cas_path).unwrap().into())
719 .await
720 .unwrap();
721
722 let (_id, mut cleaner) = upload_session
723 .start_clean(Some("test".into()), Some(read_data.len() as u64), Sha256Policy::Compute)
724 .unwrap();
725
726 cleaner.add_data(&read_data[..]).await.unwrap();
728
729 let (xet_file_info, _metrics) = cleaner.finish().await.unwrap();
730 upload_session.finalize().await.unwrap();
731
732 pf_out
733 .write_all(serde_json::to_string(&xet_file_info).unwrap().as_bytes())
734 .unwrap();
735 }
736
737 async fn test_smudge_file(cas_path: &Path, pointer_path: &Path, output_path: &Path) {
742 let mut reader = File::open(pointer_path).unwrap();
743
744 let mut input = String::new();
745 reader.read_to_string(&mut input).unwrap();
746
747 let xet_file = serde_json::from_str::<XetFileInfo>(&input).unwrap();
748
749 let ctx = XetContext::default().unwrap();
750 let config = TranslatorConfig::local_config(&ctx, cas_path).unwrap();
751 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
752
753 let (_id, _n_bytes) = session.download_file(&xet_file, output_path).await.unwrap();
754 }
755
756 use std::fs::{read, write};
757
758 use tempfile::tempdir;
759
760 use super::*;
761
762 #[test]
763 fn test_clean_smudge_round_trip() {
764 let temp = tempdir().unwrap();
765 let original_data = b"Hello, world!";
766
767 let ctx = XetContext::default().unwrap();
768
769 ctx.runtime
770 .bridge_sync(async move {
771 let cas_path = temp.path().join("cas");
772
773 let original_path = temp.path().join("original.txt");
775 write(&original_path, original_data).unwrap();
776
777 let pointer_path = temp.path().join("pointer.txt");
779 test_clean_file(&cas_path, &original_path, &pointer_path).await;
780
781 let hydrated_path = temp.path().join("hydrated.txt");
783 test_smudge_file(&cas_path, &pointer_path, &hydrated_path).await;
784
785 let result_data = read(hydrated_path).unwrap();
787 assert_eq!(original_data.to_vec(), result_data);
788 })
789 .unwrap();
790 }
791
792 #[test]
793 fn test_clean_skip_sha256_no_metadata_ext() {
794 let temp = tempdir().unwrap();
795 let data = b"Hello, skip sha256!";
796
797 let ctx = XetContext::default().unwrap();
798
799 ctx.runtime
800 .bridge_sync(async move {
801 let cas_path = temp.path().join("cas");
802
803 let session_ctx = XetContext::default().unwrap();
804 let upload_session =
805 FileUploadSession::new(TranslatorConfig::local_config(&session_ctx, &cas_path).unwrap().into())
806 .await
807 .unwrap();
808
809 let (_id, mut cleaner) = upload_session
810 .start_clean(Some("test".into()), Some(data.len() as u64), Sha256Policy::Skip)
811 .unwrap();
812 cleaner.add_data(data).await.unwrap();
813 let _ = cleaner.finish().await.unwrap();
814
815 let (_metrics, file_infos) = upload_session.finalize_with_file_info().await.unwrap();
817 assert_eq!(file_infos.len(), 1);
818 assert!(file_infos[0].metadata_ext.is_none(), "Skip should produce no metadata_ext");
819 })
820 .unwrap();
821 }
822}