1use super::{
2 fetch::{ArchiveFetcher, DownloadedArchive},
3 progress::{
4 ArchiveExtractionProgress, ArchiveExtractionProgressHandle, DownloadProgress,
5 DownloadRequestLimiter, ProgressReader, SharedProgressReader,
6 },
7 session::DownloadSession,
8 MAX_DOWNLOAD_RETRIES, RETRY_BACKOFF_SECS,
9};
10use eyre::{Result, WrapErr};
11use lz4::Decoder;
12use reqwest::blocking::Client as BlockingClient;
13use reth_cli_util::cancellation::CancellationToken;
14use reth_fs_util as fs;
15use std::{
16 io::Read,
17 path::{Component, Path, PathBuf},
18 sync::{
19 atomic::{AtomicBool, Ordering},
20 Arc,
21 },
22 thread,
23 time::{Duration, Instant},
24};
25use tar::Archive;
26use tracing::{info, warn};
27use url::Url;
28use zstd::stream::read::Decoder as ZstdDecoder;
29
30const EXTENSION_TAR_LZ4: &str = ".tar.lz4";
31const EXTENSION_TAR_ZSTD: &str = ".tar.zst";
32const STREAMING_EXTRACTION_PROGRESS_MIN_FILE_SIZE: u64 = 64 * 1024 * 1024;
33const EXTRACTION_PROGRESS_POLL_INTERVAL: Duration = Duration::from_millis(100);
34
35#[derive(Debug, Clone, Copy)]
37pub(crate) enum CompressionFormat {
38 Lz4,
40 Zstd,
42}
43
44impl CompressionFormat {
45 pub(crate) fn from_url(url: &str) -> Result<Self> {
47 let path =
48 Url::parse(url).map(|u| u.path().to_string()).unwrap_or_else(|_| url.to_string());
49
50 if path.ends_with(EXTENSION_TAR_LZ4) {
51 Ok(Self::Lz4)
52 } else if path.ends_with(EXTENSION_TAR_ZSTD) {
53 Ok(Self::Zstd)
54 } else {
55 Err(eyre::eyre!(
56 "Unsupported file format. Expected .tar.lz4 or .tar.zst, got: {}",
57 path
58 ))
59 }
60 }
61}
62
63fn extract_archive<R: Read>(
65 reader: R,
66 total_size: u64,
67 format: CompressionFormat,
68 target_dir: &Path,
69 static_files_dir: Option<&Path>,
70 cancel_token: CancellationToken,
71) -> Result<()> {
72 let progress_reader = ProgressReader::new(reader, total_size, cancel_token);
73
74 match format {
75 CompressionFormat::Lz4 => {
76 let decoder = Decoder::new(progress_reader)?;
77 unpack_archive(Archive::new(decoder), target_dir, static_files_dir, None)?;
78 }
79 CompressionFormat::Zstd => {
80 let decoder = ZstdDecoder::new(progress_reader)?;
81 unpack_archive(Archive::new(decoder), target_dir, static_files_dir, None)?;
82 }
83 }
84
85 println!();
86 Ok(())
87}
88
89pub(crate) fn extract_archive_raw<R: Read>(
91 reader: R,
92 format: CompressionFormat,
93 target_dir: &Path,
94 static_files_dir: Option<&Path>,
95 progress: Option<&mut ArchiveExtractionProgress>,
96) -> Result<()> {
97 match format {
98 CompressionFormat::Lz4 => {
99 unpack_archive(
100 Archive::new(Decoder::new(reader)?),
101 target_dir,
102 static_files_dir,
103 progress,
104 )?;
105 }
106 CompressionFormat::Zstd => {
107 unpack_archive(
108 Archive::new(ZstdDecoder::new(reader)?),
109 target_dir,
110 static_files_dir,
111 progress,
112 )?;
113 }
114 }
115
116 Ok(())
117}
118
119fn unpack_archive<R: Read>(
120 mut archive: Archive<R>,
121 target_dir: &Path,
122 static_files_dir: Option<&Path>,
123 mut progress: Option<&mut ArchiveExtractionProgress>,
124) -> Result<()> {
125 let entries = archive.entries().wrap_err_with(|| {
128 format!("failed to read archive entries for `{}`", target_dir.display())
129 })?;
130
131 for entry in entries {
132 let mut entry = entry.wrap_err_with(|| {
133 format!("failed to read archive entry for `{}`", target_dir.display())
134 })?;
135 extract_entry_with_progress(
136 &mut entry,
137 target_dir,
138 static_files_dir,
139 progress.as_deref_mut(),
140 )?;
141 }
142
143 Ok(())
144}
145
146pub(crate) fn static_file_relative_path(path: &Path) -> Option<&Path> {
148 path.strip_prefix(".").unwrap_or(path).strip_prefix("static_files").ok()
149}
150
151pub(crate) fn ensure_no_symlinks(root: &Path, relative_path: &Path) -> Result<()> {
156 let mut current = root.to_path_buf();
157 for part in relative_path.components() {
158 current.push(part);
159 match std::fs::symlink_metadata(¤t) {
160 Ok(metadata) => eyre::ensure!(
161 !metadata.file_type().is_symlink(),
162 "Archive path `{}` contains a symlink",
163 current.display()
164 ),
165 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
166 Err(err) => return Err(err.into()),
167 }
168 }
169 Ok(())
170}
171
172fn unpack_static_file<R: Read>(
175 entry: &mut tar::Entry<'_, R>,
176 static_files_dir: &Path,
177 relative_path: &Path,
178) -> Result<()> {
179 eyre::ensure!(
180 relative_path
181 .components()
182 .all(|part| matches!(part, Component::Normal(_) | Component::CurDir)),
183 "Invalid static file archive path: {}",
184 relative_path.display()
185 );
186 let entry_type = entry.header().entry_type();
187 eyre::ensure!(
188 entry_type.is_file() || entry_type.is_dir(),
189 "Unsupported static file archive entry"
190 );
191 fs::create_dir_all(static_files_dir)?;
192 let root = static_files_dir.to_path_buf();
193 let dest = root.join(relative_path);
194 ensure_no_symlinks(&root, relative_path)?;
196 if relative_path.as_os_str().is_empty() {
197 return Ok(())
198 }
199 if let Some(parent) = dest.parent() {
200 fs::create_dir_all(parent)?;
201 }
202 entry.unpack(dest)?;
203 Ok(())
204}
205
206fn unpack_entry<R: Read>(
207 entry: &mut tar::Entry<'_, R>,
208 target_dir: &Path,
209 static_files_dir: Option<&Path>,
210) -> Result<()> {
211 let entry_type = entry.header().entry_type();
214 eyre::ensure!(
215 !entry_type.is_symlink() && !entry_type.is_hard_link(),
216 "Archive links are unsupported: `{}`",
217 entry.path()?.display()
218 );
219 let path = entry.path()?.into_owned();
220 if let Some(static_files_dir) = static_files_dir &&
221 let Some(relative_path) = static_file_relative_path(&path)
222 {
223 unpack_static_file(entry, static_files_dir, relative_path)
224 } else {
225 entry.unpack_in(target_dir)?;
226 Ok(())
227 }
228}
229
230fn extract_entry_with_progress<R: Read>(
231 entry: &mut tar::Entry<'_, R>,
232 target_dir: &Path,
233 static_files_dir: Option<&Path>,
234 progress: Option<&mut ArchiveExtractionProgress>,
235) -> Result<()> {
236 let size = entry.header().entry_size().unwrap_or(0);
237 let entry_type = entry.header().entry_type();
238
239 if !entry_type.is_file() || size == 0 {
240 unpack_entry(entry, target_dir, static_files_dir).wrap_err_with(|| {
241 format!("failed to extract archive into `{}`", target_dir.display())
242 })?;
243 return Ok(())
244 }
245
246 if size < STREAMING_EXTRACTION_PROGRESS_MIN_FILE_SIZE {
247 unpack_entry(entry, target_dir, static_files_dir).wrap_err_with(|| {
248 format!("failed to extract archive into `{}`", target_dir.display())
249 })?;
250 if let Some(progress) = progress {
251 progress.record_extracted(size);
252 }
253 return Ok(())
254 }
255
256 let Some(progress_handle) = progress.as_ref().and_then(|progress| progress.handle()) else {
257 unpack_entry(entry, target_dir, static_files_dir).wrap_err_with(|| {
258 format!("failed to extract archive into `{}`", target_dir.display())
259 })?;
260 return Ok(())
261 };
262
263 let Some(entry_path) = entry_destination_path(entry, target_dir, static_files_dir)? else {
264 unpack_entry(entry, target_dir, static_files_dir).wrap_err_with(|| {
265 format!("failed to extract archive into `{}`", target_dir.display())
266 })?;
267 return Ok(())
268 };
269
270 let stop = Arc::new(AtomicBool::new(false));
271 let monitor = spawn_extraction_progress_monitor(entry_path, progress_handle, Arc::clone(&stop));
272 let unpack_result = unpack_entry(entry, target_dir, static_files_dir)
273 .wrap_err_with(|| format!("failed to extract archive into `{}`", target_dir.display()));
274 stop.store(true, Ordering::Relaxed);
275
276 let monitor_result = monitor.join();
277 unpack_result?;
278
279 monitor_result.map_err(|_| eyre::eyre!("extraction progress monitor panicked"))?;
280 Ok(())
281}
282
283fn entry_destination_path<R: Read>(
284 entry: &tar::Entry<'_, R>,
285 target_dir: &Path,
286 static_files_dir: Option<&Path>,
287) -> Result<Option<PathBuf>> {
288 let mut file_dst = target_dir.to_path_buf();
289 let path = entry.path().wrap_err("invalid path in archive entry")?;
290 let path = if let Some(static_files_dir) = static_files_dir &&
291 let Some(relative_path) = static_file_relative_path(&path)
292 {
293 file_dst = static_files_dir.to_path_buf();
294 relative_path
295 } else {
296 path.as_ref()
297 };
298
299 for part in path.components() {
300 match part {
301 Component::Prefix(..) | Component::RootDir | Component::CurDir => continue,
302 Component::ParentDir => return Ok(None),
303 Component::Normal(part) => file_dst.push(part),
304 }
305 }
306
307 if file_dst == target_dir {
308 return Ok(None)
309 }
310
311 Ok(Some(file_dst))
312}
313
314fn spawn_extraction_progress_monitor(
315 entry_path: PathBuf,
316 progress: ArchiveExtractionProgressHandle,
317 stop: Arc<AtomicBool>,
318) -> thread::JoinHandle<()> {
319 thread::spawn(move || {
320 let mut extracted = 0_u64;
321
322 loop {
323 record_extracted_file_bytes(&entry_path, &progress, &mut extracted);
324 if stop.load(Ordering::Relaxed) {
325 break;
326 }
327 thread::sleep(EXTRACTION_PROGRESS_POLL_INTERVAL);
328 }
329 })
330}
331
332fn record_extracted_file_bytes(
333 entry_path: &Path,
334 progress: &ArchiveExtractionProgressHandle,
335 extracted: &mut u64,
336) {
337 let Ok(meta) = fs::metadata(entry_path) else { return };
338 let len = meta.len();
339 if len > *extracted {
340 progress.record_extracted(len - *extracted);
341 *extracted = len;
342 }
343}
344
345fn extract_from_file(
347 path: &Path,
348 format: CompressionFormat,
349 target_dir: &Path,
350 static_files_dir: Option<&Path>,
351) -> Result<()> {
352 let file = std::fs::File::open(path)?;
353 let total_size = file.metadata()?.len();
354 info!(target: "reth::cli",
355 file = %path.display(),
356 size = %DownloadProgress::format_size(total_size),
357 "Extracting local archive"
358 );
359 let start = Instant::now();
360 extract_archive(
361 file,
362 total_size,
363 format,
364 target_dir,
365 static_files_dir,
366 CancellationToken::new(),
367 )?;
368 info!(target: "reth::cli",
369 file = %path.display(),
370 elapsed = %DownloadProgress::format_duration(start.elapsed()),
371 "Local extraction complete"
372 );
373 Ok(())
374}
375
376pub(crate) fn streaming_download_and_extract(
380 url: &str,
381 format: CompressionFormat,
382 target_dir: &Path,
383 static_files_dir: Option<&Path>,
384 session: &DownloadSession,
385) -> Result<()> {
386 if let Some(path) = archive_file_url_path(url)? {
387 let size = path.metadata()?.len();
388 extract_from_file(&path, format, target_dir, static_files_dir)?;
389 session.record_archive_output_complete(size);
390 return Ok(())
391 }
392
393 let shared = session.progress();
394 let quiet = session.progress().is_some();
395 let mut last_error: Option<eyre::Error> = None;
396
397 for attempt in 1..=MAX_DOWNLOAD_RETRIES {
398 if attempt > 1 {
399 info!(target: "reth::cli",
400 url = %url,
401 attempt,
402 max = MAX_DOWNLOAD_RETRIES,
403 "Retrying streaming download from scratch"
404 );
405 }
406
407 let client = BlockingClient::builder().connect_timeout(Duration::from_secs(30)).build()?;
408 let _request_permit = session
409 .request_limiter()
410 .map(|limiter| limiter.acquire(session.progress(), session.cancel_token()))
411 .transpose()?;
412
413 let response = match client.get(url).send().and_then(|r| r.error_for_status()) {
414 Ok(r) => r,
415 Err(error) => {
416 let err = eyre::Error::from(error);
417 if attempt < MAX_DOWNLOAD_RETRIES {
418 warn!(target: "reth::cli",
419 url = %url,
420 attempt,
421 max = MAX_DOWNLOAD_RETRIES,
422 err = %err,
423 "Streaming request failed, retrying"
424 );
425 }
426 last_error = Some(err);
427 if attempt < MAX_DOWNLOAD_RETRIES {
428 std::thread::sleep(
429 session.retry_delay(Duration::from_secs(RETRY_BACKOFF_SECS)),
430 );
431 }
432 continue;
433 }
434 };
435
436 if !quiet && let Some(size) = response.content_length() {
437 info!(target: "reth::cli",
438 url = %url,
439 size = %DownloadProgress::format_size(size),
440 "Streaming archive"
441 );
442 }
443
444 let result = if let Some(progress) = shared {
445 let reader = SharedProgressReader { inner: response, progress: Arc::clone(progress) };
446 extract_archive_raw(reader, format, target_dir, static_files_dir, None)
447 } else {
448 let total_size = response.content_length().unwrap_or(0);
449 extract_archive(
450 response,
451 total_size,
452 format,
453 target_dir,
454 static_files_dir,
455 session.cancel_token().clone(),
456 )
457 };
458
459 match result {
460 Ok(()) => return Ok(()),
461 Err(error) => {
462 if attempt < MAX_DOWNLOAD_RETRIES {
463 warn!(target: "reth::cli",
464 url = %url,
465 attempt,
466 max = MAX_DOWNLOAD_RETRIES,
467 err = %error,
468 "Streaming extraction failed, retrying"
469 );
470 }
471 last_error = Some(error);
472 if attempt < MAX_DOWNLOAD_RETRIES {
473 std::thread::sleep(
474 session.retry_delay(Duration::from_secs(RETRY_BACKOFF_SECS)),
475 );
476 }
477 }
478 }
479 }
480
481 Err(last_error.unwrap_or_else(|| {
482 eyre::eyre!("Streaming download failed after {MAX_DOWNLOAD_RETRIES} attempts")
483 }))
484}
485
486fn archive_file_url_path(url: &str) -> Result<Option<PathBuf>> {
488 let Ok(parsed) = Url::parse(url) else { return Ok(None) };
489 if parsed.scheme() != "file" {
490 return Ok(None)
491 }
492
493 parsed
494 .to_file_path()
495 .map(Some)
496 .map_err(|_| eyre::eyre!("Invalid file:// archive URL path: {url}"))
497}
498
499fn download_and_extract(
501 url: &str,
502 format: CompressionFormat,
503 target_dir: &Path,
504 static_files_dir: Option<&Path>,
505 session: DownloadSession,
506) -> Result<()> {
507 let quiet = session.progress().is_some();
508 let fetcher = ArchiveFetcher::new(url.to_string(), target_dir, session.clone());
509 let DownloadedArchive { path: downloaded_path, size: total_size } = fetcher.download(None)?;
510
511 let file_name =
512 downloaded_path.file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_default();
513
514 if !quiet {
515 info!(target: "reth::cli",
516 file = %file_name,
517 size = %DownloadProgress::format_size(total_size),
518 "Extracting archive"
519 );
520 }
521 let file = fs::open(&downloaded_path)?;
522
523 if quiet {
524 extract_archive_raw(file, format, target_dir, static_files_dir, None)?;
525 } else {
526 extract_archive(
527 file,
528 total_size,
529 format,
530 target_dir,
531 static_files_dir,
532 session.cancel_token().clone(),
533 )?;
534 info!(target: "reth::cli",
535 file = %file_name,
536 "Extraction complete"
537 );
538 }
539
540 fetcher.cleanup_downloaded_files();
541 session.record_archive_output_complete(total_size);
542
543 Ok(())
544}
545
546fn blocking_download_and_extract(
552 url: &str,
553 target_dir: &Path,
554 static_files_dir: Option<&Path>,
555 resumable: bool,
556 request_limiter: Option<Arc<DownloadRequestLimiter>>,
557 cancel_token: CancellationToken,
558 retry_backoff: Option<Duration>,
559) -> Result<()> {
560 let format = CompressionFormat::from_url(url)?;
561
562 if let Ok(parsed_url) = Url::parse(url) &&
563 parsed_url.scheme() == "file"
564 {
565 let session = DownloadSession::new(None, request_limiter, cancel_token)
566 .with_retry_backoff(retry_backoff);
567 let file_path = parsed_url
568 .to_file_path()
569 .map_err(|_| eyre::eyre!("Invalid file:// URL path: {}", url))?;
570 let result = extract_from_file(&file_path, format, target_dir, static_files_dir);
571 if result.is_ok() {
572 session.record_archive_output_complete(file_path.metadata()?.len());
573 }
574 result
575 } else if let Some(request_limiter) = request_limiter {
576 download_and_extract(
577 url,
578 format,
579 target_dir,
580 static_files_dir,
581 DownloadSession::new(None, Some(request_limiter), cancel_token)
582 .with_retry_backoff(retry_backoff),
583 )
584 } else if resumable {
585 let session =
586 DownloadSession::new(None, Some(DownloadRequestLimiter::new(1)), cancel_token)
587 .with_retry_backoff(retry_backoff);
588 download_and_extract(url, format, target_dir, static_files_dir, session)
589 } else {
590 let session =
591 DownloadSession::new(None, None, cancel_token).with_retry_backoff(retry_backoff);
592 let result =
593 streaming_download_and_extract(url, format, target_dir, static_files_dir, &session);
594 if result.is_ok() {
595 session.record_archive_output_complete(0);
596 }
597 result
598 }
599}
600
601pub(crate) async fn stream_and_extract(
606 url: &str,
607 target_dir: &Path,
608 static_files_dir: Option<&Path>,
609 resumable: bool,
610 request_limiter: Option<Arc<DownloadRequestLimiter>>,
611 cancel_token: CancellationToken,
612 retry_backoff: Option<Duration>,
613) -> Result<()> {
614 let target_dir = target_dir.to_path_buf();
615 let static_files_dir = static_files_dir.map(Path::to_path_buf);
616 let url = url.to_string();
617 reth_tasks::spawn_blocking(move || {
618 blocking_download_and_extract(
619 &url,
620 &target_dir,
621 static_files_dir.as_deref(),
622 resumable,
623 request_limiter,
624 cancel_token,
625 retry_backoff,
626 )
627 })
628 .await??;
629
630 Ok(())
631}
632
633#[cfg(test)]
634mod tests {
635 use super::*;
636
637 #[test]
638 fn custom_static_root_cannot_be_redirected_by_archive_symlinks() {
639 for custom_path in ["custom", "custom/nested"] {
640 let target = tempfile::tempdir().unwrap();
641 let outside = tempfile::tempdir().unwrap();
642 fs::create_dir_all(outside.path().join("nested")).unwrap();
643 for path in ["headers", "nested/headers"] {
644 fs::write(outside.path().join(path), b"keep").unwrap();
645 }
646 let custom = target.path().join(custom_path);
647 let mut archive = tar::Builder::new(Vec::new());
648 let mut link = tar::Header::new_gnu();
649 link.set_entry_type(tar::EntryType::Symlink);
650 link.set_size(0);
651 link.set_mode(0o777);
652 archive.append_link(&mut link, "custom", outside.path()).unwrap();
653 let mut file = tar::Header::new_gnu();
654 file.set_size(4);
655 file.set_mode(0o644);
656 file.set_cksum();
657 archive.append_data(&mut file, "static_files/headers", b"data".as_slice()).unwrap();
658 let tar = archive.into_inner().unwrap();
659 let err =
660 unpack_archive(Archive::new(tar.as_slice()), target.path(), Some(&custom), None)
661 .unwrap_err();
662 assert!(format!("{err:#}").contains("Archive links are unsupported"));
663 let outputs = [super::super::manifest::OutputFileChecksum {
664 path: "static_files/headers".into(),
665 size: 4,
666 blake3: String::new(),
667 }];
668 super::super::verify::OutputVerifier::new(target.path(), Some(&custom))
669 .cleanup(&outputs);
670 for path in ["headers", "nested/headers"] {
671 assert_eq!(fs::read(outside.path().join(path)).unwrap(), b"keep");
672 }
673 }
674 }
675
676 #[test]
677 fn link_entries_are_rejected_without_custom_static_dir() {
678 for entry_type in [tar::EntryType::Symlink, tar::EntryType::Link] {
679 for with_progress in [false, true] {
680 let root = tempfile::tempdir().unwrap();
681 let target = root.path().join("target");
682 let outside = root.path().join("outside");
683 fs::create_dir_all(&target).unwrap();
684 fs::create_dir_all(&outside).unwrap();
685 fs::write(outside.join("file"), b"keep").unwrap();
686
687 let mut archive = tar::Builder::new(Vec::new());
688 let mut link = tar::Header::new_gnu();
689 link.set_entry_type(entry_type);
690 link.set_size(0);
691 link.set_mode(0o777);
692 let link_target =
695 if entry_type.is_symlink() { outside.clone() } else { outside.join("file") };
696 archive.append_link(&mut link, "db", link_target).unwrap();
697 let mut file = tar::Header::new_gnu();
698 file.set_size(4);
699 file.set_mode(0o644);
700 file.set_cksum();
701 archive.append_data(&mut file, "db/file", b"data".as_slice()).unwrap();
702 let tar = archive.into_inner().unwrap();
703
704 let mut progress = ArchiveExtractionProgress::new(None);
705 let progress = with_progress.then_some(&mut progress);
706 let err = unpack_archive(Archive::new(tar.as_slice()), &target, None, progress)
707 .unwrap_err();
708 assert!(format!("{err:#}").contains("Archive links are unsupported"));
709
710 assert!(
711 !target.join("db").exists() && target.join("db").symlink_metadata().is_err()
712 );
713 assert_eq!(fs::read(outside.join("file")).unwrap(), b"keep");
714 assert_eq!(fs::read_dir(&outside).unwrap().count(), 1);
715 }
716 }
717 }
718
719 #[test]
720 fn remap_static_files_in_both_compression_formats() {
721 let mut archive = tar::Builder::new(Vec::new());
722 let mut directory = tar::Header::new_gnu();
723 directory.set_entry_type(tar::EntryType::Directory);
724 directory.set_size(0);
725 directory.set_mode(0o755);
726 directory.set_cksum();
727 archive.append_data(&mut directory, "./static_files/", std::io::empty()).unwrap();
728 for path in ["./static_files/nested/headers", "db/data"] {
729 let mut header = tar::Header::new_gnu();
730 header.set_size(4);
731 header.set_mode(0o644);
732 header.set_cksum();
733 archive.append_data(&mut header, path, b"data".as_slice()).unwrap();
734 }
735 let tar = archive.into_inner().unwrap();
736 for format in [CompressionFormat::Lz4, CompressionFormat::Zstd] {
737 let bytes = match format {
738 CompressionFormat::Lz4 => {
739 let mut encoder = lz4::EncoderBuilder::new().build(Vec::new()).unwrap();
740 std::io::copy(&mut tar.as_slice(), &mut encoder).unwrap();
741 let (bytes, result) = encoder.finish();
742 result.unwrap();
743 bytes
744 }
745 CompressionFormat::Zstd => zstd::encode_all(tar.as_slice(), 0).unwrap(),
746 };
747 let target = tempfile::tempdir().unwrap();
748 let custom = tempfile::tempdir().unwrap();
749 extract_archive_raw(bytes.as_slice(), format, target.path(), Some(custom.path()), None)
750 .unwrap();
751 assert_eq!(fs::read(custom.path().join("nested/headers")).unwrap(), b"data");
752 assert_eq!(fs::read(target.path().join("db/data")).unwrap(), b"data");
753 assert!(!target.path().join("static_files").exists());
754 }
755 }
756
757 #[test]
758 fn test_compression_format_detection() {
759 assert!(matches!(
760 CompressionFormat::from_url("https://example.com/snapshot.tar.lz4"),
761 Ok(CompressionFormat::Lz4)
762 ));
763 assert!(matches!(
764 CompressionFormat::from_url("https://example.com/snapshot.tar.zst"),
765 Ok(CompressionFormat::Zstd)
766 ));
767 assert!(matches!(
768 CompressionFormat::from_url("file:///path/to/snapshot.tar.lz4"),
769 Ok(CompressionFormat::Lz4)
770 ));
771 assert!(matches!(
772 CompressionFormat::from_url("file:///path/to/snapshot.tar.zst"),
773 Ok(CompressionFormat::Zstd)
774 ));
775 assert!(CompressionFormat::from_url("https://example.com/snapshot.tar.gz").is_err());
776 }
777}