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