From b9cc52db455b879fa048421851d7bd43bddde817 Mon Sep 17 00:00:00 2001 From: Masayuki Morita Date: Mon, 2 Jan 2017 12:56:20 +0900 Subject: [PATCH] New feature: GitHubReleaseDownloadStrategy GitHubReleaseDownloadStrategy downloads tarballs from GitHub Release assets. To use it, add ":using => GitHubReleaseDownloadStrategy" to the URL section of your formula. This download strategy uses GitHub access tokens (in the environment variables GITHUB_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. --- Library/Homebrew/download_strategy.rb | 76 +++++++++++++++++++ .../Homebrew/test/download_strategies_test.rb | 47 +++++++++++- 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/Library/Homebrew/download_strategy.rb b/Library/Homebrew/download_strategy.rb index 9f9b2ababe..fa54ef7166 100644 --- a/Library/Homebrew/download_strategy.rb +++ b/Library/Homebrew/download_strategy.rb @@ -532,6 +532,82 @@ class S3DownloadStrategy < CurlDownloadStrategy end end +# GitHubReleaseDownloadStrategy downloads tarballs from GitHub Release assets. +# To use it, add ":using => GitHubReleaseDownloadStrategy" to the URL section +# of your formula. This download strategy uses GitHub access tokens (in the +# environment variables GITHUB_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 GitHubReleaseDownloadStrategy < CurlDownloadStrategy + require 'open-uri' + + def initialize(name, resource) + super + + @github_token = ENV["GITHUB_TOKEN"] + unless @github_token + puts "Environmental variable GITHUB_TOKEN is required." + raise CurlDownloadStrategyError, @url + end + + url_pattern = %r|https://github.com/(\S+)/(\S+)/releases/download/(\S+)/(\S+)| + unless @url =~ url_pattern + puts "Invalid url pattern for GitHub Release." + raise CurlDownloadStrategyError, @url + end + + _, @owner, @repo, @tag, @filename = *(@url.match(url_pattern)) + end + + def _fetch + puts "Download asset_id: #{asset_id}" + # HTTP request header `Accept: application/octet-stream` is required. + # Without this, the GitHub API will respond with metadata, not binary. + curl asset_url, "-C", downloaded_size, "-o", temporary_path, "-H", 'Accept: application/octet-stream' + end + + private + + def asset_url + "https://#{@github_token}@api.github.com/repos/#{@owner}/#{@repo}/releases/assets/#{asset_id}" + end + + 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 } + if assets.empty? + puts "Asset file not found." + raise CurlDownloadStrategyError, @url + end + + return assets.first["id"] + end + + def release_url + "https://api.github.com/repos/#{@owner}/#{@repo}/releases/tags/#{@tag}" + end + + def fetch_release_metadata + begin + release_response = open(release_url, {:http_basic_authentication => [@github_token]}).read + rescue OpenURI::HTTPError => e + if e.message == '404 Not Found' + puts "GitHub Release not found." + raise CurlDownloadStrategyError, @url + else + raise e + end + end + + return JSON.parse(release_response) + end +end + class SubversionDownloadStrategy < VCSDownloadStrategy def initialize(name, resource) super diff --git a/Library/Homebrew/test/download_strategies_test.rb b/Library/Homebrew/test/download_strategies_test.rb index 87218fb12b..1df0267af3 100644 --- a/Library/Homebrew/test/download_strategies_test.rb +++ b/Library/Homebrew/test/download_strategies_test.rb @@ -2,11 +2,12 @@ require "testing_env" 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 @@ -60,6 +61,50 @@ class VCSDownloadStrategyTests < Homebrew::TestCase end end +class GitHubReleaseDownloadStrategyTests < Homebrew::TestCase + def setup + resource = ResourceDouble.new("https://github.com/owner/repo/releases/download/tag/foo_v0.1.0_darwin_amd64.tar.gz") + ENV["GITHUB_TOKEN"] = "token" + @strategy = GitHubReleaseDownloadStrategy.new("foo", resource) + end + + def test_initialize + assert_equal "token", @strategy.instance_variable_get(:@github_token) + 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_asset_url + @strategy.stubs(:resolve_asset_id).returns(456) + expected = "https://token@api.github.com/repos/owner/repo/releases/assets/456" + assert_equal expected, @strategy.send(:asset_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_release_url + expected = "https://api.github.com/repos/owner/repo/releases/tags/tag" + assert_equal expected, @strategy.send(:release_url) + end +end + class GitDownloadStrategyTests < Homebrew::TestCase include FileUtils