|
11 | 11 | from http import HTTPStatus
|
12 | 12 | from typing import BinaryIO
|
13 | 13 |
|
| 14 | +from pip._vendor.requests import PreparedRequest |
14 | 15 | from pip._vendor.requests.models import Response
|
| 16 | +from pip._vendor.urllib3 import HTTPResponse as URLlib3Response |
| 17 | +from pip._vendor.urllib3._collections import HTTPHeaderDict |
15 | 18 | from pip._vendor.urllib3.exceptions import ReadTimeoutError
|
16 | 19 |
|
17 | 20 | from pip._internal.cli.progress_bars import get_download_progress_renderer
|
18 | 21 | from pip._internal.exceptions import IncompleteDownloadError, NetworkConnectionError
|
19 | 22 | from pip._internal.models.index import PyPI
|
20 | 23 | from pip._internal.models.link import Link
|
21 | 24 | from pip._internal.network.cache import is_from_cache
|
22 |
| -from pip._internal.network.session import PipSession |
| 25 | +from pip._internal.network.session import CacheControlAdapter, PipSession |
23 | 26 | from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks
|
24 | 27 | from pip._internal.utils.misc import format_size, redact_auth_from_url, splitext
|
25 | 28 |
|
@@ -250,6 +253,67 @@ def _attempt_resumes_or_redownloads(
|
250 | 253 | os.remove(download.output_file.name)
|
251 | 254 | raise IncompleteDownloadError(download)
|
252 | 255 |
|
| 256 | + # If we successfully completed the download via resume, manually cache it |
| 257 | + # as a complete response to enable future caching |
| 258 | + if download.reattempts > 0: |
| 259 | + self._cache_resumed_download(download, first_resp) |
| 260 | + |
| 261 | + def _cache_resumed_download( |
| 262 | + self, download: _FileDownload, original_response: Response |
| 263 | + ) -> None: |
| 264 | + """ |
| 265 | + Manually cache a file that was successfully downloaded via resume retries. |
| 266 | +
|
| 267 | + cachecontrol doesn't cache 206 (Partial Content) responses, since they |
| 268 | + are not complete files. This method manually adds the final file to the |
| 269 | + cache as though it was downloaded in a single request, so that future |
| 270 | + requests can use the cache. |
| 271 | + """ |
| 272 | + url = download.link.url_without_fragment |
| 273 | + if url.startswith("https://"): |
| 274 | + adapter = self._session.adapters["https://"] |
| 275 | + elif url.startswith("http://"): |
| 276 | + adapter = self._session.adapters["http://"] |
| 277 | + else: |
| 278 | + return |
| 279 | + |
| 280 | + # Check if the adapter is the CacheControlAdapter (i.e. caching is enabled) |
| 281 | + if not isinstance(adapter, CacheControlAdapter): |
| 282 | + logger.debug( |
| 283 | + "Skipping resume download caching: no cache controller for %s", url |
| 284 | + ) |
| 285 | + return |
| 286 | + |
| 287 | + synthetic_request = PreparedRequest() |
| 288 | + synthetic_request.prepare(method="GET", url=url, headers={}) |
| 289 | + |
| 290 | + synthetic_response_headers = HTTPHeaderDict() |
| 291 | + for key, value in original_response.headers.items(): |
| 292 | + if key.lower() not in ["content-range", "content-length"]: |
| 293 | + synthetic_response_headers[key] = value |
| 294 | + synthetic_response_headers["content-length"] = str(download.size) |
| 295 | + |
| 296 | + synthetic_response = URLlib3Response( |
| 297 | + body="", |
| 298 | + headers=synthetic_response_headers, |
| 299 | + status=200, |
| 300 | + preload_content=False, |
| 301 | + ) |
| 302 | + |
| 303 | + # Use the cache controller to store this as a complete response |
| 304 | + download.output_file.flush() |
| 305 | + with open(download.output_file.name, "rb") as f: |
| 306 | + adapter.controller.cache_response( |
| 307 | + synthetic_request, |
| 308 | + synthetic_response, |
| 309 | + body=f.read(), |
| 310 | + status_codes=(200, 203, 300, 301, 308), |
| 311 | + ) |
| 312 | + |
| 313 | + logger.debug( |
| 314 | + "Cached resumed download as complete response for future use: %s", url |
| 315 | + ) |
| 316 | + |
253 | 317 | def _http_get_resume(
|
254 | 318 | self, download: _FileDownload, should_match: Response
|
255 | 319 | ) -> Response:
|
|
0 commit comments