1use std::cmp::Reverse;
2use std::future::Future;
3use std::io;
4use std::path::Path;
5use std::pin::Pin;
6use std::sync::Arc;
7use std::task::{Context, Poll};
8
9use futures::{FutureExt, TryStreamExt};
10use http_content_range::{ContentRange, ContentRangeBytes, ContentRangeUnbound};
11use rayon::in_place_scope;
12use rayon::prelude::*;
13use rustc_hash::FxHashMap;
14use tokio::io::{AsyncRead, AsyncSeekExt, AsyncWriteExt, ReadBuf};
15use tokio::sync::Semaphore;
16use tokio_util::compat::FuturesAsyncReadCompatExt;
17use tracing::{Instrument, debug, info_span, instrument, warn};
18use url::Url;
19
20use uv_cache::{ArchiveFileId, ArchiveId, Cache, CacheBucket, CacheEntry, WheelCache};
21use uv_cache_info::{CacheInfo, Timestamp};
22use uv_client::{
23 CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient,
24 RequestBuilder, RetryState,
25};
26use uv_distribution_filename::WheelFilename;
27use uv_distribution_types::{
28 ArchiveHashPolicy, BuildInfo, BuildableSource, BuiltDist, Dist, DistRef, HashCollection,
29 HashValidation, Hashed, IndexUrl, InstalledDist, MetadataHashPolicy, Name, SourceDist,
30 SourceUrl, parse_url_hashes,
31};
32use uv_extract::dirhash::{DirectoryDigest, HashedFile};
33use uv_extract::hash::Hasher;
34use uv_fs::{LockedFile, write_atomic};
35use uv_git::{GIT_LFS, GitError};
36use uv_platform_tags::Tags;
37use uv_preview::PreviewFeature;
38use uv_pypi_types::{HashDigest, HashDigests, PyProjectToml};
39use uv_python::PythonVariant;
40use uv_redacted::DisplaySafeUrl;
41use uv_threads::initialize_rayon_once;
42use uv_types::{BuildContext, BuildStack};
43
44use crate::archive::Archive;
45use crate::error::PythonVersion;
46use crate::extracted_wheel::{ExtractedWheel, HashedWheel, WheelExtractor};
47use crate::hash::http_hash_algorithms;
48use crate::metadata::{ArchiveMetadata, Metadata};
49use crate::source::SourceDistributionBuilder;
50use crate::{Error, LocalWheel, Reporter, RequiresDist};
51
52pub struct DistributionDatabase<'a, Context: BuildContext> {
65 build_context: &'a Context,
66 builder: SourceDistributionBuilder<'a, Context>,
67 client: ManagedClient<'a>,
68 reporter: Option<Arc<dyn Reporter>>,
69 content_addressed_cache: bool,
70}
71
72impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
73 pub fn new(
74 client: &'a RegistryClient,
75 build_context: &'a Context,
76 downloads_semaphore: Arc<Semaphore>,
77 ) -> Self {
78 let content_addressed_cache = uv_preview::is_enabled(PreviewFeature::ContentAddressedCache)
82 && !uv_extract::insecure_no_validate();
83 Self {
84 build_context,
85 builder: SourceDistributionBuilder::new(build_context),
86 client: ManagedClient::new(client, downloads_semaphore),
87 reporter: None,
88 content_addressed_cache,
89 }
90 }
91
92 #[must_use]
94 pub fn with_build_stack(self, build_stack: &'a BuildStack) -> Self {
95 Self {
96 builder: self.builder.with_build_stack(build_stack),
97 ..self
98 }
99 }
100
101 #[must_use]
103 pub fn with_reporter(self, reporter: Arc<dyn Reporter>) -> Self {
104 Self {
105 builder: self.builder.with_reporter(reporter.clone()),
106 reporter: Some(reporter),
107 ..self
108 }
109 }
110
111 fn handle_response_errors(&self, err: reqwest::Error) -> io::Error {
113 if err.is_timeout() {
114 io::Error::new(
116 io::ErrorKind::TimedOut,
117 format!(
118 "Failed to download distribution due to network timeout. Try increasing UV_HTTP_TIMEOUT (current value: {}s).",
119 self.client.unmanaged.read_timeout().as_secs()
120 ),
121 )
122 } else {
123 io::Error::other(err)
124 }
125 }
126
127 async fn lock_wheel(
136 wheel_entry: &CacheEntry,
137 filename: &WheelFilename,
138 ) -> Result<LockedFile, Error> {
139 #[cfg(windows)]
143 let lock_key = filename.stem();
144 #[cfg(not(windows))]
146 let lock_key = filename.cache_key();
147
148 let lock_entry = wheel_entry.with_file(format!("{lock_key}.lock"));
149 lock_entry.lock().await.map_err(Error::CacheLock)
150 }
151
152 fn validate_hashes(
154 dist: &BuiltDist,
155 hashes: ArchiveHashPolicy<'_>,
156 hashers: Vec<Hasher>,
157 ) -> Result<HashDigests, Error> {
158 let computed_hashes: HashDigests = hashers.into_iter().map(HashDigest::from).collect();
159 if hashes.requires_validation() && !hashes.matches(computed_hashes.as_slice()) {
160 return Err(Error::hash_mismatch(
161 dist.to_string(),
162 hashes.digests(),
163 computed_hashes.as_slice(),
164 ));
165 }
166 Ok(computed_hashes)
167 }
168
169 #[instrument(skip_all, fields(%dist))]
176 pub async fn get_or_build_wheel(
177 &self,
178 dist: &Dist,
179 tags: &Tags,
180 hashes: ArchiveHashPolicy<'_>,
181 ) -> Result<LocalWheel, Error> {
182 match dist {
183 Dist::Built(built) => self.get_wheel(built, hashes).await,
184 Dist::Source(source) => self.build_wheel(source, tags, hashes).await,
185 }
186 }
187
188 #[instrument(skip_all, fields(%dist))]
194 pub async fn get_installed_metadata(
195 &self,
196 dist: &InstalledDist,
197 ) -> Result<ArchiveMetadata, Error> {
198 if let Some(metadata) = self
200 .build_context
201 .dependency_metadata()
202 .get(dist.name(), Some(dist.version()))
203 {
204 return Ok(Metadata::from_dependency_metadata(metadata).into());
205 }
206
207 let metadata = dist
208 .read_metadata()
209 .map_err(|err| Error::ReadInstalled(Box::new(dist.clone()), err))?;
210
211 Ok(ArchiveMetadata::from_metadata23(metadata.clone()))
212 }
213
214 #[instrument(skip_all, fields(%dist))]
219 pub async fn get_or_build_wheel_metadata(
220 &self,
221 dist: &Dist,
222 hashes: MetadataHashPolicy<'_>,
223 ) -> Result<ArchiveMetadata, Error> {
224 match dist {
225 Dist::Built(built) => self.get_wheel_metadata(built, hashes).await,
226 Dist::Source(source) => {
227 self.build_wheel_metadata(&BuildableSource::Dist(source), hashes)
228 .await
229 }
230 }
231 }
232
233 async fn get_wheel(
239 &self,
240 dist: &BuiltDist,
241 hashes: ArchiveHashPolicy<'_>,
242 ) -> Result<LocalWheel, Error> {
243 match dist {
244 BuiltDist::Registry(wheels) => {
245 let wheel = wheels.best_wheel();
246 let url = wheel.file.url.to_url()?;
247 let size = wheel.file.size;
248
249 let wheel_entry = self.build_context.cache().entry(
251 CacheBucket::Wheels,
252 WheelCache::Index(&wheel.index).wheel_dir(wheel.name().as_ref()),
253 wheel.filename.cache_key(),
254 );
255
256 if url.scheme() == "file" {
258 let path = url
259 .to_file_path()
260 .map_err(|()| Error::NonFileUrl(url.clone()))?;
261 return self
262 .load_wheel(&path, &wheel.filename, wheel_entry, dist, hashes)
263 .await;
264 }
265
266 match self
268 .stream_wheel(
269 url.clone(),
270 dist.index(),
271 &wheel.filename,
272 size,
273 &wheel_entry,
274 dist,
275 hashes,
276 )
277 .await
278 {
279 Ok(archive) => Ok(LocalWheel {
280 dist: Dist::Built(dist.clone()),
281 archive: self
282 .build_context
283 .cache()
284 .archive(&archive.id)
285 .into_boxed_path(),
286 hashes: archive.hashes,
287 filename: wheel.filename.clone(),
288 cache: CacheInfo::default(),
289 build: None,
290 }),
291 Err(Error::Extract(name, err)) => {
292 if err.is_http_streaming_unsupported() {
293 warn!(
294 "Streaming unsupported for {dist}; downloading wheel to disk ({err})"
295 );
296 } else if err.is_http_streaming_failed() {
297 warn!("Streaming failed for {dist}; downloading wheel to disk ({err})");
298 } else {
299 return Err(Error::Extract(name, err));
300 }
301
302 let archive = self
305 .download_wheel(
306 url,
307 dist.index(),
308 &wheel.filename,
309 size,
310 &wheel_entry,
311 dist,
312 hashes,
313 )
314 .await?;
315
316 Ok(LocalWheel {
317 dist: Dist::Built(dist.clone()),
318 archive: self
319 .build_context
320 .cache()
321 .archive(&archive.id)
322 .into_boxed_path(),
323 hashes: archive.hashes,
324 filename: wheel.filename.clone(),
325 cache: CacheInfo::default(),
326 build: None,
327 })
328 }
329 Err(err) => Err(err),
330 }
331 }
332
333 BuiltDist::DirectUrl(wheel) => {
334 let wheel_entry = self.build_context.cache().entry(
336 CacheBucket::Wheels,
337 WheelCache::Url(&wheel.url).wheel_dir(wheel.name().as_ref()),
338 wheel.filename.cache_key(),
339 );
340
341 match self
343 .stream_wheel(
344 wheel.url.raw().clone(),
345 None,
346 &wheel.filename,
347 wheel.size,
348 &wheel_entry,
349 dist,
350 hashes,
351 )
352 .await
353 {
354 Ok(archive) => Ok(LocalWheel {
355 dist: Dist::Built(dist.clone()),
356 archive: self
357 .build_context
358 .cache()
359 .archive(&archive.id)
360 .into_boxed_path(),
361 hashes: archive.hashes,
362 filename: wheel.filename.clone(),
363 cache: CacheInfo::default(),
364 build: None,
365 }),
366 Err(Error::Extract(name, err)) => {
367 if err.is_http_streaming_unsupported() {
368 warn!(
369 "Streaming unsupported for {dist}; downloading wheel to disk ({err})"
370 );
371 } else if err.is_http_streaming_failed() {
372 warn!("Streaming failed for {dist}; downloading wheel to disk ({err})");
373 } else {
374 return Err(Error::Extract(name, err));
375 }
376
377 let archive = self
380 .download_wheel(
381 wheel.url.raw().clone(),
382 None,
383 &wheel.filename,
384 wheel.size,
385 &wheel_entry,
386 dist,
387 hashes,
388 )
389 .await?;
390 Ok(LocalWheel {
391 dist: Dist::Built(dist.clone()),
392 archive: self
393 .build_context
394 .cache()
395 .archive(&archive.id)
396 .into_boxed_path(),
397 hashes: archive.hashes,
398 filename: wheel.filename.clone(),
399 cache: CacheInfo::default(),
400 build: None,
401 })
402 }
403 Err(err) => Err(err),
404 }
405 }
406
407 BuiltDist::GitPath(wheel) => {
408 let fetch = self
410 .build_context
411 .git()
412 .fetch(
413 &wheel.git,
414 self.client.unmanaged.git_http_settings(wheel.git.url()),
415 self.build_context.cache().bucket(CacheBucket::Git),
416 self.reporter.clone().map(<dyn Reporter>::into_git_reporter),
417 )
418 .await?;
419
420 if wheel.git.lfs().enabled() && !fetch.lfs_ready() {
421 if GIT_LFS.is_err() {
422 return Err(Error::MissingWheelGitLfsArtifacts(
423 wheel.url.to_url(),
424 GitError::GitLfsNotFound,
425 ));
426 }
427 return Err(Error::MissingWheelGitLfsArtifacts(
428 wheel.url.to_url(),
429 GitError::GitLfsNotConfigured,
430 ));
431 }
432
433 let git_sha = fetch.git().precise().expect("Exact commit after checkout");
434 let cache_entry = self.build_context.cache().entry(
435 CacheBucket::Wheels,
436 WheelCache::Git(&wheel.url, git_sha.as_short_str()).root(),
437 wheel.filename.stem(),
438 );
439
440 let install_path = fetch.path().join(&wheel.install_path);
441
442 self.load_wheel(&install_path, &wheel.filename, cache_entry, dist, hashes)
443 .await
444 }
445
446 BuiltDist::Path(wheel) => {
447 let cache_entry = self.build_context.cache().entry(
448 CacheBucket::Wheels,
449 WheelCache::Url(&wheel.url).wheel_dir(wheel.name().as_ref()),
450 wheel.filename.cache_key(),
451 );
452
453 self.load_wheel(
454 &wheel.install_path,
455 &wheel.filename,
456 cache_entry,
457 dist,
458 hashes,
459 )
460 .await
461 }
462 }
463 }
464
465 async fn build_wheel(
471 &self,
472 dist: &SourceDist,
473 tags: &Tags,
474 hashes: ArchiveHashPolicy<'_>,
475 ) -> Result<LocalWheel, Error> {
476 let built_wheel = self
477 .builder
478 .download_and_build(&BuildableSource::Dist(dist), tags, hashes, &self.client)
479 .boxed_local()
480 .await?;
481
482 if !built_wheel.filename.is_compatible(tags) {
488 return if tags.is_cross() {
489 Err(Error::BuiltWheelIncompatibleTargetPlatform {
490 filename: built_wheel.filename,
491 python_platform: tags.python_platform().clone(),
492 python_version: PythonVersion {
493 version: tags.python_version(),
494 variant: if tags.is_freethreaded() {
495 PythonVariant::Freethreaded
496 } else {
497 PythonVariant::Default
498 },
499 },
500 })
501 } else {
502 Err(Error::BuiltWheelIncompatibleHostPlatform {
503 filename: built_wheel.filename,
504 python_platform: tags.python_platform().clone(),
505 python_version: PythonVersion {
506 version: tags.python_version(),
507 variant: if tags.is_freethreaded() {
508 PythonVariant::Freethreaded
509 } else {
510 PythonVariant::Default
511 },
512 },
513 })
514 };
515 }
516
517 let wheel_entry = CacheEntry::from_path(built_wheel.target.as_ref());
519 let _lock = Self::lock_wheel(&wheel_entry, &built_wheel.filename).await?;
520
521 match self.build_context.cache().resolve_link(&built_wheel.target) {
524 Ok(archive) => {
525 return Ok(LocalWheel {
526 dist: Dist::Source(dist.clone()),
527 archive: archive.into_boxed_path(),
528 filename: built_wheel.filename,
529 hashes: built_wheel.hashes,
530 cache: built_wheel.cache_info,
531 build: Some(built_wheel.build_info),
532 });
533 }
534 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
535 Err(err) => return Err(Error::CacheRead(err)),
536 }
537
538 let id = self
540 .unzip_wheel(
541 &built_wheel.path,
542 &built_wheel.target,
543 DistRef::Source(dist),
544 )
545 .await?;
546
547 Ok(LocalWheel {
548 dist: Dist::Source(dist.clone()),
549 archive: self.build_context.cache().archive(&id).into_boxed_path(),
550 hashes: built_wheel.hashes,
551 filename: built_wheel.filename,
552 cache: built_wheel.cache_info,
553 build: Some(built_wheel.build_info),
554 })
555 }
556
557 async fn get_wheel_metadata(
562 &self,
563 dist: &BuiltDist,
564 hashes: MetadataHashPolicy<'_>,
565 ) -> Result<ArchiveMetadata, Error> {
566 let hash_policy = match hashes.validation {
567 HashValidation::None => {
568 let compute_hashes = match hashes.collection {
569 HashCollection::None => false,
570 HashCollection::Url | HashCollection::All => match dist {
571 BuiltDist::Registry(dist) => {
572 hashes.collection == HashCollection::All
575 && dist.best_wheel().file.hashes.is_empty()
576 }
577 BuiltDist::DirectUrl(dist) => parse_url_hashes(&dist.url).is_none(),
578 BuiltDist::Path(_) | BuiltDist::GitPath(_) => true,
579 },
580 };
581 if compute_hashes {
582 ArchiveHashPolicy::Generate
583 } else {
584 ArchiveHashPolicy::None
585 }
586 }
587 HashValidation::Any(_) | HashValidation::All(_) => hashes.validation.into(),
588 };
589
590 if hash_policy == ArchiveHashPolicy::Generate {
593 let wheel = self.get_wheel(dist, hash_policy).await?;
594 let metadata = if let Some(metadata) = self
596 .build_context
597 .dependency_metadata()
598 .get(dist.name(), Some(dist.version()))
599 {
600 Metadata::from_dependency_metadata(metadata)
601 } else {
602 Metadata::from_metadata23(wheel.metadata()?)
603 };
604 let hashes = wheel.hashes;
605 return Ok(ArchiveMetadata { metadata, hashes });
606 }
607
608 if let Some(metadata) = self
610 .build_context
611 .dependency_metadata()
612 .get(dist.name(), Some(dist.version()))
613 {
614 return Ok(Metadata::from_dependency_metadata(metadata).into());
615 }
616
617 let result = self
618 .client
619 .managed(|client| {
620 client
621 .wheel_metadata(
622 dist,
623 self.build_context.git(),
624 self.build_context.capabilities(),
625 self.reporter.clone().map(<dyn Reporter>::into_git_reporter),
626 )
627 .boxed_local()
628 })
629 .await;
630
631 match result {
632 Ok(metadata) => {
633 Ok(ArchiveMetadata::from_metadata23(metadata))
635 }
636 Err(err) if err.is_http_streaming_unsupported() => {
637 warn!(
638 "Streaming unsupported when fetching metadata for {dist}; downloading wheel directly ({err})"
639 );
640
641 let wheel = self.get_wheel(dist, hash_policy).await?;
644 let metadata = wheel.metadata()?;
645 let hashes = wheel.hashes;
646 Ok(ArchiveMetadata {
647 metadata: Metadata::from_metadata23(metadata),
648 hashes,
649 })
650 }
651 Err(err) => Err(err.into()),
652 }
653 }
654
655 pub async fn build_wheel_metadata(
660 &self,
661 source: &BuildableSource<'_>,
662 hashes: MetadataHashPolicy<'_>,
663 ) -> Result<ArchiveMetadata, Error> {
664 if let Some(dist) = source.as_dist() {
666 if let Some(metadata) = self
667 .build_context
668 .dependency_metadata()
669 .get(dist.name(), dist.version())
670 {
671 self.builder.resolve_revision(source, &self.client).await?;
674
675 return Ok(Metadata::from_dependency_metadata(metadata).into());
676 }
677 }
678
679 let url_hashes = if let BuildableSource::Dist(SourceDist::DirectUrl(dist)) = source {
680 parse_url_hashes(&dist.url)
681 } else if let BuildableSource::Url(SourceUrl::Direct(url)) = source {
682 parse_url_hashes(url.url)
683 } else {
684 None
685 };
686
687 let build_hash_policy = match hashes.validation {
688 HashValidation::None => match hashes.collection {
689 HashCollection::None => ArchiveHashPolicy::None,
690 HashCollection::Url | HashCollection::All => match url_hashes.as_ref() {
693 Some(digests) => ArchiveHashPolicy::All(digests.as_slice()),
694 None => ArchiveHashPolicy::Generate,
695 },
696 },
697 HashValidation::Any(_) | HashValidation::All(_) => hashes.validation.into(),
698 };
699 let ArchiveMetadata { metadata, hashes } = self
700 .builder
701 .download_and_build_metadata(source, build_hash_policy, &self.client)
702 .boxed_local()
703 .await?;
704
705 Ok(ArchiveMetadata {
709 metadata: metadata.with_force_relative(true),
710 hashes,
711 })
712 }
713
714 pub async fn requires_dist(
716 &self,
717 path: &Path,
718 pyproject_toml: &PyProjectToml,
719 ) -> Result<Option<RequiresDist>, Error> {
720 self.builder
721 .source_tree_requires_dist(
722 path,
723 pyproject_toml,
724 self.client.unmanaged.credentials_cache(),
725 )
726 .await
727 }
728
729 async fn stream_wheel(
735 &self,
736 url: DisplaySafeUrl,
737 index: Option<&IndexUrl>,
738 filename: &WheelFilename,
739 progress_size_hint: Option<u64>,
740 wheel_entry: &CacheEntry,
741 dist: &BuiltDist,
742 hashes: ArchiveHashPolicy<'_>,
743 ) -> Result<Archive, Error> {
744 let expected_size = match dist {
746 BuiltDist::Registry(dist) if dist.best_wheel().size_is_authoritative => {
747 progress_size_hint
748 }
749 BuiltDist::DirectUrl(_) => progress_size_hint,
750 _ => None,
751 };
752
753 let _lock = Self::lock_wheel(wheel_entry, filename).await?;
755
756 let http_entry = wheel_entry.with_file(format!("{}.http", filename.cache_key()));
758
759 let download = |response: reqwest::Response, _: &mut RetryState| {
760 async {
761 let hashes = if let BuiltDist::Registry(wheels) = dist {
762 hashes.with_index_hashes(wheels.best_wheel().file.hashes.as_slice())
763 } else {
764 hashes
765 };
766
767 let progress_size_hint = progress_size_hint.or_else(|| content_length(&response));
768
769 let progress = self.reporter.as_ref().map(|reporter| {
770 (
771 reporter,
772 reporter.on_download_start(dist.name(), progress_size_hint),
773 )
774 });
775
776 let reader = response
777 .bytes_stream()
778 .map_err(|err| self.handle_response_errors(err))
779 .into_async_read();
780
781 let algorithms = http_hash_algorithms(hashes);
783 let mut hashers = algorithms.into_iter().map(Hasher::from).collect::<Vec<_>>();
784 let mut hasher = uv_extract::hash::HashReader::new(reader.compat(), &mut hashers);
785
786 let extractor = WheelExtractor::new(
788 self.build_context.cache().root(),
789 self.content_addressed_cache,
790 )
791 .map_err(Error::CacheWrite)?;
792
793 let mut extracted = match progress {
794 Some((reporter, progress)) => {
795 let mut reader = ProgressReader::new(&mut hasher, progress, &**reporter);
796 extractor
797 .extract_streaming(&mut reader)
798 .await
799 .map_err(|err| Error::Extract(filename.to_string(), err))?
800 }
801 None => extractor
802 .extract_streaming(&mut hasher)
803 .await
804 .map_err(|err| Error::Extract(filename.to_string(), err))?,
805 };
806 hasher.finish().await.map_err(Error::HashExhaustion)?;
808 let actual_size = hasher.bytes_read();
809 if let Some(expected) = expected_size
810 && actual_size != expected
811 {
812 return Err(Error::MismatchedSize {
813 distribution: dist.to_string(),
814 expected,
815 actual: actual_size,
816 });
817 }
818
819 let computed_hashes = Self::validate_hashes(dist, hashes, hashers)?;
820
821 extracted.validate_and_heal_record(dist)?;
824
825 let id = self
827 .persist_extracted_wheel(extracted, wheel_entry.path())
828 .await?;
829
830 if let Some((reporter, progress)) = progress {
831 reporter.on_download_complete(dist.name(), progress);
832 }
833
834 Ok(Archive::new(
835 id,
836 computed_hashes,
837 filename.clone(),
838 Some(actual_size),
839 ))
840 }
841 .instrument(info_span!("wheel", wheel = %dist))
842 };
843
844 let req = self.request(url.clone())?;
846
847 let cache_control = match self.client.unmanaged.connectivity() {
849 Connectivity::Online
850 if let Some(header) = index.and_then(|index| {
851 self.build_context
852 .locations()
853 .artifact_cache_control_for(index)
854 }) =>
855 {
856 CacheControl::Override(header)
857 }
858 Connectivity::Online => CacheControl::from(
859 self.build_context
860 .cache()
861 .freshness(&http_entry, Some(&filename.name), None)
862 .map_err(Error::CacheRead)?,
863 ),
864 Connectivity::Offline => CacheControl::AllowStale,
865 };
866
867 let archive = self
868 .client
869 .managed(|client| {
870 client.cached_client().get_serde_with_retry(
871 req,
872 &http_entry,
873 cache_control.clone(),
874 download,
875 )
876 })
877 .await
878 .map_err(|err| match err {
879 CachedClientError::Callback { err, .. } => err,
880 CachedClientError::Client(err) => Error::Client(err),
881 })?;
882
883 if let (Some(expected), Some(actual)) = (expected_size, archive.size)
884 && expected != actual
885 {
886 return Err(Error::MismatchedSize {
887 distribution: dist.to_string(),
888 expected,
889 actual,
890 });
891 }
892
893 let archive = Some(archive)
895 .filter(|archive| archive.has_digests(hashes))
896 .filter(|archive| archive.exists(self.build_context.cache()))
897 .filter(|archive| expected_size.is_none() || archive.size.is_some());
898
899 let archive = if let Some(archive) = archive {
900 archive
901 } else {
902 self.client
903 .managed(async |client| {
904 client
905 .cached_client()
906 .skip_cache_with_retry(
907 self.request(url)?,
908 &http_entry,
909 cache_control,
910 download,
911 )
912 .await
913 .map_err(|err| match err {
914 CachedClientError::Callback { err, .. } => err,
915 CachedClientError::Client(err) => Error::Client(err),
916 })
917 })
918 .await?
919 };
920
921 Ok(archive)
922 }
923
924 async fn download_wheel(
930 &self,
931 url: DisplaySafeUrl,
932 index: Option<&IndexUrl>,
933 filename: &WheelFilename,
934 progress_size_hint: Option<u64>,
935 wheel_entry: &CacheEntry,
936 dist: &BuiltDist,
937 hashes: ArchiveHashPolicy<'_>,
938 ) -> Result<Archive, Error> {
939 let expected_size = match dist {
941 BuiltDist::Registry(dist) if dist.best_wheel().size_is_authoritative => {
942 progress_size_hint
943 }
944 BuiltDist::DirectUrl(_) => progress_size_hint,
945 _ => None,
946 };
947
948 let _lock = Self::lock_wheel(wheel_entry, filename).await?;
950
951 let http_entry = wheel_entry.with_file(format!("{}.http", filename.cache_key()));
953
954 let download_url = url.clone();
955
956 let download = async |response, retry_state: &mut RetryState| {
957 self.download_wheel_response(
958 response,
959 &download_url,
960 retry_state,
961 filename,
962 progress_size_hint,
963 expected_size,
964 wheel_entry,
965 dist,
966 hashes,
967 )
968 .instrument(info_span!("wheel", wheel = %dist))
969 .await
970 };
971
972 let req = self.request(url.clone())?;
974
975 let cache_control = match self.client.unmanaged.connectivity() {
977 Connectivity::Online
978 if let Some(header) = index.and_then(|index| {
979 self.build_context
980 .locations()
981 .artifact_cache_control_for(index)
982 }) =>
983 {
984 CacheControl::Override(header)
985 }
986 Connectivity::Online => CacheControl::from(
987 self.build_context
988 .cache()
989 .freshness(&http_entry, Some(&filename.name), None)
990 .map_err(Error::CacheRead)?,
991 ),
992 Connectivity::Offline => CacheControl::AllowStale,
993 };
994
995 let archive = self
996 .client
997 .managed(|client| {
998 client.cached_client().get_serde_with_retry(
999 req,
1000 &http_entry,
1001 cache_control.clone(),
1002 download,
1003 )
1004 })
1005 .await
1006 .map_err(|err| match err {
1007 CachedClientError::Callback { err, .. } => err,
1008 CachedClientError::Client(err) => Error::Client(err),
1009 })?;
1010
1011 if let (Some(expected), Some(actual)) = (expected_size, archive.size)
1012 && expected != actual
1013 {
1014 return Err(Error::MismatchedSize {
1015 distribution: dist.to_string(),
1016 expected,
1017 actual,
1018 });
1019 }
1020
1021 let archive = Some(archive)
1023 .filter(|archive| archive.has_digests(hashes))
1024 .filter(|archive| archive.exists(self.build_context.cache()))
1025 .filter(|archive| expected_size.is_none() || archive.size.is_some());
1026
1027 let archive = if let Some(archive) = archive {
1028 archive
1029 } else {
1030 self.client
1031 .managed(async |client| {
1032 client
1033 .cached_client()
1034 .skip_cache_with_retry(
1035 self.request(url)?,
1036 &http_entry,
1037 cache_control,
1038 download,
1039 )
1040 .await
1041 .map_err(|err| match err {
1042 CachedClientError::Callback { err, .. } => err,
1043 CachedClientError::Client(err) => Error::Client(err),
1044 })
1045 })
1046 .await?
1047 };
1048
1049 Ok(archive)
1050 }
1051
1052 async fn download_wheel_response(
1060 &self,
1061 mut response: reqwest::Response,
1062 url: &DisplaySafeUrl,
1063 retry_state: &mut RetryState,
1064 filename: &WheelFilename,
1065 progress_size_hint: Option<u64>,
1066 expected_size: Option<u64>,
1067 wheel_entry: &CacheEntry,
1068 dist: &BuiltDist,
1069 hashes: ArchiveHashPolicy<'_>,
1070 ) -> Result<Archive, Error> {
1071 let hashes = if let BuiltDist::Registry(wheels) = dist {
1072 hashes.with_index_hashes(wheels.best_wheel().file.hashes.as_slice())
1073 } else {
1074 hashes
1075 };
1076
1077 let progress_size_hint = progress_size_hint.or_else(|| content_length(&response));
1078 let mut download_size = content_length(&response).or(expected_size);
1079
1080 let progress = self.reporter.as_ref().map(|reporter| {
1081 (
1082 reporter,
1083 reporter.on_download_start(dist.name(), progress_size_hint),
1084 )
1085 });
1086
1087 let algorithms = http_hash_algorithms(hashes);
1088
1089 let temp_file =
1091 tempfile::tempfile_in(self.build_context.cache().root()).map_err(Error::CacheWrite)?;
1092 let mut writer = tokio::io::BufWriter::new(fs_err::tokio::File::from_std(
1093 fs_err::File::from_parts(temp_file, self.build_context.cache().root()),
1095 ));
1096
1097 let mut hashers = algorithms.into_iter().map(Hasher::from).collect::<Vec<_>>();
1099 let mut bytes_retrieved = 0;
1103 let mut resumed_at = None;
1105 let mut resumed_range: Option<ContentRangeBytes> = None;
1107
1108 loop {
1114 if response.status() == reqwest::StatusCode::OK
1117 && let (Some(expected), Some(actual)) = (expected_size, content_length(&response))
1118 && expected != actual
1119 {
1120 return Err(Error::MismatchedContentLength {
1121 distribution: dist.to_string(),
1122 expected,
1123 actual,
1124 });
1125 }
1126
1127 let supports_range_requests = response.status() == reqwest::StatusCode::PARTIAL_CONTENT
1130 || response
1131 .headers()
1132 .get(reqwest::header::ACCEPT_RANGES)
1133 .is_some_and(|value| value == "bytes");
1134
1135 let replaces_partial_download =
1138 resumed_at.is_some() && response.status() != reqwest::StatusCode::PARTIAL_CONTENT;
1139 if replaces_partial_download {
1140 writer
1141 .get_mut()
1142 .set_len(0)
1143 .await
1144 .map_err(Error::CacheWrite)?;
1145 writer
1146 .seek(io::SeekFrom::Start(0))
1147 .await
1148 .map_err(Error::CacheWrite)?;
1149 hashers = http_hash_algorithms(hashes)
1150 .into_iter()
1151 .map(Hasher::from)
1152 .collect();
1153 bytes_retrieved = 0;
1154 }
1155
1156 let reader = response
1157 .bytes_stream()
1158 .map_err(|err| self.handle_response_errors(err))
1159 .into_async_read();
1160 let mut hasher = uv_extract::hash::HashReader::new(reader.compat(), &mut hashers);
1161
1162 let copy_result = match progress {
1166 Some((reporter, progress)) => {
1167 let mut reader = ProgressReader::new(&mut hasher, progress, &**reporter);
1170
1171 tokio::io::copy(&mut reader, &mut writer)
1172 .await
1173 .map_err(Error::CacheWrite)
1174 }
1175 None => tokio::io::copy(&mut hasher, &mut writer)
1176 .await
1177 .map_err(Error::CacheWrite),
1178 };
1179
1180 bytes_retrieved += hasher.bytes_read();
1181
1182 let interrupted = copy_result.is_err();
1183 let err = match copy_result {
1184 Ok(_) => {
1187 let Some(range) = resumed_range else {
1190 break;
1191 };
1192
1193 if bytes_retrieved != range.last_byte + 1 {
1199 return Err(Error::MismatchedRangeSize {
1200 distribution: dist.to_string(),
1201 expected: range.last_byte - range.first_byte + 1,
1202 actual: hasher.bytes_read(),
1203 });
1204 }
1205
1206 if bytes_retrieved == range.complete_length {
1209 break;
1210 }
1211
1212 Error::CacheWrite(io::Error::new(
1218 io::ErrorKind::UnexpectedEof,
1219 "Range response did not complete the download",
1220 ))
1221 }
1222 Err(err) => err,
1225 };
1226
1227 if replaces_partial_download || !supports_range_requests {
1236 return Err(err);
1237 }
1238
1239 writer.flush().await.map_err(Error::CacheWrite)?;
1244 let offset = writer
1245 .get_mut()
1246 .stream_position()
1247 .await
1248 .map_err(Error::CacheWrite)?;
1249 if offset == 0
1250 || offset != bytes_retrieved
1251 || resumed_at.is_some_and(|previous| offset <= previous)
1252 {
1253 return Err(err);
1254 }
1255
1256 if interrupted {
1259 let Some(backoff) = retry_state.should_retry(&err, 0) else {
1260 return Err(err);
1261 };
1262 retry_state.sleep_backoff(backoff).await;
1263 }
1264
1265 debug!("Resuming download of {url} at byte {offset}");
1267 let resumed_response = retry_state
1268 .send(self.request_with_offset(url.clone(), offset))
1269 .await?;
1270
1271 if resumed_response.status() == reqwest::StatusCode::RANGE_NOT_SATISFIABLE {
1274 debug!("Range not satisfiable while resuming {url}; abandoning resumed download");
1275 return Err(err);
1276 }
1277 resumed_response.error_for_status_ref()?;
1278
1279 resumed_range = if resumed_response.status() == reqwest::StatusCode::PARTIAL_CONTENT {
1280 let Some(range) = content_range(&resumed_response, offset, download_size) else {
1283 warn!(
1284 "Invalid range request response from server that declares HTTP range \
1285 request support, abandoning resumed download: {url}"
1286 );
1287 return Err(err);
1288 };
1289 download_size = Some(range.complete_length);
1290 Some(range)
1291 } else {
1292 None
1295 };
1296
1297 response = resumed_response;
1298 resumed_at = Some(offset);
1299 }
1300
1301 if let Some(expected) = expected_size
1304 && bytes_retrieved != expected
1305 {
1306 return Err(Error::MismatchedSize {
1307 distribution: dist.to_string(),
1308 expected,
1309 actual: bytes_retrieved,
1310 });
1311 }
1312
1313 let extractor = WheelExtractor::new(
1315 self.build_context.cache().root(),
1316 self.content_addressed_cache,
1317 )
1318 .map_err(Error::CacheWrite)?;
1319 let mut file = writer.into_inner();
1320 file.seek(io::SeekFrom::Start(0))
1321 .await
1322 .map_err(Error::CacheWrite)?;
1323
1324 let file = file.into_std().await;
1325 let mut extracted = tokio::task::spawn_blocking(move || extractor.extract_seekable(file))
1326 .await?
1327 .map_err(|err| Error::Extract(filename.to_string(), err))?;
1328 let computed_hashes = Self::validate_hashes(dist, hashes, hashers)?;
1329
1330 extracted.validate_and_heal_record(dist)?;
1333
1334 let id = self
1336 .persist_extracted_wheel(extracted, wheel_entry.path())
1337 .await?;
1338
1339 if let Some((reporter, progress)) = progress {
1340 reporter.on_download_complete(dist.name(), progress);
1341 }
1342
1343 Ok(Archive::new(
1344 id,
1345 computed_hashes,
1346 filename.clone(),
1347 Some(bytes_retrieved),
1348 ))
1349 }
1350
1351 async fn load_wheel(
1353 &self,
1354 path: &Path,
1355 filename: &WheelFilename,
1356 wheel_entry: CacheEntry,
1357 dist: &BuiltDist,
1358 hashes: ArchiveHashPolicy<'_>,
1359 ) -> Result<LocalWheel, Error> {
1360 let _lock = Self::lock_wheel(&wheel_entry, filename).await?;
1362
1363 let modified = Timestamp::from_path(path).map_err(Error::CacheRead)?;
1365
1366 let pointer_entry = wheel_entry.with_file(format!("{}.rev", filename.cache_key()));
1368 let pointer = PathArchivePointer::read_from(&pointer_entry)?;
1369
1370 let archive = pointer
1372 .filter(|pointer| pointer.is_up_to_date(modified))
1373 .map(PathArchivePointer::into_archive)
1374 .filter(|archive| archive.has_digests(hashes));
1375
1376 let caller_hashes = hashes;
1378 let hashes = if let BuiltDist::Registry(wheels) = dist {
1379 hashes.with_index_hashes(wheels.best_wheel().file.hashes.as_slice())
1380 } else {
1381 hashes
1382 };
1383
1384 if let Some(archive) = archive {
1386 Ok(LocalWheel {
1387 dist: Dist::Built(dist.clone()),
1388 archive: self
1389 .build_context
1390 .cache()
1391 .archive(&archive.id)
1392 .into_boxed_path(),
1393 hashes: archive.hashes,
1394 filename: filename.clone(),
1395 cache: CacheInfo::from_timestamp(modified),
1396 build: None,
1397 })
1398 } else if hashes.is_none() {
1399 let archive = Archive::new(
1401 self.unzip_wheel(path, wheel_entry.path(), DistRef::Built(dist))
1402 .await?,
1403 HashDigests::empty(),
1404 filename.clone(),
1405 None,
1406 );
1407
1408 let pointer = PathArchivePointer {
1410 timestamp: modified,
1411 archive: archive.clone(),
1412 };
1413 pointer.write_to(&pointer_entry).await?;
1414
1415 Ok(LocalWheel {
1416 dist: Dist::Built(dist.clone()),
1417 archive: self
1418 .build_context
1419 .cache()
1420 .archive(&archive.id)
1421 .into_boxed_path(),
1422 hashes: archive.hashes,
1423 filename: filename.clone(),
1424 cache: CacheInfo::from_timestamp(modified),
1425 build: None,
1426 })
1427 } else {
1428 let file = fs_err::tokio::File::open(path)
1430 .await
1431 .map_err(Error::CacheRead)?;
1432 let extractor = WheelExtractor::new(
1433 self.build_context.cache().root(),
1434 self.content_addressed_cache,
1435 )
1436 .map_err(Error::CacheWrite)?;
1437
1438 let mut algorithms = caller_hashes.algorithms();
1440 algorithms.extend(hashes.algorithms());
1441 algorithms.sort();
1442 algorithms.dedup();
1443 let mut hashers = algorithms.into_iter().map(Hasher::from).collect::<Vec<_>>();
1444 let mut hasher = uv_extract::hash::HashReader::new(file, &mut hashers);
1445
1446 let mut extracted = extractor
1448 .extract_streaming(&mut hasher)
1449 .await
1450 .map_err(|err| Error::Extract(filename.to_string(), err))?;
1451
1452 hasher.finish().await.map_err(Error::HashExhaustion)?;
1454
1455 let computed_hashes = Self::validate_hashes(dist, hashes, hashers)?;
1456
1457 extracted.validate_and_heal_record(dist)?;
1460
1461 let id = self
1463 .persist_extracted_wheel(extracted, wheel_entry.path())
1464 .await?;
1465
1466 let archive = Archive::new(id, computed_hashes, filename.clone(), None);
1468
1469 let pointer = PathArchivePointer {
1471 timestamp: modified,
1472 archive: archive.clone(),
1473 };
1474 pointer.write_to(&pointer_entry).await?;
1475
1476 Ok(LocalWheel {
1477 dist: Dist::Built(dist.clone()),
1478 archive: self
1479 .build_context
1480 .cache()
1481 .archive(&archive.id)
1482 .into_boxed_path(),
1483 hashes: archive.hashes,
1484 filename: filename.clone(),
1485 cache: CacheInfo::from_timestamp(modified),
1486 build: None,
1487 })
1488 }
1489 }
1490
1491 async fn unzip_wheel(
1493 &self,
1494 path: &Path,
1495 target: &Path,
1496 dist: DistRef<'_>,
1497 ) -> Result<ArchiveId, Error> {
1498 let content_addressed_cache = self.content_addressed_cache;
1499
1500 let mut extracted = tokio::task::spawn_blocking({
1501 let path = path.to_owned();
1502 let root = self.build_context.cache().root().to_path_buf();
1503 move || -> Result<_, Error> {
1504 let extractor = WheelExtractor::new(&root, content_addressed_cache)
1506 .map_err(Error::CacheWrite)?;
1507 let reader = fs_err::File::open(&path).map_err(Error::CacheWrite)?;
1508 extractor
1509 .extract_seekable(reader)
1510 .map_err(|err| Error::Extract(path.to_string_lossy().into_owned(), err))
1511 }
1512 })
1513 .await??;
1514
1515 extracted.validate_and_heal_record(dist)?;
1517
1518 let id = self.persist_extracted_wheel(extracted, target).await?;
1520
1521 Ok(id)
1522 }
1523
1524 async fn persist_extracted_wheel(
1529 &self,
1530 extracted: ExtractedWheel,
1531 target: &Path,
1532 ) -> Result<ArchiveId, Error> {
1533 let (temp_dir, hashed_wheel) = extracted.into_parts();
1534 let cache = self.build_context.cache();
1535 let (temp_dir, id) = if let Some(HashedWheel { files, tree }) = hashed_wheel {
1536 let digest = DirectoryDigest::from(tree.hash());
1537 let id = ArchiveId::from_digest(digest.into());
1538 let cache = cache.clone();
1539 let temp_dir = tokio::task::spawn_blocking(move || {
1540 persist_archive_files(&cache, temp_dir.path(), &files)
1541 .map_err(Error::CacheWrite)?;
1542 Ok::<_, Error>(temp_dir)
1543 })
1544 .await??;
1545 (temp_dir, id)
1546 } else {
1547 (temp_dir, ArchiveId::default())
1548 };
1549
1550 cache
1551 .persist_with_id(temp_dir, target, id)
1552 .await
1553 .map_err(Error::CacheWrite)
1554 }
1555
1556 fn request(&self, url: DisplaySafeUrl) -> Result<reqwest::Request, reqwest::Error> {
1558 self.client
1559 .unmanaged
1560 .uncached_client(&url)
1561 .get(Url::from(url))
1562 .header(
1563 "accept-encoding",
1567 reqwest::header::HeaderValue::from_static("identity"),
1568 )
1569 .build()
1570 }
1571
1572 fn request_with_offset(&self, url: DisplaySafeUrl, offset: u64) -> RequestBuilder<'_> {
1576 self.client
1577 .unmanaged
1578 .uncached_client(&url)
1579 .get(Url::from(url))
1580 .header(
1581 "accept-encoding",
1582 reqwest::header::HeaderValue::from_static("identity"),
1583 )
1584 .header(reqwest::header::RANGE, format!("bytes={offset}-"))
1585 }
1586
1587 pub fn client(&self) -> &ManagedClient<'a> {
1589 &self.client
1590 }
1591}
1592
1593fn persist_archive_files(cache: &Cache, archive: &Path, files: &[HashedFile]) -> io::Result<()> {
1595 initialize_rayon_once();
1596 let targets = files
1597 .par_iter()
1598 .filter(|file| !file.path().ends_with("RECORD"))
1600 .map(|file| {
1601 let id = ArchiveFileId::from_digest(&file.object_digest_hex());
1602 (archive.join(file.path()), cache.archive_file(&id))
1603 })
1604 .collect::<Vec<_>>();
1605
1606 let mut shards: FxHashMap<&Path, Vec<_>> = FxHashMap::default();
1609 for (source, target) in &targets {
1610 let Some(parent) = target.parent() else {
1611 return Err(io::Error::new(
1612 io::ErrorKind::InvalidInput,
1613 "archive file path must have a parent directory",
1614 ));
1615 };
1616 shards.entry(parent).or_default().push((source, target));
1617 }
1618
1619 let mut shards = shards
1620 .into_iter()
1621 .map(|(parent, files)| (parent, files, Ok(())))
1622 .collect::<Vec<_>>();
1623 shards.sort_unstable_by_key(|(_, files, _)| Reverse(files.len()));
1625
1626 in_place_scope(|scope| -> io::Result<()> {
1629 for (parent, files, result) in &mut shards {
1630 fs_err::create_dir_all(parent)?;
1631 scope.spawn(move |_| {
1632 *result = files
1633 .iter()
1634 .try_for_each(|(source, target)| persist_archive_file(source, target));
1635 });
1636 }
1637 Ok(())
1638 })?;
1639
1640 shards.into_iter().try_for_each(|(_, _, result)| result)
1641}
1642
1643fn persist_archive_file(src: &Path, dst: &Path) -> io::Result<()> {
1645 match fs_err::hard_link(src, dst) {
1648 Ok(()) => return Ok(()),
1649 Err(_) if dst.try_exists()? => {}
1650 Err(_) => return uv_fs::copy_atomic_sync(src, dst),
1651 }
1652
1653 if let Err(err) = fs_err::remove_file(src)
1655 && err.kind() != io::ErrorKind::NotFound
1656 {
1657 return Err(err);
1658 }
1659
1660 fs_err::hard_link(dst, src).or_else(|_| uv_fs::copy_atomic_sync(dst, src))
1661}
1662
1663pub struct ManagedClient<'a> {
1665 pub unmanaged: &'a RegistryClient,
1666 control: Arc<Semaphore>,
1667}
1668
1669impl<'a> ManagedClient<'a> {
1670 fn new(client: &'a RegistryClient, control: Arc<Semaphore>) -> Self {
1672 ManagedClient {
1673 unmanaged: client,
1674 control,
1675 }
1676 }
1677
1678 pub async fn managed<F, T>(&self, f: impl FnOnce(&'a RegistryClient) -> F) -> T
1683 where
1684 F: Future<Output = T>,
1685 {
1686 let _permit = self.control.acquire().await.unwrap();
1687 f(self.unmanaged).await
1688 }
1689
1690 pub async fn manual<F, T>(&'a self, f: impl FnOnce(&'a RegistryClient, &'a Semaphore) -> F) -> T
1698 where
1699 F: Future<Output = T>,
1700 {
1701 f(self.unmanaged, &self.control).await
1702 }
1703}
1704
1705fn content_length(response: &reqwest::Response) -> Option<u64> {
1707 response
1708 .headers()
1709 .get(reqwest::header::CONTENT_LENGTH)
1710 .and_then(|val| val.to_str().ok())
1711 .and_then(|val| val.parse::<u64>().ok())
1712}
1713
1714fn content_range(
1716 response: &reqwest::Response,
1717 offset: u64,
1718 download_size: Option<u64>,
1719) -> Option<ContentRangeBytes> {
1720 let range = response
1721 .headers()
1722 .get(reqwest::header::CONTENT_RANGE)
1723 .and_then(|value| value.to_str().ok())
1724 .and_then(ContentRange::parse)?;
1725
1726 let range = match range {
1727 ContentRange::Bytes(range) => range,
1728 ContentRange::UnboundBytes(ContentRangeUnbound {
1729 first_byte,
1730 last_byte,
1731 }) => ContentRangeBytes {
1732 first_byte,
1733 last_byte,
1734 complete_length: download_size?,
1735 },
1736 ContentRange::Unsatisfied(_) => return None,
1737 };
1738
1739 if range.first_byte != offset
1740 || range.last_byte >= range.complete_length
1741 || download_size.is_some_and(|size| size != range.complete_length)
1742 {
1743 return None;
1744 }
1745
1746 Some(range)
1747}
1748
1749struct ProgressReader<'a, R> {
1751 reader: R,
1752 index: usize,
1753 reporter: &'a dyn Reporter,
1754}
1755
1756impl<'a, R> ProgressReader<'a, R> {
1757 fn new(reader: R, index: usize, reporter: &'a dyn Reporter) -> Self {
1759 Self {
1760 reader,
1761 index,
1762 reporter,
1763 }
1764 }
1765}
1766
1767impl<R> AsyncRead for ProgressReader<'_, R>
1768where
1769 R: AsyncRead + Unpin,
1770{
1771 fn poll_read(
1772 mut self: Pin<&mut Self>,
1773 cx: &mut Context<'_>,
1774 buf: &mut ReadBuf<'_>,
1775 ) -> Poll<io::Result<()>> {
1776 Pin::new(&mut self.as_mut().reader)
1777 .poll_read(cx, buf)
1778 .map_ok(|()| {
1779 self.reporter
1780 .on_download_progress(self.index, buf.filled().len() as u64);
1781 })
1782 }
1783}
1784
1785#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1789pub struct HttpArchivePointer {
1790 archive: Archive,
1791}
1792
1793impl HttpArchivePointer {
1794 pub fn read_from(path: impl AsRef<Path>) -> Result<Option<Self>, Error> {
1796 match fs_err::File::open(path.as_ref()) {
1797 Ok(file) => {
1798 let data = DataWithCachePolicy::from_reader(file)?.data;
1799 let archive = rmp_serde::from_slice::<Archive>(&data)?;
1800 Ok(Some(Self { archive }))
1801 }
1802 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
1803 Err(err) => Err(Error::CacheRead(err)),
1804 }
1805 }
1806
1807 pub fn into_archive(self) -> Archive {
1809 self.archive
1810 }
1811
1812 pub fn to_cache_info(&self) -> CacheInfo {
1814 CacheInfo::default()
1815 }
1816
1817 pub fn to_build_info(&self) -> Option<BuildInfo> {
1819 None
1820 }
1821}
1822
1823#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1827pub struct PathArchivePointer {
1828 timestamp: Timestamp,
1829 archive: Archive,
1830}
1831
1832impl PathArchivePointer {
1833 pub fn read_from(path: impl AsRef<Path>) -> Result<Option<Self>, Error> {
1835 match fs_err::read(path) {
1836 Ok(cached) => Ok(Some(rmp_serde::from_slice::<Self>(&cached)?)),
1837 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
1838 Err(err) => Err(Error::CacheRead(err)),
1839 }
1840 }
1841
1842 async fn write_to(&self, entry: &CacheEntry) -> Result<(), Error> {
1844 write_atomic(entry.path(), rmp_serde::to_vec(&self)?)
1845 .await
1846 .map_err(Error::CacheWrite)
1847 }
1848
1849 pub fn is_up_to_date(&self, modified: Timestamp) -> bool {
1851 self.timestamp == modified
1852 }
1853
1854 pub fn into_archive(self) -> Archive {
1856 self.archive
1857 }
1858
1859 pub fn to_cache_info(&self) -> CacheInfo {
1861 CacheInfo::from_timestamp(self.timestamp)
1862 }
1863
1864 pub fn to_build_info(&self) -> Option<BuildInfo> {
1866 None
1867 }
1868}