Skip to content
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

New feature: Allow download from private GitHub repository #1763

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions Library/Homebrew/download_strategy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,110 @@ def _fetch
end
end

# GitHubPrivateRepositoryDownloadStrategy downloads contents from GitHub
# Private Repository. To use it, add
# ":using => GitHubPrivateRepositoryDownloadStrategy" to the URL section of
# your formula. This download strategy uses GitHub access tokens (in the
# environment variables HOMEBREW_GITHUB_API_TOKEN) to sign the request. This
# strategy is suitable for corporate use just like S3DownloadStrategy, because
# it lets you use a private GttHub repository for internal distribution. It
# works with public one, but in that case simply use CurlDownloadStrategy.
class GitHubPrivateRepositoryDownloadStrategy < CurlDownloadStrategy
require "utils/formatter"
require "utils/github"

def initialize(name, resource)
super
parse_url_pattern
set_github_token
end

def parse_url_pattern
url_pattern = %r{https://github.com/([^/]+)/([^/]+)/(\S+)}
unless @url =~ url_pattern
raise CurlDownloadStrategyError, "Invalid url pattern for GitHub Repository."
end

_, @owner, @repo, @filepath = *@url.match(url_pattern)
end

def download_url
"https://#{@github_token}@github.com/#{@owner}/#{@repo}/#{@filepath}"
end

def _fetch
curl download_url, "-C", downloaded_size, "-o", temporary_path
end

private

def set_github_token
@github_token = ENV["HOMEBREW_GITHUB_API_TOKEN"]
unless @github_token
raise CurlDownloadStrategyError, "Environmental variable HOMEBREW_GITHUB_API_TOKEN is required."
end
validate_github_repository_access!
end

def validate_github_repository_access!
# Test access to the repository
GitHub.repository(@owner, @repo)
rescue GitHub::HTTPNotFoundError
# We only handle HTTPNotFoundError here,
# becase AuthenticationFailedError is handled within util/github.
message = <<-EOS.undent
HOMEBREW_GITHUB_API_TOKEN can not access the repository: #{@owner}/#{@repo}
This token may not have permission to access the repository or the url of formula may be incorrect.
EOS
raise CurlDownloadStrategyError, message
end
end

# GitHubPrivateRepositoryReleaseDownloadStrategy downloads tarballs from GitHub
# Release assets. To use it, add
# ":using => GitHubPrivateRepositoryReleaseDownloadStrategy" to the URL section
# of your formula. This download strategy uses GitHub access tokens (in the
# environment variables HOMEBREW_GITHUB_API_TOKEN) to sign the request.
class GitHubPrivateRepositoryReleaseDownloadStrategy < GitHubPrivateRepositoryDownloadStrategy
def parse_url_pattern
url_pattern = %r{https://github.com/([^/]+)/([^/]+)/releases/download/([^/]+)/(\S+)}
unless @url =~ url_pattern
raise CurlDownloadStrategyError, "Invalid url pattern for GitHub Release."
end

_, @owner, @repo, @tag, @filename = *@url.match(url_pattern)
end

def download_url
"https://#{@github_token}@api.github.com/repos/#{@owner}/#{@repo}/releases/assets/#{asset_id}"
end

def _fetch
# HTTP request header `Accept: application/octet-stream` is required.
# Without this, the GitHub API will respond with metadata, not binary.
curl download_url, "-C", downloaded_size, "-o", temporary_path, "-H", "Accept: application/octet-stream"
end

private

def asset_id
@asset_id ||= resolve_asset_id
end

def resolve_asset_id
release_metadata = fetch_release_metadata
assets = release_metadata["assets"].select { |a| a["name"] == @filename }
raise CurlDownloadStrategyError, "Asset file not found." if assets.empty?

assets.first["id"]
end

def fetch_release_metadata
release_url = "https://api.github.com/repos/#{@owner}/#{@repo}/releases/tags/#{@tag}"
GitHub.open(release_url)
end
end

class SubversionDownloadStrategy < VCSDownloadStrategy
def initialize(name, resource)
super
Expand Down
76 changes: 75 additions & 1 deletion Library/Homebrew/test/download_strategies_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
require "download_strategy"

class ResourceDouble
attr_reader :url, :specs, :version
attr_reader :url, :specs, :version, :mirrors

def initialize(url = "http://example.com/foo.tar.gz", specs = {})
@url = url
@specs = specs
@mirrors = []
end
end

Expand Down Expand Up @@ -60,6 +61,79 @@ def cache_tag
end
end

class GitHubPrivateRepositoryDownloadStrategyTests < Homebrew::TestCase
def setup
resource = ResourceDouble.new("https://github.com/owner/repo/archive/1.1.5.tar.gz")
ENV["HOMEBREW_GITHUB_API_TOKEN"] = "token"
GitHub.stubs(:repository).returns {}
@strategy = GitHubPrivateRepositoryDownloadStrategy.new("foo", resource)
end

def test_set_github_token
assert_equal "token", @strategy.instance_variable_get(:@github_token)
end

def test_parse_url_pattern
assert_equal "owner", @strategy.instance_variable_get(:@owner)
assert_equal "repo", @strategy.instance_variable_get(:@repo)
assert_equal "archive/1.1.5.tar.gz", @strategy.instance_variable_get(:@filepath)
end

def test_download_url
expected = "https://token@github.com/owner/repo/archive/1.1.5.tar.gz"
assert_equal expected, @strategy.download_url
end
end

class GitHubPrivateRepositoryReleaseDownloadStrategyTests < Homebrew::TestCase
def setup
resource = ResourceDouble.new("https://github.com/owner/repo/releases/download/tag/foo_v0.1.0_darwin_amd64.tar.gz")
ENV["HOMEBREW_GITHUB_API_TOKEN"] = "token"
GitHub.stubs(:repository).returns {}
@strategy = GitHubPrivateRepositoryReleaseDownloadStrategy.new("foo", resource)
end

def test_parse_url_pattern
assert_equal "owner", @strategy.instance_variable_get(:@owner)
assert_equal "repo", @strategy.instance_variable_get(:@repo)
assert_equal "tag", @strategy.instance_variable_get(:@tag)
assert_equal "foo_v0.1.0_darwin_amd64.tar.gz", @strategy.instance_variable_get(:@filename)
end

def test_download_url
@strategy.stubs(:resolve_asset_id).returns(456)
expected = "https://token@api.github.com/repos/owner/repo/releases/assets/456"
assert_equal expected, @strategy.download_url
end

def test_resolve_asset_id
release_metadata = {
"assets" => [
{
"id" => 123,
"name" => "foo_v0.1.0_linux_amd64.tar.gz",
},
{
"id" => 456,
"name" => "foo_v0.1.0_darwin_amd64.tar.gz",
},
],
}
@strategy.stubs(:fetch_release_metadata).returns(release_metadata)
assert_equal 456, @strategy.send(:resolve_asset_id)
end

def test_fetch_release_metadata
expected_release_url = "https://api.github.com/repos/owner/repo/releases/tags/tag"
github_mock = MiniTest::Mock.new
github_mock.expect :call, {}, [expected_release_url]
GitHub.stub :open, github_mock do
@strategy.send(:fetch_release_metadata)
end
github_mock.verify
end
end

class GitDownloadStrategyTests < Homebrew::TestCase
include FileUtils

Expand Down