-
Notifications
You must be signed in to change notification settings - Fork 63
chore: replace git dependency for http #2490
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,106 @@ | ||||||||||||||||||||
| """Generic HTTP archive downloader.""" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import hashlib | ||||||||||||||||||||
| import os | ||||||||||||||||||||
| import shutil | ||||||||||||||||||||
| import tarfile | ||||||||||||||||||||
| import zipfile | ||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||
| from typing import Tuple | ||||||||||||||||||||
| from urllib.parse import urlparse | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import requests | ||||||||||||||||||||
|
|
||||||||||||||||||||
| from common.logging import get_logger | ||||||||||||||||||||
|
|
||||||||||||||||||||
| LOGGER = get_logger(__name__) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Supported archive formats (order matters for detection) | ||||||||||||||||||||
| SUPPORTED_ARCHIVE_FORMATS = {".tar.gz": "tarball", ".zip": "zip"} | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| def _get_archive_type(archive_path: str) -> str | None: | ||||||||||||||||||||
| """Detect archive type from path. Returns 'tarball', 'zip', or None""" | ||||||||||||||||||||
| for ext, archive_type in SUPPORTED_ARCHIVE_FORMATS.items(): | ||||||||||||||||||||
| if archive_path.endswith(ext): | ||||||||||||||||||||
| return archive_type | ||||||||||||||||||||
| return None | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| def extract_commit_sha_from_archive(archive_path: str) -> str: | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| Extract commit SHA from local archive file. | ||||||||||||||||||||
| Returns commit SHA (from PAX headers/ZIP comment, or SHA256 hash as fallback) | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| archive_type = _get_archive_type(archive_path) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| try: | ||||||||||||||||||||
| if archive_type == "zip": | ||||||||||||||||||||
| with zipfile.ZipFile(archive_path, "r") as z: | ||||||||||||||||||||
| if sha := z.comment.decode("utf-8").strip(): | ||||||||||||||||||||
| LOGGER.debug("Extracted commit SHA from ZIP comment: %s", sha[:8]) | ||||||||||||||||||||
| return sha | ||||||||||||||||||||
| elif archive_type == "tarball": | ||||||||||||||||||||
| with tarfile.open(archive_path, "r:gz") as t: | ||||||||||||||||||||
| if (first := next(iter(t), None)) and first.pax_headers: | ||||||||||||||||||||
| if sha := first.pax_headers.get("comment"): | ||||||||||||||||||||
| LOGGER.debug("Extracted commit SHA from PAX headers: %s", sha[:8]) | ||||||||||||||||||||
| return sha | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| LOGGER.error("Unsupported archive format %s", archive_path) | ||||||||||||||||||||
| except Exception as err: | ||||||||||||||||||||
| LOGGER.warning("Could not extract commit SHA: %s", err) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Fallback: SHA256 hash of archive | ||||||||||||||||||||
| LOGGER.warning("No metadata SHA, using SHA256 hash of archive") | ||||||||||||||||||||
| with open(archive_path, "rb") as f: | ||||||||||||||||||||
| return hashlib.sha256(f.read()).hexdigest()[:40] | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why truncating remaining 24 chars? |
||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| class ArchiveDownloader: | ||||||||||||||||||||
| """Generic downloader for tar.gz and zip archives""" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def __init__(self, archive_url: str, auth_headers: dict = None): | ||||||||||||||||||||
| self.archive_url = archive_url | ||||||||||||||||||||
| self.auth_headers = auth_headers or {} | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def _extract_archive(self, archive_path: str, extract_to: str) -> None: | ||||||||||||||||||||
| archive_type = _get_archive_type(archive_path) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if archive_type == "zip": | ||||||||||||||||||||
| with zipfile.ZipFile(archive_path, "r") as z: | ||||||||||||||||||||
| z.extractall(extract_to) | ||||||||||||||||||||
|
Comment on lines
+71
to
+72
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚨 issue (security): ZIP archives are extracted without validating member paths, so a repository archive containing Triggers: When a downloaded ZIP archive contains path-traversal entries. Suggested fix: Validate every ZIP member resolves beneath
Suggested change
|
||||||||||||||||||||
| elif archive_type == "tarball": | ||||||||||||||||||||
| with tarfile.open(archive_path, "r:gz") as t: | ||||||||||||||||||||
| t.extractall(extract_to, filter="data") | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| raise ValueError(f"Unsupported archive format: {archive_path}") | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
sourcery-ai[bot] marked this conversation as resolved.
|
||||||||||||||||||||
| def download_and_extract(self, extract_to: str) -> Tuple[str, str]: | ||||||||||||||||||||
| """Downloads, extracts, and returns (extracted_dir_path, commit_sha)""" | ||||||||||||||||||||
| os.makedirs(extract_to, exist_ok=True) | ||||||||||||||||||||
| # Detect extension from URL path (handles query params, supports multi-suffix like .tar.gz) | ||||||||||||||||||||
| url_path = urlparse(self.archive_url).path | ||||||||||||||||||||
| ext = "".join(Path(url_path).suffixes).lstrip(".") | ||||||||||||||||||||
| archive_path = os.path.join(extract_to, f"archive.{ext}") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| LOGGER.info("Downloading %s", self.archive_url) | ||||||||||||||||||||
| with requests.get(self.archive_url, headers=self.auth_headers, stream=True, timeout=300) as r: | ||||||||||||||||||||
| r.raise_for_status() | ||||||||||||||||||||
| with open(archive_path, "wb") as f: | ||||||||||||||||||||
| # tell Python to stream data securely from the source to drive | ||||||||||||||||||||
| shutil.copyfileobj(r.raw, f) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| commit_sha = extract_commit_sha_from_archive(archive_path) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| self._extract_archive(archive_path, extract_to) | ||||||||||||||||||||
| os.remove(archive_path) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Unpacked archive may have a folder with a different naming inside, find the newly extracted folder | ||||||||||||||||||||
| dirs = [os.path.join(extract_to, d) for d in os.listdir(extract_to) if os.path.isdir(os.path.join(extract_to, d))] | ||||||||||||||||||||
| if not dirs: # We should always have top level directory, so no fallback here | ||||||||||||||||||||
| raise RuntimeError(f"Archive extraction failed: no directories found in {extract_to}") | ||||||||||||||||||||
| extracted_dir = dirs[0] | ||||||||||||||||||||
|
|
||||||||||||||||||||
| LOGGER.debug("Extracted to: %s (SHA: %s)", extracted_dir, commit_sha[:8]) | ||||||||||||||||||||
| return extracted_dir, commit_sha | ||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.