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};
18
19use uv_cache::{ArchiveFileId, ArchiveId, Cache, CacheBucket, CacheEntry, WheelCache};
20use uv_cache_info::{CacheInfo, Timestamp};
21use uv_client::{
22 CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient,
23 RequestBuilder, RetryState,
24};
25use uv_distribution_filename::WheelFilename;
26use uv_distribution_types::{
27 ArchiveHashPolicy, BuildInfo, BuildableSource, BuiltDist, Dist, DistRef, HashCollection,
28 HashValidation, Hashed, IndexUrl, InstalledDist, MetadataHashPolicy, Name, ResolutionRecorder,
29 SourceDist, SourceUrl, parse_url_hashes,
30};
31use uv_extract::dirhash::{DirectoryDigest, HashedFile};
32use uv_extract::hash::Hasher;
33use uv_fs::{LockedFile, write_atomic};
34use uv_git::{GIT_LFS, GitError};
35use uv_normalize::PackageName;
36use uv_pep440::Version;
37use uv_platform_tags::Tags;
38use uv_preview::PreviewFeature;
39use uv_pypi_types::{HashDigest, HashDigests, PyProjectToml, ResolutionMetadata};
40use uv_python_types::PythonVariant;
41use uv_redacted::SafeUrl;
42use uv_threads::initialize_rayon_once;
43use uv_types::{BuildContext, BuildStack};
44
45use crate::archive::Archive;
46use crate::error::PythonVersion;
47use crate::extracted_wheel::{ExtractedWheel, HashedWheel, WheelExtractor};
48use crate::hash::http_hash_algorithms;
49use crate::metadata::{ArchiveMetadata, Metadata};
50use crate::source::SourceDistributionBuilder;
51use crate::{Error, FirstPartyPackages, LocalWheel, Reporter, RequiresDist};
52
53pub struct DistributionDatabase<'a, Context: BuildContext> {
66 build_context: &'a Context,
67 recorder: Option<ResolutionRecorder>,
68 builder: SourceDistributionBuilder<'a, Context>,
69 client: ManagedClient<'a>,
70 reporter: Option<Arc<dyn Reporter>>,
71 content_addressed_cache: bool,
72 first_party_packages: Option<&'a FirstPartyPackages>,
73}
74
75impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
76 pub fn new(
77 client: &'a RegistryClient,
78 build_context: &'a Context,
79 downloads_semaphore: Arc<Semaphore>,
80 ) -> Self {
81 let content_addressed_cache = uv_preview::is_enabled(PreviewFeature::ContentAddressedCache)
85 && !uv_extract::insecure_no_validate();
86 Self {
87 recorder: None,
88 build_context,
89 builder: SourceDistributionBuilder::new(build_context),
90 client: ManagedClient::new(client, downloads_semaphore),
91 reporter: None,
92 content_addressed_cache,
93 first_party_packages: None,
94 }
95 }
96
97 #[must_use]
99 pub fn with_first_party_packages(
100 mut self,
101 first_party_packages: &'a FirstPartyPackages,
102 ) -> Self {
103 self.first_party_packages = Some(first_party_packages);
104 self
105 }
106
107 pub fn is_first_party(&self, name: &PackageName, path: &Path) -> bool {
109 self.first_party_packages
110 .is_some_and(|packages| packages.contains(name, path))
111 }
112
113 #[must_use]
115 pub fn with_recorder(mut self, recorder: Option<ResolutionRecorder>) -> Self {
116 self.recorder = recorder;
117 self
118 }
119
120 pub fn record_metadata(&self, dist: &Dist) {
122 if let Some(recorder) = &self.recorder {
123 recorder.dependency_metadata(dist.name());
124 }
125 }
126
127 pub fn dependency_metadata(
129 &self,
130 name: &PackageName,
131 version: Option<&Version>,
132 ) -> Option<ResolutionMetadata> {
133 if let Some(recorder) = &self.recorder {
134 recorder.dependency_metadata(name);
135 }
136 self.build_context.dependency_metadata().get(name, version)
137 }
138
139 #[must_use]
141 pub fn with_build_stack(self, build_stack: &'a BuildStack) -> Self {
142 Self {
143 builder: self.builder.with_build_stack(build_stack),
144 ..self
145 }
146 }
147
148 #[must_use]
150 pub fn with_reporter(self, reporter: Arc<dyn Reporter>) -> Self {
151 Self {
152 builder: self.builder.with_reporter(reporter.clone()),
153 reporter: Some(reporter),
154 ..self
155 }
156 }
157
158 fn handle_response_errors(&self, err: reqwest::Error) -> io::Error {
160 if err.is_timeout() {
161 io::Error::new(
163 io::ErrorKind::TimedOut,
164 format!(
165 "Failed to download distribution due to network timeout. Try increasing UV_HTTP_TIMEOUT (current value: {}s).",
166 self.client.unmanaged.read_timeout().as_secs()
167 ),
168 )
169 } else {
170 io::Error::other(err)
171 }
172 }
173
174 async fn lock_wheel(
183 wheel_entry: &CacheEntry,
184 filename: &WheelFilename,
185 ) -> Result<LockedFile, Error> {
186 let lock_key = filename.cache_key();
188
189 let lock_entry = wheel_entry.with_file(format!("{lock_key}.lock"));
190 lock_entry.lock().await.map_err(Error::CacheLock)
191 }
192
193 fn validate_hashes(
195 dist: &BuiltDist,
196 hashes: ArchiveHashPolicy<'_>,
197 hashers: Vec<Hasher>,
198 ) -> Result<HashDigests, Error> {
199 let computed_hashes: HashDigests = hashers.into_iter().map(HashDigest::from).collect();
200 if hashes.requires_validation() && !hashes.matches(computed_hashes.as_slice()) {
201 return Err(Error::hash_mismatch(
202 dist.to_string(),
203 hashes.digests(),
204 computed_hashes.as_slice(),
205 ));
206 }
207 Ok(computed_hashes)
208 }
209
210 #[instrument(skip_all, fields(%dist))]
217 pub async fn get_or_build_wheel(
218 &self,
219 dist: &Dist,
220 tags: &Tags,
221 hashes: ArchiveHashPolicy<'_>,
222 ) -> Result<LocalWheel, Error> {
223 if let Dist::Built(built) = dist
225 && built.wheel_filename().variant().is_some()
226 {
227 return Err(Error::UnsupportedWheelVariant(Box::new(
228 built.wheel_filename().clone(),
229 )));
230 }
231 match dist {
232 Dist::Built(built) => self.get_wheel(built, hashes).await,
233 Dist::Source(source) => self.build_wheel(source, tags, hashes).await,
234 }
235 }
236
237 #[instrument(skip_all, fields(%dist))]
243 pub async fn get_installed_metadata(
244 &self,
245 dist: &InstalledDist,
246 ) -> Result<ArchiveMetadata, Error> {
247 if let Some(metadata) = self.dependency_metadata(dist.name(), Some(dist.version())) {
249 return Ok(Metadata::from_dependency_metadata(metadata).into());
250 }
251
252 let metadata = dist
253 .read_metadata()
254 .map_err(|err| Error::ReadInstalled(Box::new(dist.clone()), err))?;
255
256 Ok(ArchiveMetadata::from_metadata23(metadata.clone()))
257 }
258
259 #[instrument(skip_all, fields(%dist))]
264 pub async fn get_or_build_wheel_metadata(
265 &self,
266 dist: &Dist,
267 hashes: MetadataHashPolicy<'_>,
268 ) -> Result<ArchiveMetadata, Error> {
269 if let Dist::Built(built) = dist
271 && built.wheel_filename().variant().is_some()
272 {
273 return Err(Error::UnsupportedWheelVariant(Box::new(
274 built.wheel_filename().clone(),
275 )));
276 }
277 match dist {
278 Dist::Built(built) => self.get_wheel_metadata(built, hashes).await,
279 Dist::Source(source) => {
280 self.build_wheel_metadata(&BuildableSource::Dist(source), hashes)
281 .await
282 }
283 }
284 }
285
286 async fn get_wheel(
292 &self,
293 dist: &BuiltDist,
294 hashes: ArchiveHashPolicy<'_>,
295 ) -> Result<LocalWheel, Error> {
296 match dist {
297 BuiltDist::Registry(wheels) => {
298 let wheel = wheels.best_wheel();
299 let url = wheel.file.url.to_url()?;
300 let size = wheel.file.size;
301
302 let wheel_entry = self.build_context.cache().entry(
304 CacheBucket::Wheels,
305 WheelCache::Index(&wheel.index).wheel_dir(wheel.name().as_ref()),
306 wheel.filename.cache_key(),
307 );
308
309 if url.scheme() == "file" {
311 let path = url
312 .to_file_path()
313 .map_err(|()| Error::NonFileUrl(url.clone()))?;
314 return self
315 .load_wheel(&path, &wheel.filename, wheel_entry, dist, hashes)
316 .await;
317 }
318
319 match self
321 .stream_wheel(
322 url.clone(),
323 dist.index(),
324 &wheel.filename,
325 size,
326 &wheel_entry,
327 dist,
328 hashes,
329 )
330 .await
331 {
332 Ok(archive) => Ok(LocalWheel {
333 dist: Dist::Built(dist.clone()),
334 archive: self
335 .build_context
336 .cache()
337 .archive(&archive.id)
338 .into_boxed_path(),
339 hashes: archive.hashes,
340 filename: wheel.filename.clone(),
341 cache: CacheInfo::default(),
342 build: None,
343 }),
344 Err(Error::Extract(name, err)) => {
345 if err.is_http_streaming_unsupported() {
346 warn!(
347 "Streaming unsupported for `{dist}`; downloading wheel to disk ({err})"
348 );
349 } else if err.is_http_streaming_failed() {
350 warn!(
351 "Streaming failed for `{dist}`; downloading wheel to disk ({err})"
352 );
353 } else {
354 return Err(Error::Extract(name, err));
355 }
356
357 let archive = self
360 .download_wheel(
361 url,
362 dist.index(),
363 &wheel.filename,
364 size,
365 &wheel_entry,
366 dist,
367 hashes,
368 )
369 .await?;
370
371 Ok(LocalWheel {
372 dist: Dist::Built(dist.clone()),
373 archive: self
374 .build_context
375 .cache()
376 .archive(&archive.id)
377 .into_boxed_path(),
378 hashes: archive.hashes,
379 filename: wheel.filename.clone(),
380 cache: CacheInfo::default(),
381 build: None,
382 })
383 }
384 Err(err) => Err(err),
385 }
386 }
387
388 BuiltDist::DirectUrl(wheel) => {
389 let wheel_entry = self.build_context.cache().entry(
391 CacheBucket::Wheels,
392 WheelCache::Url(&wheel.url).wheel_dir(wheel.name().as_ref()),
393 wheel.filename.cache_key(),
394 );
395
396 match self
398 .stream_wheel(
399 wheel.url.raw().clone(),
400 None,
401 &wheel.filename,
402 wheel.size,
403 &wheel_entry,
404 dist,
405 hashes,
406 )
407 .await
408 {
409 Ok(archive) => Ok(LocalWheel {
410 dist: Dist::Built(dist.clone()),
411 archive: self
412 .build_context
413 .cache()
414 .archive(&archive.id)
415 .into_boxed_path(),
416 hashes: archive.hashes,
417 filename: wheel.filename.clone(),
418 cache: CacheInfo::default(),
419 build: None,
420 }),
421 Err(Error::Extract(name, err)) => {
422 if err.is_http_streaming_unsupported() {
423 warn!(
424 "Streaming unsupported for `{dist}`; downloading wheel to disk ({err})"
425 );
426 } else if err.is_http_streaming_failed() {
427 warn!(
428 "Streaming failed for `{dist}`; downloading wheel to disk ({err})"
429 );
430 } else {
431 return Err(Error::Extract(name, err));
432 }
433
434 let archive = self
437 .download_wheel(
438 wheel.url.raw().clone(),
439 None,
440 &wheel.filename,
441 wheel.size,
442 &wheel_entry,
443 dist,
444 hashes,
445 )
446 .await?;
447 Ok(LocalWheel {
448 dist: Dist::Built(dist.clone()),
449 archive: self
450 .build_context
451 .cache()
452 .archive(&archive.id)
453 .into_boxed_path(),
454 hashes: archive.hashes,
455 filename: wheel.filename.clone(),
456 cache: CacheInfo::default(),
457 build: None,
458 })
459 }
460 Err(err) => Err(err),
461 }
462 }
463
464 BuiltDist::GitPath(wheel) => {
465 let fetch = self
467 .build_context
468 .git()
469 .fetch(
470 &wheel.git,
471 self.client.unmanaged.git_http_settings(wheel.git.url()),
472 self.build_context.cache().bucket(CacheBucket::Git),
473 self.reporter.clone().map(<dyn Reporter>::into_git_reporter),
474 )
475 .await?;
476
477 if wheel.git.lfs().enabled() && !fetch.lfs_ready() {
478 if GIT_LFS.is_err() {
479 return Err(Error::MissingWheelGitLfsArtifacts(
480 wheel.url.to_url(),
481 GitError::GitLfsNotFound,
482 ));
483 }
484 return Err(Error::MissingWheelGitLfsArtifacts(
485 wheel.url.to_url(),
486 GitError::GitLfsNotConfigured,
487 ));
488 }
489
490 let git_sha = fetch.git().precise().expect("Exact commit after checkout");
491 let cache_entry = self.build_context.cache().entry(
492 CacheBucket::Wheels,
493 WheelCache::Git(&wheel.url, git_sha.as_short_str()).root(),
494 wheel.filename.stem(),
495 );
496
497 let install_path = fetch.path().join(&wheel.install_path);
498
499 self.load_wheel(&install_path, &wheel.filename, cache_entry, dist, hashes)
500 .await
501 }
502
503 BuiltDist::Path(wheel) => {
504 let cache_entry = self.build_context.cache().entry(
505 CacheBucket::Wheels,
506 WheelCache::Url(&wheel.url).wheel_dir(wheel.name().as_ref()),
507 wheel.filename.cache_key(),
508 );
509
510 self.load_wheel(
511 &wheel.install_path,
512 &wheel.filename,
513 cache_entry,
514 dist,
515 hashes,
516 )
517 .await
518 }
519 }
520 }
521
522 async fn build_wheel(
528 &self,
529 dist: &SourceDist,
530 tags: &Tags,
531 hashes: ArchiveHashPolicy<'_>,
532 ) -> Result<LocalWheel, Error> {
533 let built_wheel = self
534 .builder
535 .download_and_build(&BuildableSource::Dist(dist), tags, hashes, &self.client)
536 .boxed_local()
537 .await?;
538
539 if !built_wheel.filename.is_compatible(tags) {
545 return if tags.is_cross() {
546 Err(Error::BuiltWheelIncompatibleTargetPlatform {
547 filename: built_wheel.filename,
548 python_platform: tags.python_platform().clone(),
549 python_version: PythonVersion {
550 version: tags.python_version(),
551 variant: if tags.is_freethreaded() {
552 PythonVariant::Freethreaded
553 } else {
554 PythonVariant::Default
555 },
556 },
557 })
558 } else {
559 Err(Error::BuiltWheelIncompatibleHostPlatform {
560 filename: built_wheel.filename,
561 python_platform: tags.python_platform().clone(),
562 python_version: PythonVersion {
563 version: tags.python_version(),
564 variant: if tags.is_freethreaded() {
565 PythonVariant::Freethreaded
566 } else {
567 PythonVariant::Default
568 },
569 },
570 })
571 };
572 }
573
574 let wheel_entry = CacheEntry::from_path(built_wheel.target.as_ref());
576 let _lock = Self::lock_wheel(&wheel_entry, &built_wheel.filename).await?;
577
578 match self.build_context.cache().resolve_link(&built_wheel.target) {
581 Ok(archive) => {
582 return Ok(LocalWheel {
583 dist: Dist::Source(dist.clone()),
584 archive: archive.into_boxed_path(),
585 filename: built_wheel.filename,
586 hashes: built_wheel.hashes,
587 cache: built_wheel.cache_info,
588 build: Some(built_wheel.build_info),
589 });
590 }
591 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
592 Err(err) => return Err(Error::CacheRead(err)),
593 }
594
595 let id = self
597 .unzip_wheel(
598 &built_wheel.path,
599 &built_wheel.target,
600 DistRef::Source(dist),
601 )
602 .await?;
603
604 Ok(LocalWheel {
605 dist: Dist::Source(dist.clone()),
606 archive: self.build_context.cache().archive(&id).into_boxed_path(),
607 hashes: built_wheel.hashes,
608 filename: built_wheel.filename,
609 cache: built_wheel.cache_info,
610 build: Some(built_wheel.build_info),
611 })
612 }
613
614 async fn get_wheel_metadata(
619 &self,
620 dist: &BuiltDist,
621 hashes: MetadataHashPolicy<'_>,
622 ) -> Result<ArchiveMetadata, Error> {
623 let hash_policy = match hashes.validation {
624 HashValidation::None => {
625 let compute_hashes = match hashes.collection {
626 HashCollection::None => false,
627 HashCollection::Url | HashCollection::All => match dist {
628 BuiltDist::Registry(dist) => {
629 hashes.collection == HashCollection::All
632 && dist.best_wheel().file.hashes.is_empty()
633 }
634 BuiltDist::DirectUrl(dist) => parse_url_hashes(&dist.url).is_none(),
635 BuiltDist::Path(_) | BuiltDist::GitPath(_) => true,
636 },
637 };
638 if compute_hashes {
639 ArchiveHashPolicy::Generate
640 } else {
641 ArchiveHashPolicy::None
642 }
643 }
644 HashValidation::Any(_) | HashValidation::All(_) => hashes.validation.into(),
645 };
646
647 if hash_policy == ArchiveHashPolicy::Generate {
650 let wheel = self.get_wheel(dist, hash_policy).await?;
651 let metadata = if let Some(metadata) =
653 self.dependency_metadata(dist.name(), Some(dist.version()))
654 {
655 Metadata::from_dependency_metadata(metadata)
656 } else {
657 Metadata::from_metadata23(wheel.metadata()?)
658 };
659 let hashes = wheel.hashes;
660 return Ok(ArchiveMetadata { metadata, hashes });
661 }
662
663 if let Some(metadata) = self.dependency_metadata(dist.name(), Some(dist.version())) {
665 return Ok(Metadata::from_dependency_metadata(metadata).into());
666 }
667
668 let result = self
669 .client
670 .managed(|client| {
671 client
672 .wheel_metadata(
673 dist,
674 self.build_context.git(),
675 self.build_context.capabilities(),
676 self.reporter.clone().map(<dyn Reporter>::into_git_reporter),
677 )
678 .boxed_local()
679 })
680 .await;
681
682 match result {
683 Ok(metadata) => {
684 Ok(ArchiveMetadata::from_metadata23(metadata))
686 }
687 Err(err) if err.is_http_streaming_unsupported() => {
688 warn!(
689 "Streaming unsupported when fetching metadata for `{dist}`; downloading wheel directly ({err})"
690 );
691
692 let wheel = self.get_wheel(dist, hash_policy).await?;
695 let metadata = wheel.metadata()?;
696 let hashes = wheel.hashes;
697 Ok(ArchiveMetadata {
698 metadata: Metadata::from_metadata23(metadata),
699 hashes,
700 })
701 }
702 Err(err) => Err(err.into()),
703 }
704 }
705
706 pub async fn build_wheel_metadata(
711 &self,
712 source: &BuildableSource<'_>,
713 hashes: MetadataHashPolicy<'_>,
714 ) -> Result<ArchiveMetadata, Error> {
715 if let Some(dist) = source.as_dist() {
717 if let Some(metadata) = self.dependency_metadata(dist.name(), dist.version()) {
718 self.builder.resolve_revision(source, &self.client).await?;
721
722 return Ok(Metadata::from_dependency_metadata(metadata).into());
723 }
724 }
725
726 let url_hashes = if let BuildableSource::Dist(SourceDist::DirectUrl(dist)) = source {
727 parse_url_hashes(&dist.url)
728 } else if let BuildableSource::Url(SourceUrl::Direct(url)) = source {
729 parse_url_hashes(url.url)
730 } else {
731 None
732 };
733
734 let build_hash_policy = match hashes.validation {
735 HashValidation::None => match hashes.collection {
736 HashCollection::None => ArchiveHashPolicy::None,
737 HashCollection::Url | HashCollection::All => match url_hashes.as_ref() {
740 Some(digests) => ArchiveHashPolicy::All(digests.as_slice()),
741 None => ArchiveHashPolicy::Generate,
742 },
743 },
744 HashValidation::Any(_) | HashValidation::All(_) => hashes.validation.into(),
745 };
746 let ArchiveMetadata { metadata, hashes } = self
747 .builder
748 .for_metadata(self.first_party_packages)
749 .download_and_build_metadata(source, build_hash_policy, &self.client)
750 .boxed_local()
751 .await?;
752
753 Ok(ArchiveMetadata {
757 metadata: metadata.with_force_relative(true),
758 hashes,
759 })
760 }
761
762 pub async fn requires_dist(
764 &self,
765 path: &Path,
766 pyproject_toml: &PyProjectToml,
767 ) -> Result<Option<RequiresDist>, Error> {
768 self.builder
769 .source_tree_requires_dist(
770 path,
771 pyproject_toml,
772 self.client.unmanaged.credentials_cache(),
773 )
774 .await
775 }
776
777 async fn stream_wheel(
783 &self,
784 url: SafeUrl,
785 index: Option<&IndexUrl>,
786 filename: &WheelFilename,
787 progress_size_hint: Option<u64>,
788 wheel_entry: &CacheEntry,
789 dist: &BuiltDist,
790 hashes: ArchiveHashPolicy<'_>,
791 ) -> Result<Archive, Error> {
792 let expected_size = match dist {
794 BuiltDist::Registry(dist) if dist.best_wheel().size_is_authoritative => {
795 progress_size_hint
796 }
797 BuiltDist::DirectUrl(_) => progress_size_hint,
798 _ => None,
799 };
800
801 let _lock = Self::lock_wheel(wheel_entry, filename).await?;
803
804 let http_entry = wheel_entry.with_file(format!("{}.http", filename.cache_key()));
806
807 let download = |response: reqwest::Response, _: &mut RetryState| {
808 async {
809 let hashes = if let BuiltDist::Registry(wheels) = dist {
810 hashes.with_index_hashes(wheels.best_wheel().file.hashes.as_slice())
811 } else {
812 hashes
813 };
814
815 let progress_size_hint = progress_size_hint.or_else(|| content_length(&response));
816
817 let progress = self.reporter.as_ref().map(|reporter| {
818 (
819 reporter,
820 reporter.on_download_start(dist.name(), progress_size_hint),
821 )
822 });
823
824 let reader = response
825 .bytes_stream()
826 .map_err(|err| self.handle_response_errors(err))
827 .into_async_read();
828
829 let algorithms = http_hash_algorithms(hashes);
831 let mut hashers = algorithms.into_iter().map(Hasher::from).collect::<Vec<_>>();
832 let mut hasher = uv_extract::hash::HashReader::new(reader.compat(), &mut hashers);
833
834 let extractor = WheelExtractor::new(
836 self.build_context.cache().root(),
837 self.content_addressed_cache,
838 )
839 .map_err(Error::CacheWrite)?;
840
841 let mut extracted = match progress {
842 Some((reporter, progress)) => {
843 let mut reader = ProgressReader::new(&mut hasher, progress, &**reporter);
844 extractor
845 .extract_streaming(&mut reader)
846 .await
847 .map_err(|err| Error::Extract(filename.to_string(), err))?
848 }
849 None => extractor
850 .extract_streaming(&mut hasher)
851 .await
852 .map_err(|err| Error::Extract(filename.to_string(), err))?,
853 };
854 hasher.finish().await.map_err(Error::HashExhaustion)?;
856 let actual_size = hasher.bytes_read();
857 if let Some(expected) = expected_size
858 && actual_size != expected
859 {
860 return Err(Error::MismatchedSize {
861 distribution: dist.to_string(),
862 expected,
863 actual: actual_size,
864 });
865 }
866
867 let computed_hashes = Self::validate_hashes(dist, hashes, hashers)?;
868
869 extracted.validate_and_heal_record(dist)?;
872
873 let id = self
875 .persist_extracted_wheel(extracted, wheel_entry.path())
876 .await?;
877
878 if let Some((reporter, progress)) = progress {
879 reporter.on_download_complete(dist.name(), progress);
880 }
881
882 Ok(Archive::new(
883 id,
884 computed_hashes,
885 filename.clone(),
886 Some(actual_size),
887 ))
888 }
889 .instrument(info_span!("wheel", wheel = %dist))
890 };
891
892 let req = self.request(url.clone())?;
894
895 let cache_control = match self.client.unmanaged.connectivity() {
897 Connectivity::Online
898 if let Some(header) = index.and_then(|index| {
899 self.build_context
900 .locations()
901 .artifact_cache_control_for(index)
902 }) =>
903 {
904 CacheControl::Override(header)
905 }
906 Connectivity::Online => CacheControl::from(
907 self.build_context
908 .cache()
909 .freshness(&http_entry, Some(&filename.name), None)
910 .map_err(Error::CacheRead)?,
911 ),
912 Connectivity::Offline => CacheControl::AllowStale,
913 };
914
915 let archive = self
916 .client
917 .managed(|client| {
918 client.cached_client().get_serde_with_retry(
919 req,
920 &http_entry,
921 cache_control.clone(),
922 download,
923 )
924 })
925 .await
926 .map_err(|err| match err {
927 CachedClientError::Callback { err, .. } => err,
928 CachedClientError::Client(err) => Error::Client(err),
929 })?;
930
931 if let (Some(expected), Some(actual)) = (expected_size, archive.size)
932 && expected != actual
933 {
934 return Err(Error::MismatchedSize {
935 distribution: dist.to_string(),
936 expected,
937 actual,
938 });
939 }
940
941 let archive = Some(archive)
943 .filter(|archive| archive.has_digests(hashes))
944 .filter(|archive| archive.exists(self.build_context.cache()))
945 .filter(|archive| expected_size.is_none() || archive.size.is_some());
946
947 let archive = if let Some(archive) = archive {
948 archive
949 } else {
950 self.client
951 .managed(async |client| {
952 client
953 .cached_client()
954 .skip_cache_with_retry(
955 self.request(url)?,
956 &http_entry,
957 cache_control,
958 download,
959 )
960 .await
961 .map_err(|err| match err {
962 CachedClientError::Callback { err, .. } => err,
963 CachedClientError::Client(err) => Error::Client(err),
964 })
965 })
966 .await?
967 };
968
969 Ok(archive)
970 }
971
972 async fn download_wheel(
978 &self,
979 url: SafeUrl,
980 index: Option<&IndexUrl>,
981 filename: &WheelFilename,
982 progress_size_hint: Option<u64>,
983 wheel_entry: &CacheEntry,
984 dist: &BuiltDist,
985 hashes: ArchiveHashPolicy<'_>,
986 ) -> Result<Archive, Error> {
987 let expected_size = match dist {
989 BuiltDist::Registry(dist) if dist.best_wheel().size_is_authoritative => {
990 progress_size_hint
991 }
992 BuiltDist::DirectUrl(_) => progress_size_hint,
993 _ => None,
994 };
995
996 let _lock = Self::lock_wheel(wheel_entry, filename).await?;
998
999 let http_entry = wheel_entry.with_file(format!("{}.http", filename.cache_key()));
1001
1002 let download_url = url.clone();
1003
1004 let download = async |response, retry_state: &mut RetryState| {
1005 self.download_wheel_response(
1006 response,
1007 &download_url,
1008 retry_state,
1009 filename,
1010 progress_size_hint,
1011 expected_size,
1012 wheel_entry,
1013 dist,
1014 hashes,
1015 )
1016 .instrument(info_span!("wheel", wheel = %dist))
1017 .await
1018 };
1019
1020 let req = self.request(url.clone())?;
1022
1023 let cache_control = match self.client.unmanaged.connectivity() {
1025 Connectivity::Online
1026 if let Some(header) = index.and_then(|index| {
1027 self.build_context
1028 .locations()
1029 .artifact_cache_control_for(index)
1030 }) =>
1031 {
1032 CacheControl::Override(header)
1033 }
1034 Connectivity::Online => CacheControl::from(
1035 self.build_context
1036 .cache()
1037 .freshness(&http_entry, Some(&filename.name), None)
1038 .map_err(Error::CacheRead)?,
1039 ),
1040 Connectivity::Offline => CacheControl::AllowStale,
1041 };
1042
1043 let archive = self
1044 .client
1045 .managed(|client| {
1046 client.cached_client().get_serde_with_retry(
1047 req,
1048 &http_entry,
1049 cache_control.clone(),
1050 download,
1051 )
1052 })
1053 .await
1054 .map_err(|err| match err {
1055 CachedClientError::Callback { err, .. } => err,
1056 CachedClientError::Client(err) => Error::Client(err),
1057 })?;
1058
1059 if let (Some(expected), Some(actual)) = (expected_size, archive.size)
1060 && expected != actual
1061 {
1062 return Err(Error::MismatchedSize {
1063 distribution: dist.to_string(),
1064 expected,
1065 actual,
1066 });
1067 }
1068
1069 let archive = Some(archive)
1071 .filter(|archive| archive.has_digests(hashes))
1072 .filter(|archive| archive.exists(self.build_context.cache()))
1073 .filter(|archive| expected_size.is_none() || archive.size.is_some());
1074
1075 let archive = if let Some(archive) = archive {
1076 archive
1077 } else {
1078 self.client
1079 .managed(async |client| {
1080 client
1081 .cached_client()
1082 .skip_cache_with_retry(
1083 self.request(url)?,
1084 &http_entry,
1085 cache_control,
1086 download,
1087 )
1088 .await
1089 .map_err(|err| match err {
1090 CachedClientError::Callback { err, .. } => err,
1091 CachedClientError::Client(err) => Error::Client(err),
1092 })
1093 })
1094 .await?
1095 };
1096
1097 Ok(archive)
1098 }
1099
1100 async fn download_wheel_response(
1108 &self,
1109 mut response: reqwest::Response,
1110 url: &SafeUrl,
1111 retry_state: &mut RetryState,
1112 filename: &WheelFilename,
1113 progress_size_hint: Option<u64>,
1114 expected_size: Option<u64>,
1115 wheel_entry: &CacheEntry,
1116 dist: &BuiltDist,
1117 hashes: ArchiveHashPolicy<'_>,
1118 ) -> Result<Archive, Error> {
1119 let hashes = if let BuiltDist::Registry(wheels) = dist {
1120 hashes.with_index_hashes(wheels.best_wheel().file.hashes.as_slice())
1121 } else {
1122 hashes
1123 };
1124
1125 let progress_size_hint = progress_size_hint.or_else(|| content_length(&response));
1126 let mut download_size = content_length(&response).or(expected_size);
1127
1128 let progress = self.reporter.as_ref().map(|reporter| {
1129 (
1130 reporter,
1131 reporter.on_download_start(dist.name(), progress_size_hint),
1132 )
1133 });
1134
1135 let algorithms = http_hash_algorithms(hashes);
1136
1137 let temp_file =
1139 tempfile::tempfile_in(self.build_context.cache().root()).map_err(Error::CacheWrite)?;
1140 let mut writer = tokio::io::BufWriter::new(fs_err::tokio::File::from_std(
1141 fs_err::File::from_parts(temp_file, self.build_context.cache().root()),
1143 ));
1144
1145 let mut hashers = algorithms.into_iter().map(Hasher::from).collect::<Vec<_>>();
1147 let mut bytes_retrieved = 0;
1151 let mut resumed_at = None;
1153 let mut resumed_range: Option<ContentRangeBytes> = None;
1155
1156 loop {
1162 if response.status() == reqwest::StatusCode::OK
1165 && let (Some(expected), Some(actual)) = (expected_size, content_length(&response))
1166 && expected != actual
1167 {
1168 return Err(Error::MismatchedContentLength {
1169 distribution: dist.to_string(),
1170 expected,
1171 actual,
1172 });
1173 }
1174
1175 let supports_range_requests = response.status() == reqwest::StatusCode::PARTIAL_CONTENT
1178 || response
1179 .headers()
1180 .get(reqwest::header::ACCEPT_RANGES)
1181 .is_some_and(|value| value == "bytes");
1182
1183 let replaces_partial_download =
1186 resumed_at.is_some() && response.status() != reqwest::StatusCode::PARTIAL_CONTENT;
1187 if replaces_partial_download {
1188 writer
1189 .get_mut()
1190 .set_len(0)
1191 .await
1192 .map_err(Error::CacheWrite)?;
1193 writer
1194 .seek(io::SeekFrom::Start(0))
1195 .await
1196 .map_err(Error::CacheWrite)?;
1197 hashers = http_hash_algorithms(hashes)
1198 .into_iter()
1199 .map(Hasher::from)
1200 .collect();
1201 bytes_retrieved = 0;
1202 }
1203
1204 let reader = response
1205 .bytes_stream()
1206 .map_err(|err| self.handle_response_errors(err))
1207 .into_async_read();
1208 let mut hasher = uv_extract::hash::HashReader::new(reader.compat(), &mut hashers);
1209
1210 let copy_result = match progress {
1214 Some((reporter, progress)) => {
1215 let mut reader = ProgressReader::new(&mut hasher, progress, &**reporter);
1218
1219 tokio::io::copy(&mut reader, &mut writer)
1220 .await
1221 .map_err(Error::CacheWrite)
1222 }
1223 None => tokio::io::copy(&mut hasher, &mut writer)
1224 .await
1225 .map_err(Error::CacheWrite),
1226 };
1227
1228 bytes_retrieved += hasher.bytes_read();
1229
1230 let interrupted = copy_result.is_err();
1231 let err = match copy_result {
1232 Ok(_) => {
1235 let Some(range) = resumed_range else {
1238 break;
1239 };
1240
1241 if bytes_retrieved != range.last_byte + 1 {
1247 return Err(Error::MismatchedRangeSize {
1248 distribution: dist.to_string(),
1249 expected: range.last_byte - range.first_byte + 1,
1250 actual: hasher.bytes_read(),
1251 });
1252 }
1253
1254 if bytes_retrieved == range.complete_length {
1257 break;
1258 }
1259
1260 Error::CacheWrite(io::Error::new(
1266 io::ErrorKind::UnexpectedEof,
1267 "Range response did not complete the download",
1268 ))
1269 }
1270 Err(err) => err,
1273 };
1274
1275 if replaces_partial_download || !supports_range_requests {
1284 return Err(err);
1285 }
1286
1287 writer.flush().await.map_err(Error::CacheWrite)?;
1292 let offset = writer
1293 .get_mut()
1294 .stream_position()
1295 .await
1296 .map_err(Error::CacheWrite)?;
1297 if offset == 0
1298 || offset != bytes_retrieved
1299 || resumed_at.is_some_and(|previous| offset <= previous)
1300 {
1301 return Err(err);
1302 }
1303
1304 if interrupted {
1307 let Some(backoff) = retry_state.should_retry(&err, 0) else {
1308 return Err(err);
1309 };
1310 retry_state.sleep_backoff(backoff).await;
1311 }
1312
1313 debug!("Resuming download of `{url}` at byte {offset}");
1315 let resumed_response = retry_state
1316 .send(self.request_with_offset(url.clone(), offset))
1317 .await?;
1318
1319 if resumed_response.status() == reqwest::StatusCode::RANGE_NOT_SATISFIABLE {
1322 debug!("Range not satisfiable while resuming `{url}`; abandoning resumed download");
1323 return Err(err);
1324 }
1325 resumed_response.error_for_status_ref()?;
1326
1327 resumed_range = if resumed_response.status() == reqwest::StatusCode::PARTIAL_CONTENT {
1328 let Some(range) = content_range(&resumed_response, offset, download_size) else {
1331 warn!(
1332 "Invalid range request response from server that declares HTTP range \
1333 request support, abandoning resumed download: {url}"
1334 );
1335 return Err(err);
1336 };
1337 download_size = Some(range.complete_length);
1338 Some(range)
1339 } else {
1340 None
1343 };
1344
1345 response = resumed_response;
1346 resumed_at = Some(offset);
1347 }
1348
1349 if let Some(expected) = expected_size
1352 && bytes_retrieved != expected
1353 {
1354 return Err(Error::MismatchedSize {
1355 distribution: dist.to_string(),
1356 expected,
1357 actual: bytes_retrieved,
1358 });
1359 }
1360
1361 let extractor = WheelExtractor::new(
1363 self.build_context.cache().root(),
1364 self.content_addressed_cache,
1365 )
1366 .map_err(Error::CacheWrite)?;
1367 let mut file = writer.into_inner();
1368 file.seek(io::SeekFrom::Start(0))
1369 .await
1370 .map_err(Error::CacheWrite)?;
1371
1372 let file = file.into_std().await;
1373 let mut extracted = tokio::task::spawn_blocking(move || extractor.extract_seekable(file))
1374 .await?
1375 .map_err(|err| Error::Extract(filename.to_string(), err))?;
1376 let computed_hashes = Self::validate_hashes(dist, hashes, hashers)?;
1377
1378 extracted.validate_and_heal_record(dist)?;
1381
1382 let id = self
1384 .persist_extracted_wheel(extracted, wheel_entry.path())
1385 .await?;
1386
1387 if let Some((reporter, progress)) = progress {
1388 reporter.on_download_complete(dist.name(), progress);
1389 }
1390
1391 Ok(Archive::new(
1392 id,
1393 computed_hashes,
1394 filename.clone(),
1395 Some(bytes_retrieved),
1396 ))
1397 }
1398
1399 async fn load_wheel(
1401 &self,
1402 path: &Path,
1403 filename: &WheelFilename,
1404 wheel_entry: CacheEntry,
1405 dist: &BuiltDist,
1406 hashes: ArchiveHashPolicy<'_>,
1407 ) -> Result<LocalWheel, Error> {
1408 let _lock = Self::lock_wheel(&wheel_entry, filename).await?;
1410
1411 let modified = Timestamp::from_path(path).map_err(Error::CacheRead)?;
1413
1414 let pointer_entry = wheel_entry.with_file(format!("{}.rev", filename.cache_key()));
1416 let pointer = PathArchivePointer::read_from(&pointer_entry)?;
1417
1418 let archive = pointer
1420 .filter(|pointer| pointer.is_up_to_date(modified))
1421 .map(PathArchivePointer::into_archive)
1422 .filter(|archive| archive.has_digests(hashes));
1423
1424 let caller_hashes = hashes;
1426 let hashes = if let BuiltDist::Registry(wheels) = dist {
1427 hashes.with_index_hashes(wheels.best_wheel().file.hashes.as_slice())
1428 } else {
1429 hashes
1430 };
1431
1432 if let Some(archive) = archive {
1434 Ok(LocalWheel {
1435 dist: Dist::Built(dist.clone()),
1436 archive: self
1437 .build_context
1438 .cache()
1439 .archive(&archive.id)
1440 .into_boxed_path(),
1441 hashes: archive.hashes,
1442 filename: filename.clone(),
1443 cache: CacheInfo::from_timestamp(modified),
1444 build: None,
1445 })
1446 } else if hashes.is_none() {
1447 let archive = Archive::new(
1449 self.unzip_wheel(path, wheel_entry.path(), DistRef::Built(dist))
1450 .await?,
1451 HashDigests::empty(),
1452 filename.clone(),
1453 None,
1454 );
1455
1456 let pointer = PathArchivePointer {
1458 timestamp: modified,
1459 archive: archive.clone(),
1460 };
1461 pointer.write_to(&pointer_entry).await?;
1462
1463 Ok(LocalWheel {
1464 dist: Dist::Built(dist.clone()),
1465 archive: self
1466 .build_context
1467 .cache()
1468 .archive(&archive.id)
1469 .into_boxed_path(),
1470 hashes: archive.hashes,
1471 filename: filename.clone(),
1472 cache: CacheInfo::from_timestamp(modified),
1473 build: None,
1474 })
1475 } else {
1476 let file = fs_err::tokio::File::open(path)
1478 .await
1479 .map_err(Error::CacheRead)?;
1480 let extractor = WheelExtractor::new(
1481 self.build_context.cache().root(),
1482 self.content_addressed_cache,
1483 )
1484 .map_err(Error::CacheWrite)?;
1485
1486 let mut algorithms = caller_hashes.algorithms();
1488 algorithms.extend(hashes.algorithms());
1489 algorithms.sort();
1490 algorithms.dedup();
1491 let mut hashers = algorithms.into_iter().map(Hasher::from).collect::<Vec<_>>();
1492 let mut hasher = uv_extract::hash::HashReader::new(file, &mut hashers);
1493
1494 let mut extracted = extractor
1496 .extract_streaming(&mut hasher)
1497 .await
1498 .map_err(|err| Error::Extract(filename.to_string(), err))?;
1499
1500 hasher.finish().await.map_err(Error::HashExhaustion)?;
1502
1503 let computed_hashes = Self::validate_hashes(dist, hashes, hashers)?;
1504
1505 extracted.validate_and_heal_record(dist)?;
1508
1509 let id = self
1511 .persist_extracted_wheel(extracted, wheel_entry.path())
1512 .await?;
1513
1514 let archive = Archive::new(id, computed_hashes, filename.clone(), None);
1516
1517 let pointer = PathArchivePointer {
1519 timestamp: modified,
1520 archive: archive.clone(),
1521 };
1522 pointer.write_to(&pointer_entry).await?;
1523
1524 Ok(LocalWheel {
1525 dist: Dist::Built(dist.clone()),
1526 archive: self
1527 .build_context
1528 .cache()
1529 .archive(&archive.id)
1530 .into_boxed_path(),
1531 hashes: archive.hashes,
1532 filename: filename.clone(),
1533 cache: CacheInfo::from_timestamp(modified),
1534 build: None,
1535 })
1536 }
1537 }
1538
1539 async fn unzip_wheel(
1541 &self,
1542 path: &Path,
1543 target: &Path,
1544 dist: DistRef<'_>,
1545 ) -> Result<ArchiveId, Error> {
1546 let content_addressed_cache = self.content_addressed_cache;
1547
1548 let mut extracted = tokio::task::spawn_blocking({
1549 let path = path.to_owned();
1550 let root = self.build_context.cache().root().to_path_buf();
1551 move || -> Result<_, Error> {
1552 let extractor = WheelExtractor::new(&root, content_addressed_cache)
1554 .map_err(Error::CacheWrite)?;
1555 let reader = fs_err::File::open(&path).map_err(Error::CacheWrite)?;
1556 extractor
1557 .extract_seekable(reader)
1558 .map_err(|err| Error::Extract(path.to_string_lossy().into_owned(), err))
1559 }
1560 })
1561 .await??;
1562
1563 extracted.validate_and_heal_record(dist)?;
1565
1566 let id = self.persist_extracted_wheel(extracted, target).await?;
1568
1569 Ok(id)
1570 }
1571
1572 async fn persist_extracted_wheel(
1577 &self,
1578 extracted: ExtractedWheel,
1579 target: &Path,
1580 ) -> Result<ArchiveId, Error> {
1581 let (temp_dir, hashed_wheel) = extracted.into_parts();
1582 let cache = self.build_context.cache();
1583 let (temp_dir, id) = if let Some(HashedWheel { files, tree }) = hashed_wheel {
1584 let digest = DirectoryDigest::from(tree.hash());
1585 let id = ArchiveId::from_digest(digest.into());
1586 let cache = cache.clone();
1587 let temp_dir = tokio::task::spawn_blocking(move || {
1588 persist_archive_files(&cache, temp_dir.path(), &files)
1589 .map_err(Error::CacheWrite)?;
1590 Ok::<_, Error>(temp_dir)
1591 })
1592 .await??;
1593 (temp_dir, id)
1594 } else {
1595 (temp_dir, ArchiveId::default())
1596 };
1597
1598 cache
1599 .persist_with_id(temp_dir, target, id)
1600 .await
1601 .map_err(Error::CacheWrite)
1602 }
1603
1604 fn request(&self, url: SafeUrl) -> Result<reqwest::Request, reqwest::Error> {
1606 self.client
1607 .unmanaged
1608 .uncached_client(&url)
1609 .get(url)
1610 .header(
1611 "accept-encoding",
1615 reqwest::header::HeaderValue::from_static("identity"),
1616 )
1617 .build()
1618 }
1619
1620 fn request_with_offset(&self, url: SafeUrl, offset: u64) -> RequestBuilder<'_> {
1624 self.client
1625 .unmanaged
1626 .uncached_client(&url)
1627 .get(url)
1628 .header(
1629 "accept-encoding",
1630 reqwest::header::HeaderValue::from_static("identity"),
1631 )
1632 .header(reqwest::header::RANGE, format!("bytes={offset}-"))
1633 }
1634
1635 pub fn client(&self) -> &ManagedClient<'a> {
1637 &self.client
1638 }
1639}
1640
1641fn persist_archive_files(cache: &Cache, archive: &Path, files: &[HashedFile]) -> io::Result<()> {
1643 initialize_rayon_once();
1644 let targets = files
1645 .par_iter()
1646 .filter(|file| !file.path().ends_with("RECORD"))
1648 .map(|file| {
1649 let id = ArchiveFileId::from_digest(&file.object_digest_hex());
1650 (archive.join(file.path()), cache.archive_file(&id))
1651 })
1652 .collect::<Vec<_>>();
1653
1654 let mut shards: FxHashMap<&Path, Vec<_>> = FxHashMap::default();
1657 for (source, target) in &targets {
1658 let Some(parent) = target.parent() else {
1659 return Err(io::Error::new(
1660 io::ErrorKind::InvalidInput,
1661 "archive file path must have a parent directory",
1662 ));
1663 };
1664 shards.entry(parent).or_default().push((source, target));
1665 }
1666
1667 let mut shards = shards
1668 .into_iter()
1669 .map(|(parent, files)| (parent, files, Ok(())))
1670 .collect::<Vec<_>>();
1671 shards.sort_unstable_by_key(|(_, files, _)| Reverse(files.len()));
1673
1674 in_place_scope(|scope| -> io::Result<()> {
1677 for (parent, files, result) in &mut shards {
1678 fs_err::create_dir_all(parent)?;
1679 scope.spawn(move |_| {
1680 *result = files
1681 .iter()
1682 .try_for_each(|(source, target)| persist_archive_file(source, target));
1683 });
1684 }
1685 Ok(())
1686 })?;
1687
1688 shards.into_iter().try_for_each(|(_, _, result)| result)
1689}
1690
1691fn persist_archive_file(src: &Path, dst: &Path) -> io::Result<()> {
1693 match fs_err::hard_link(src, dst) {
1696 Ok(()) => return Ok(()),
1697 Err(_) if dst.try_exists()? => {}
1698 Err(_) => return uv_fs::copy_atomic_sync(src, dst),
1699 }
1700
1701 if let Err(err) = fs_err::remove_file(src)
1703 && err.kind() != io::ErrorKind::NotFound
1704 {
1705 return Err(err);
1706 }
1707
1708 fs_err::hard_link(dst, src).or_else(|_| uv_fs::copy_atomic_sync(dst, src))
1709}
1710
1711pub struct ManagedClient<'a> {
1713 pub(super) unmanaged: &'a RegistryClient,
1714 control: Arc<Semaphore>,
1715}
1716
1717impl<'a> ManagedClient<'a> {
1718 fn new(client: &'a RegistryClient, control: Arc<Semaphore>) -> Self {
1720 ManagedClient {
1721 unmanaged: client,
1722 control,
1723 }
1724 }
1725
1726 pub fn is_offline(&self) -> bool {
1728 self.unmanaged.connectivity().is_offline()
1729 }
1730
1731 pub async fn managed<F, T>(&self, f: impl FnOnce(&'a RegistryClient) -> F) -> T
1736 where
1737 F: Future<Output = T>,
1738 {
1739 let _permit = self.control.acquire().await.unwrap();
1740 f(self.unmanaged).await
1741 }
1742
1743 pub async fn manual<F, T>(&'a self, f: impl FnOnce(&'a RegistryClient, &'a Semaphore) -> F) -> T
1751 where
1752 F: Future<Output = T>,
1753 {
1754 f(self.unmanaged, &self.control).await
1755 }
1756}
1757
1758fn content_length(response: &reqwest::Response) -> Option<u64> {
1760 response
1761 .headers()
1762 .get(reqwest::header::CONTENT_LENGTH)
1763 .and_then(|val| val.to_str().ok())
1764 .and_then(|val| val.parse::<u64>().ok())
1765}
1766
1767fn content_range(
1769 response: &reqwest::Response,
1770 offset: u64,
1771 download_size: Option<u64>,
1772) -> Option<ContentRangeBytes> {
1773 let range = response
1774 .headers()
1775 .get(reqwest::header::CONTENT_RANGE)
1776 .and_then(|value| value.to_str().ok())
1777 .and_then(ContentRange::parse)?;
1778
1779 let range = match range {
1780 ContentRange::Bytes(range) => range,
1781 ContentRange::UnboundBytes(ContentRangeUnbound {
1782 first_byte,
1783 last_byte,
1784 }) => ContentRangeBytes {
1785 first_byte,
1786 last_byte,
1787 complete_length: download_size?,
1788 },
1789 ContentRange::Unsatisfied(_) => return None,
1790 };
1791
1792 if range.first_byte != offset
1793 || range.last_byte >= range.complete_length
1794 || download_size.is_some_and(|size| size != range.complete_length)
1795 {
1796 return None;
1797 }
1798
1799 Some(range)
1800}
1801
1802struct ProgressReader<'a, R> {
1804 reader: R,
1805 index: usize,
1806 reporter: &'a dyn Reporter,
1807}
1808
1809impl<'a, R> ProgressReader<'a, R> {
1810 fn new(reader: R, index: usize, reporter: &'a dyn Reporter) -> Self {
1812 Self {
1813 reader,
1814 index,
1815 reporter,
1816 }
1817 }
1818}
1819
1820impl<R> AsyncRead for ProgressReader<'_, R>
1821where
1822 R: AsyncRead + Unpin,
1823{
1824 fn poll_read(
1825 mut self: Pin<&mut Self>,
1826 cx: &mut Context<'_>,
1827 buf: &mut ReadBuf<'_>,
1828 ) -> Poll<io::Result<()>> {
1829 Pin::new(&mut self.as_mut().reader)
1830 .poll_read(cx, buf)
1831 .map_ok(|()| {
1832 self.reporter
1833 .on_download_progress(self.index, buf.filled().len() as u64);
1834 })
1835 }
1836}
1837
1838#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1842pub struct HttpArchivePointer {
1843 archive: Archive,
1844}
1845
1846impl HttpArchivePointer {
1847 pub fn read_from(path: impl AsRef<Path>) -> Result<Option<Self>, Error> {
1849 match fs_err::File::open(path.as_ref()) {
1850 Ok(file) => {
1851 let data = DataWithCachePolicy::from_reader(file)?.into_data();
1852 let archive = rmp_serde::from_slice::<Archive>(&data)?;
1853 Ok(Some(Self { archive }))
1854 }
1855 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
1856 Err(err) => Err(Error::CacheRead(err)),
1857 }
1858 }
1859
1860 pub fn into_archive(self) -> Archive {
1862 self.archive
1863 }
1864
1865 pub fn to_cache_info(&self) -> CacheInfo {
1867 CacheInfo::default()
1868 }
1869
1870 pub fn to_build_info(&self) -> Option<BuildInfo> {
1872 None
1873 }
1874}
1875
1876#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1880pub struct PathArchivePointer {
1881 timestamp: Timestamp,
1882 archive: Archive,
1883}
1884
1885impl PathArchivePointer {
1886 pub fn read_from(path: impl AsRef<Path>) -> Result<Option<Self>, Error> {
1888 match fs_err::read(path) {
1889 Ok(cached) => Ok(Some(rmp_serde::from_slice::<Self>(&cached)?)),
1890 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
1891 Err(err) => Err(Error::CacheRead(err)),
1892 }
1893 }
1894
1895 async fn write_to(&self, entry: &CacheEntry) -> Result<(), Error> {
1897 write_atomic(entry.path(), rmp_serde::to_vec(&self)?)
1898 .await
1899 .map_err(Error::CacheWrite)
1900 }
1901
1902 pub fn is_up_to_date(&self, modified: Timestamp) -> bool {
1904 self.timestamp == modified
1905 }
1906
1907 pub fn into_archive(self) -> Archive {
1909 self.archive
1910 }
1911
1912 pub fn to_cache_info(&self) -> CacheInfo {
1914 CacheInfo::from_timestamp(self.timestamp)
1915 }
1916
1917 pub fn to_build_info(&self) -> Option<BuildInfo> {
1919 None
1920 }
1921}