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::{GroupProgress, GroupProgressReport, ItemProgressReport};
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 progress: Arc<GroupProgress>,
56
57 current_session_data: Mutex<DataAggregator>,
59
60 deduplication_metrics: Mutex<DeduplicationMetrics>,
62
63 xorb_upload_tasks: Mutex<JoinSet<Result<()>>>,
65
66 finalized: AtomicBool,
68}
69
70impl FileUploadSession {
72 pub async fn new(config: Arc<TranslatorConfig>) -> Result<Arc<FileUploadSession>> {
73 FileUploadSession::new_impl(config, false).await
74 }
75
76 pub async fn dry_run(config: Arc<TranslatorConfig>) -> Result<Arc<FileUploadSession>> {
77 FileUploadSession::new_impl(config, true).await
78 }
79
80 async fn new_impl(config: Arc<TranslatorConfig>, dry_run: bool) -> Result<Arc<FileUploadSession>> {
81 let ctx = config.ctx.clone();
82 let session_id = config
83 .session
84 .session_id
85 .as_ref()
86 .map(Cow::Borrowed)
87 .unwrap_or_else(|| Cow::Owned(UniqueId::new().to_string()));
88
89 let progress = GroupProgress::with_speed_config(
90 ctx.config.data.progress_update_speed_sampling_window,
91 ctx.config.data.progress_update_speed_min_observations,
92 );
93 let completion_tracker = Arc::new(CompletionTracker::new(progress.clone()));
94
95 let client = create_remote_client(&config, &session_id, dry_run).await?;
96
97 let shard_interface = SessionShardInterface::new(&ctx, config.clone(), client.clone(), dry_run).await?;
98
99 Ok(Arc::new(Self {
100 ctx,
101 shard_interface,
102 client,
103 completion_tracker,
104 progress,
105 current_session_data: Mutex::new(DataAggregator::default()),
106 deduplication_metrics: Mutex::new(DeduplicationMetrics::default()),
107 xorb_upload_tasks: Mutex::new(JoinSet::new()),
108 finalized: AtomicBool::new(false),
109 }))
110 }
111
112 #[cfg(not(target_family = "wasm"))]
113 pub async fn upload_files(
114 self: &Arc<Self>,
115 files_and_sha256: impl IntoIterator<Item = (impl AsRef<Path>, Sha256Policy)> + Send,
116 ) -> Result<Vec<XetFileInfo>> {
117 self.check_not_finalized()?;
118 let mut cleaning_tasks: Vec<JoinHandle<_>> = vec![];
119
120 for (f, sha256) in files_and_sha256.into_iter() {
121 let file_path = f.as_ref().to_owned();
122 let file_name: Arc<str> = Arc::from(file_path.to_string_lossy());
123
124 let file_size = std::fs::metadata(&file_path)?.len();
125
126 let updater = self.progress.new_item(UniqueId::new(), file_name.clone());
127 let file_id = self.completion_tracker.register_new_file(updater, Some(file_size));
128
129 let ingestion_concurrency_limiter = self.ctx.common.file_ingestion_semaphore.clone();
130 let ingestion_block_size = *self.ctx.config.data.ingestion_block_size;
131 let session = self.clone();
132
133 cleaning_tasks.push(tokio::spawn(async move {
134 let span = info_span!(
136 "clean_file_task",
137 "file.name" = file_name.to_string(),
138 "file.len" = file_size,
139 "file.new_bytes" = tracing::field::Empty,
140 "file.deduped_bytes" = tracing::field::Empty,
141 "file.defrag_prevented_dedup_bytes" = tracing::field::Empty,
142 "file.new_chunks" = tracing::field::Empty,
143 "file.deduped_chunks" = tracing::field::Empty,
144 "file.defrag_prevented_dedup_chunks" = tracing::field::Empty,
145 );
146 let _processing_permit = ingestion_concurrency_limiter.acquire().await?;
148
149 async move {
150 let mut reader = File::open(&file_path)?;
151
152 let mut cleaner = SingleFileCleaner::new(Some(file_name), file_id, sha256, session);
154 let mut bytes_read = 0;
155
156 while bytes_read < file_size {
157 let bytes_left = file_size - bytes_read;
159 let n_bytes_read = ingestion_block_size.min(bytes_left) as usize;
160
161 let mut buffer = Vec::with_capacity(n_bytes_read);
170 #[allow(clippy::uninit_vec)]
171 unsafe {
172 buffer.set_len(n_bytes_read);
173 }
174
175 reader.read_exact(&mut buffer)?;
177
178 bytes_read += buffer.len() as u64;
179
180 cleaner.add_data_from_bytes(Bytes::from(buffer)).await?;
181 }
182
183 let (xfi, metrics) = cleaner.finish().await?;
185
186 let span = Span::current();
188 span.record("file.new_bytes", metrics.new_bytes);
189 span.record("file.deduped_bytes", metrics.deduped_bytes);
190 span.record("file.defrag_prevented_dedup_bytes", metrics.defrag_prevented_dedup_bytes);
191 span.record("file.new_chunks", metrics.new_chunks);
192 span.record("file.deduped_chunks", metrics.deduped_chunks);
193 span.record("file.defrag_prevented_dedup_chunks", metrics.defrag_prevented_dedup_chunks);
194
195 Result::Ok(xfi)
196 }
197 .instrument(span)
198 .await
199 }));
200 }
201
202 let mut ret = Vec::with_capacity(cleaning_tasks.len());
204
205 for task in cleaning_tasks {
206 ret.push(task.await??);
207 }
208
209 Ok(ret)
210 }
211
212 pub fn start_clean(
223 self: &Arc<Self>,
224 tracking_name: Option<Arc<str>>,
225 size: Option<u64>,
226 sha256: Sha256Policy,
227 ) -> Result<(UniqueId, SingleFileCleaner)> {
228 self.check_not_finalized()?;
229 let id = UniqueId::new();
230 let cleaner = self.start_clean_with_id(id, tracking_name, size, sha256);
231 Ok((id, cleaner))
232 }
233
234 fn start_clean_with_id(
235 self: &Arc<Self>,
236 id: UniqueId,
237 tracking_name: Option<Arc<str>>,
238 size: Option<u64>,
239 sha256: Sha256Policy,
240 ) -> SingleFileCleaner {
241 let updater = self.progress.new_item(id, tracking_name.clone().unwrap_or_default());
242 let file_id = self.completion_tracker.register_new_file(updater, size);
243 SingleFileCleaner::new(tracking_name, file_id, sha256, self.clone())
244 }
245
246 #[cfg(not(target_family = "wasm"))]
250 pub async fn spawn_upload_from_path(
251 self: &Arc<Self>,
252 file_path: PathBuf,
253 sha256: Sha256Policy,
254 ) -> Result<(UniqueId, JoinHandle<Result<(XetFileInfo, DeduplicationMetrics)>>)> {
255 self.check_not_finalized()?;
256 let file_size = std::fs::metadata(&file_path)?.len();
257 let tracking_name: Arc<str> = Arc::from(file_path.to_string_lossy().as_ref());
258 let (id, cleaner) = self.start_clean(Some(tracking_name), Some(file_size), sha256)?;
259
260 let session = self.clone();
261 let runtime = self.ctx.runtime.clone();
262 let semaphore = self.ctx.common.file_ingestion_semaphore.clone();
263 let handle = runtime.spawn(async move {
264 let _permit = semaphore.acquire().await?;
265 Self::feed_file_to_cleaner(&session, cleaner, &file_path).await
266 });
267
268 Ok((id, handle))
269 }
270
271 pub async fn spawn_upload_bytes(
275 self: &Arc<Self>,
276 bytes: Vec<u8>,
277 sha256: Sha256Policy,
278 tracking_name: Option<Arc<str>>,
279 ) -> Result<(UniqueId, JoinHandle<Result<(XetFileInfo, DeduplicationMetrics)>>)> {
280 self.check_not_finalized()?;
281 let (id, mut cleaner) = self.start_clean(tracking_name, Some(bytes.len() as u64), sha256)?;
282
283 let semaphore = self.ctx.common.file_ingestion_semaphore.clone();
284 #[cfg(not(target_family = "wasm"))]
289 let handle = self.ctx.runtime.spawn(async move {
290 let _permit = semaphore.acquire().await?;
291 cleaner.add_data(&bytes).await?;
292 cleaner.finish().await
293 });
294 #[cfg(target_family = "wasm")]
295 let handle = tokio::task::spawn(async move {
296 let _permit = semaphore.acquire().await?;
297 cleaner.add_data(&bytes).await?;
298 cleaner.finish().await
299 });
300
301 Ok((id, handle))
302 }
303
304 #[cfg(not(target_family = "wasm"))]
305 async fn feed_file_to_cleaner(
306 _session: &Arc<Self>,
307 mut cleaner: SingleFileCleaner,
308 file_path: &Path,
309 ) -> Result<(XetFileInfo, DeduplicationMetrics)> {
310 let mut reader = File::open(file_path)?;
311 let filesize = reader.metadata()?.len();
312 let mut buffer = vec![0u8; u64::min(filesize, *_session.ctx.config.data.ingestion_block_size) as usize];
313
314 loop {
315 let n = reader.read(&mut buffer)?;
316 if n == 0 {
317 break;
318 }
319 cleaner.add_data(&buffer[..n]).await?;
320 }
321 cleaner.finish().await
322 }
323
324 #[instrument(skip_all, name="FileUploadSession::register_new_xorb_for_upload", fields(xorb_len = xorb.num_bytes()))]
327 pub(crate) async fn register_new_xorb(
328 self: &Arc<Self>,
329 xorb: RawXorbData,
330 file_dependencies: &[FileXorbDependency],
331 ) -> Result<bool> {
332 {
334 let mut upload_tasks = self.xorb_upload_tasks.lock().await;
335 while let Some(result) = upload_tasks.try_join_next() {
336 result??;
337 }
338 }
339
340 let xorb_hash = xorb.hash();
341
342 let xorb_is_new = self.completion_tracker.register_new_xorb(xorb_hash, xorb.num_bytes() as u64);
348
349 self.completion_tracker.register_dependencies(file_dependencies);
352
353 if !xorb_is_new {
354 return Ok(false);
355 }
356
357 if xorb.num_bytes() == 0 {
360 self.completion_tracker.register_xorb_upload_completion(xorb_hash);
361 return Ok(true);
362 }
363
364 let xorb_info = Arc::new(xorb.xorb_info.clone());
368 self.shard_interface.add_xorb_block(xorb_info.clone()).await?;
369
370 let runtime = self.ctx.runtime.clone();
373 let compression_policy = self.ctx.config.xorb.compression_policy.clone();
374 let compression_scheme_retest_interval = self.ctx.config.xorb.compression_scheme_retest_interval;
375 let xorb_obj = runtime
376 .spawn_blocking(move || {
377 SerializedXorbObject::from_xorb(
378 xorb,
379 false,
380 compression_policy.as_str(),
381 compression_scheme_retest_interval,
382 )
383 })
384 .await??;
385
386 let session = self.clone();
387 let upload_permit = self.client.acquire_upload_permit().await?;
388 let cas_prefix = self.ctx.config.data.default_prefix.clone();
389 let completion_tracker = self.completion_tracker.clone();
390 let xorb_hash = xorb_obj.hash;
391 let raw_num_bytes = xorb_obj.raw_num_bytes;
392 let progress_callback: ProgressCallback = Arc::new(move |delta, _completed, total| {
393 let raw_delta = (delta * raw_num_bytes).checked_div(total).unwrap_or(0);
394 if raw_delta > 0 {
395 completion_tracker
396 .clone()
397 .register_xorb_upload_progress_background(xorb_hash, raw_delta);
398 }
399 });
400
401 self.xorb_upload_tasks.lock().await.spawn(
402 async move {
403 let n_bytes_transmitted = session
404 .client
405 .upload_xorb(&cas_prefix, xorb_obj, Some(progress_callback), upload_permit)
406 .await?;
407
408 session.completion_tracker.register_xorb_upload_completion(xorb_hash);
410
411 session.deduplication_metrics.lock().await.xorb_bytes_uploaded += n_bytes_transmitted;
413
414 session.shard_interface.add_uploaded_xorb_block(xorb_info).await?;
416
417 Ok(())
418 }
419 .instrument(info_span!("FileUploadSession::upload_xorb_task", xorb.hash = xorb_hash.hex())),
420 );
421
422 Ok(true)
423 }
424
425 #[instrument(skip_all, name="FileUploadSession::register_single_file_clean_completion", fields(num_bytes = file_data.num_bytes(), num_chunks = file_data.num_chunks()))]
427 pub(crate) async fn register_single_file_clean_completion(
428 self: &Arc<Self>,
429 mut file_data: DataAggregator,
430 dedup_metrics: &DeduplicationMetrics,
431 ) -> Result<()> {
432 {
434 let mut current_session_data = self.current_session_data.lock().await;
435
436 #[cfg(feature = "simulation")]
437 let xorb_cut_bytes = self
438 .ctx
439 .config
440 .xorb
441 .simulation_max_bytes
442 .map(|bs| bs.as_u64().min(*MAX_XORB_BYTES as u64) as usize)
443 .unwrap_or(*MAX_XORB_BYTES);
444 #[cfg(not(feature = "simulation"))]
445 let xorb_cut_bytes = *MAX_XORB_BYTES;
446 #[cfg(feature = "simulation")]
447 let xorb_cut_chunks = self
448 .ctx
449 .config
450 .xorb
451 .simulation_max_chunks
452 .unwrap_or(*MAX_XORB_CHUNKS)
453 .min(*MAX_XORB_CHUNKS);
454 #[cfg(not(feature = "simulation"))]
455 let xorb_cut_chunks = *MAX_XORB_CHUNKS;
456
457 if current_session_data.num_bytes() + file_data.num_bytes() > xorb_cut_bytes
459 || current_session_data.num_chunks() + file_data.num_chunks() > xorb_cut_chunks
460 {
461 if current_session_data.num_bytes() > file_data.num_bytes() {
463 swap(&mut *current_session_data, &mut file_data);
464 }
465
466 debug_assert_le!(current_session_data.num_bytes(), file_data.num_bytes());
468
469 drop(current_session_data);
471
472 self.process_aggregated_data_as_xorb(file_data).await?;
473 } else {
474 current_session_data.merge_in(file_data);
475 }
476 }
477
478 #[cfg(debug_assertions)]
479 {
480 let current_session_data = self.current_session_data.lock().await;
481 debug_assert_le!(current_session_data.num_bytes(), *MAX_XORB_BYTES);
482 debug_assert_le!(current_session_data.num_chunks(), *MAX_XORB_CHUNKS);
483 }
484
485 self.deduplication_metrics.lock().await.merge_in(dedup_metrics);
487
488 Ok(())
489 }
490
491 pub(crate) async fn register_single_file_clean_completion_detached(
495 self: &Arc<Self>,
496 file_data: DataAggregator,
497 dedup_metrics: &DeduplicationMetrics,
498 ) -> Result<MDBFileInfo> {
499 let file_infos = self.process_aggregated_data_as_xorb_detached(file_data).await?;
502
503 self.deduplication_metrics.lock().await.merge_in(dedup_metrics);
504
505 debug_assert_eq!(file_infos.len(), 1);
506 file_infos
507 .into_iter()
508 .next()
509 .ok_or_else(|| DataError::InternalError("detached completion produced no file info".into()))
510 }
511
512 async fn process_aggregated_data_as_xorb(self: &Arc<Self>, data_agg: DataAggregator) -> Result<()> {
514 self.process_aggregated_data_as_xorb_impl(data_agg, true).await.map(|_| ())
515 }
516
517 async fn process_aggregated_data_as_xorb_detached(
520 self: &Arc<Self>,
521 data_agg: DataAggregator,
522 ) -> Result<Vec<MDBFileInfo>> {
523 self.process_aggregated_data_as_xorb_impl(data_agg, false).await
524 }
525
526 async fn process_aggregated_data_as_xorb_impl(
527 self: &Arc<Self>,
528 data_agg: DataAggregator,
529 register_files: bool,
530 ) -> Result<Vec<MDBFileInfo>> {
531 let (xorb, new_files) = data_agg.finalize();
532 let xorb_hash = xorb.hash();
533
534 debug_assert_le!(xorb.num_bytes(), *MAX_XORB_BYTES);
535 debug_assert_le!(xorb.data.len(), *MAX_XORB_CHUNKS);
536
537 let mut new_dependencies = Vec::with_capacity(new_files.len());
538 let mut file_infos = Vec::with_capacity(new_files.len());
539
540 for (file_id, fi, bytes_in_xorb) in new_files {
541 new_dependencies.push(FileXorbDependency {
542 file_id,
543 xorb_hash,
544 n_bytes: bytes_in_xorb,
545 is_external: false,
546 });
547
548 if register_files {
549 self.shard_interface.add_file_reconstruction_info(fi).await?;
550 } else {
551 file_infos.push(fi);
552 }
553 }
554
555 self.register_new_xorb(xorb, &new_dependencies).await?;
557
558 Ok(file_infos)
559 }
560
561 pub(crate) fn register_xorb_dependencies(self: &Arc<Self>, xorb_dependencies: &[FileXorbDependency]) {
563 self.completion_tracker.register_dependencies(xorb_dependencies);
564 }
565
566 #[instrument(skip_all, name="FileUploadSession::finalize", fields(session.id))]
568 async fn finalize_impl(
569 self: Arc<Self>,
570 return_files: bool,
571 ) -> Result<(DeduplicationMetrics, Vec<MDBFileInfo>, GroupProgressReport)> {
572 if self.finalized.swap(true, Ordering::AcqRel) {
573 return Err(DataError::InvalidOperation("FileUploadSession already finalized".to_string()));
574 }
575
576 let data_agg = take(&mut *self.current_session_data.lock().await);
578 self.process_aggregated_data_as_xorb(data_agg).await?;
579
580 let mut upload_tasks = take(&mut *self.xorb_upload_tasks.lock().await);
589
590 while let Some(result) = upload_tasks.join_next().await {
591 result??;
592 }
593
594 let mut metrics = take(&mut *self.deduplication_metrics.lock().await);
595
596 let all_file_info = if return_files {
597 self.shard_interface.session_file_info_list().await?
598 } else {
599 Vec::new()
600 };
601
602 metrics.shard_bytes_uploaded = self.shard_interface.upload_and_register_session_shards().await?;
605 metrics.total_bytes_uploaded = metrics.shard_bytes_uploaded + metrics.xorb_bytes_uploaded;
606
607 #[cfg(debug_assertions)]
608 {
609 self.completion_tracker.assert_complete();
610 self.progress.assert_complete();
611 }
612
613 let report = self.report();
614 Ok((metrics, all_file_info, report))
615 }
616
617 pub async fn checkpoint(self: &Arc<Self>) -> Result<()> {
622 let data_agg = take(&mut *self.current_session_data.lock().await);
624 self.process_aggregated_data_as_xorb(data_agg).await?;
625
626 {
628 let mut upload_tasks = self.xorb_upload_tasks.lock().await;
629
630 while let Some(result) = upload_tasks.join_next().await {
631 result??;
632 }
633 }
634
635 Ok(())
636 }
637
638 pub async fn register_composed_file(self: &Arc<Self>, file_info: MDBFileInfo) -> Result<()> {
642 self.check_not_finalized()?;
643 self.shard_interface.add_file_reconstruction_info(file_info).await
644 }
645
646 fn check_not_finalized(&self) -> Result<()> {
647 if self.finalized.load(Ordering::Acquire) {
648 return Err(DataError::InvalidOperation("FileUploadSession already finalized".to_string()));
649 }
650 Ok(())
651 }
652
653 pub fn client(&self) -> Arc<dyn Client + Send + Sync> {
654 Arc::clone(&self.client)
655 }
656
657 pub fn progress(&self) -> &Arc<GroupProgress> {
658 &self.progress
659 }
660
661 pub fn report(&self) -> GroupProgressReport {
662 self.progress.report()
663 }
664
665 pub fn item_report(&self, id: UniqueId) -> Option<ItemProgressReport> {
666 self.progress.item_report(id)
667 }
668
669 pub fn item_reports(&self) -> HashMap<UniqueId, ItemProgressReport> {
670 self.progress.item_reports()
671 }
672
673 pub async fn finalize(self: Arc<Self>) -> Result<DeduplicationMetrics> {
674 Ok(self.finalize_impl(false).await?.0)
675 }
676
677 pub async fn finalize_with_report(self: Arc<Self>) -> Result<(DeduplicationMetrics, GroupProgressReport)> {
678 let (metrics, _file_info, report) = self.finalize_impl(false).await?;
679 Ok((metrics, report))
680 }
681
682 pub async fn finalize_with_file_info(self: Arc<Self>) -> Result<(DeduplicationMetrics, Vec<MDBFileInfo>)> {
683 let (metrics, file_info, _report) = self.finalize_impl(true).await?;
684 Ok((metrics, file_info))
685 }
686}
687
688#[cfg(all(test, not(target_family = "wasm")))]
689mod tests {
690 use std::fs::{File, OpenOptions};
691 use std::io::{Read, Write};
692 use std::path::Path;
693
694 use xet_runtime::core::XetContext;
695
696 use crate::processing::{FileDownloadSession, FileUploadSession, XetFileInfo};
697
698 async fn test_clean_file(cas_path: &Path, input_path: &Path, output_path: &Path) {
703 let read_data = read(input_path).unwrap().to_vec();
704
705 let mut pf_out = Box::new(
706 OpenOptions::new()
707 .create(true)
708 .write(true)
709 .truncate(true)
710 .open(output_path)
711 .unwrap(),
712 );
713
714 let ctx = XetContext::default().unwrap();
715 let upload_session = FileUploadSession::new(TranslatorConfig::local_config(&ctx, cas_path).unwrap().into())
716 .await
717 .unwrap();
718
719 let (_id, mut cleaner) = upload_session
720 .start_clean(Some("test".into()), Some(read_data.len() as u64), Sha256Policy::Compute)
721 .unwrap();
722
723 cleaner.add_data(&read_data[..]).await.unwrap();
725
726 let (xet_file_info, _metrics) = cleaner.finish().await.unwrap();
727 upload_session.finalize().await.unwrap();
728
729 pf_out
730 .write_all(serde_json::to_string(&xet_file_info).unwrap().as_bytes())
731 .unwrap();
732 }
733
734 async fn test_smudge_file(cas_path: &Path, pointer_path: &Path, output_path: &Path) {
739 let mut reader = File::open(pointer_path).unwrap();
740
741 let mut input = String::new();
742 reader.read_to_string(&mut input).unwrap();
743
744 let xet_file = serde_json::from_str::<XetFileInfo>(&input).unwrap();
745
746 let ctx = XetContext::default().unwrap();
747 let config = TranslatorConfig::local_config(&ctx, cas_path).unwrap();
748 let session = FileDownloadSession::new(config.into(), None).await.unwrap();
749
750 let (_id, _n_bytes) = session.download_file(&xet_file, output_path).await.unwrap();
751 }
752
753 use std::fs::{read, write};
754
755 use tempfile::tempdir;
756
757 use super::*;
758
759 #[test]
760 fn test_clean_smudge_round_trip() {
761 let temp = tempdir().unwrap();
762 let original_data = b"Hello, world!";
763
764 let ctx = XetContext::default().unwrap();
765
766 ctx.runtime
767 .bridge_sync(async move {
768 let cas_path = temp.path().join("cas");
769
770 let original_path = temp.path().join("original.txt");
772 write(&original_path, original_data).unwrap();
773
774 let pointer_path = temp.path().join("pointer.txt");
776 test_clean_file(&cas_path, &original_path, &pointer_path).await;
777
778 let hydrated_path = temp.path().join("hydrated.txt");
780 test_smudge_file(&cas_path, &pointer_path, &hydrated_path).await;
781
782 let result_data = read(hydrated_path).unwrap();
784 assert_eq!(original_data.to_vec(), result_data);
785 })
786 .unwrap();
787 }
788
789 #[test]
790 fn test_clean_skip_sha256_no_metadata_ext() {
791 let temp = tempdir().unwrap();
792 let data = b"Hello, skip sha256!";
793
794 let ctx = XetContext::default().unwrap();
795
796 ctx.runtime
797 .bridge_sync(async move {
798 let cas_path = temp.path().join("cas");
799
800 let session_ctx = XetContext::default().unwrap();
801 let upload_session =
802 FileUploadSession::new(TranslatorConfig::local_config(&session_ctx, &cas_path).unwrap().into())
803 .await
804 .unwrap();
805
806 let (_id, mut cleaner) = upload_session
807 .start_clean(Some("test".into()), Some(data.len() as u64), Sha256Policy::Skip)
808 .unwrap();
809 cleaner.add_data(data).await.unwrap();
810 let _ = cleaner.finish().await.unwrap();
811
812 let (_metrics, file_infos) = upload_session.finalize_with_file_info().await.unwrap();
814 assert_eq!(file_infos.len(), 1);
815 assert!(file_infos[0].metadata_ext.is_none(), "Skip should produce no metadata_ext");
816 })
817 .unwrap();
818 }
819}