|
| 1 | +require "download_strategy" |
| 2 | + |
| 3 | +# S3DownloadStrategy downloads tarballs from AWS S3. |
| 4 | +# To use it, add `:using => :s3` to the URL section of your |
| 5 | +# formula. This download strategy uses AWS access tokens (in the |
| 6 | +# environment variables `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`) |
| 7 | +# to sign the request. This strategy is good in a corporate setting, |
| 8 | +# because it lets you use a private S3 bucket as a repo for internal |
| 9 | +# distribution. (It will work for public buckets as well.) |
| 10 | +class S3DownloadStrategy < CurlDownloadStrategy |
| 11 | + def initialize(url, name, version, **meta) |
| 12 | + super |
| 13 | + end |
| 14 | + |
| 15 | + def _fetch(url:, resolved_url:, timeout:) |
| 16 | + if url !~ %r{^https?://([^.].*)\.s3\.amazonaws\.com/(.+)$} && |
| 17 | + url !~ %r{^s3://([^.].*?)/(.+)$} |
| 18 | + raise "Bad S3 URL: " + url |
| 19 | + end |
| 20 | + |
| 21 | + bucket = Regexp.last_match(1) |
| 22 | + key = Regexp.last_match(2) |
| 23 | + |
| 24 | + ENV["AWS_ACCESS_KEY_ID"] = ENV["HOMEBREW_AWS_ACCESS_KEY_ID"] |
| 25 | + ENV["AWS_SECRET_ACCESS_KEY"] = ENV["HOMEBREW_AWS_SECRET_ACCESS_KEY"] |
| 26 | + |
| 27 | + begin |
| 28 | + signer = Aws::S3::Presigner.new |
| 29 | + s3url = signer.presigned_url :get_object, bucket: bucket, key: key |
| 30 | + rescue Aws::Sigv4::Errors::MissingCredentialsError |
| 31 | + ohai "AWS credentials missing, trying public URL instead." |
| 32 | + s3url = url |
| 33 | + end |
| 34 | + |
| 35 | + curl_download s3url, to: temporary_path |
| 36 | + end |
| 37 | +end |
| 38 | + |
| 39 | +# GitHubPrivateRepositoryDownloadStrategy downloads contents from GitHub |
| 40 | +# Private Repository. To use it, add |
| 41 | +# `:using => :github_private_repo` to the URL section of |
| 42 | +# your formula. This download strategy uses GitHub access tokens (in the |
| 43 | +# environment variables `HOMEBREW_GITHUB_API_TOKEN`) to sign the request. This |
| 44 | +# strategy is suitable for corporate use just like S3DownloadStrategy, because |
| 45 | +# it lets you use a private GitHub repository for internal distribution. It |
| 46 | +# works with public one, but in that case simply use CurlDownloadStrategy. |
| 47 | +class GitHubPrivateRepositoryDownloadStrategy < CurlDownloadStrategy |
| 48 | + require "utils/formatter" |
| 49 | + require "utils/github" |
| 50 | + |
| 51 | + def initialize(url, name, version, **meta) |
| 52 | + super |
| 53 | + parse_url_pattern |
| 54 | + set_github_token |
| 55 | + end |
| 56 | + |
| 57 | + def parse_url_pattern |
| 58 | + unless match = url.match(%r{https://github.com/([^/]+)/([^/]+)/(\S+)}) |
| 59 | + raise CurlDownloadStrategyError, "Invalid url pattern for GitHub Repository." |
| 60 | + end |
| 61 | + |
| 62 | + _, @owner, @repo, @filepath = *match |
| 63 | + end |
| 64 | + |
| 65 | + def download_url |
| 66 | + "https://#{@github_token}@github.com/#{@owner}/#{@repo}/#{@filepath}" |
| 67 | + end |
| 68 | + |
| 69 | + private |
| 70 | + |
| 71 | + def _fetch(url:, resolved_url:, timeout:) |
| 72 | + curl_download download_url, to: temporary_path |
| 73 | + end |
| 74 | + |
| 75 | + def set_github_token |
| 76 | + @github_token = ENV["HOMEBREW_GITHUB_API_TOKEN"] |
| 77 | + unless @github_token |
| 78 | + raise CurlDownloadStrategyError, "Environmental variable HOMEBREW_GITHUB_API_TOKEN is required." |
| 79 | + end |
| 80 | + |
| 81 | + validate_github_repository_access! |
| 82 | + end |
| 83 | + |
| 84 | + def validate_github_repository_access! |
| 85 | + # Test access to the repository |
| 86 | + GitHub.repository(@owner, @repo) |
| 87 | + rescue GitHub::HTTPNotFoundError |
| 88 | + # We only handle HTTPNotFoundError here, |
| 89 | + # becase AuthenticationFailedError is handled within util/github. |
| 90 | + message = <<~EOS |
| 91 | + HOMEBREW_GITHUB_API_TOKEN can not access the repository: #{@owner}/#{@repo} |
| 92 | + This token may not have permission to access the repository or the url of formula may be incorrect. |
| 93 | + EOS |
| 94 | + raise CurlDownloadStrategyError, message |
| 95 | + end |
| 96 | +end |
| 97 | + |
| 98 | +# GitHubPrivateRepositoryReleaseDownloadStrategy downloads tarballs from GitHub |
| 99 | +# Release assets. To use it, add `:using => :github_private_release` to the URL section |
| 100 | +# of your formula. This download strategy uses GitHub access tokens (in the |
| 101 | +# environment variables HOMEBREW_GITHUB_API_TOKEN) to sign the request. |
| 102 | +class GitHubPrivateRepositoryReleaseDownloadStrategy < GitHubPrivateRepositoryDownloadStrategy |
| 103 | + def initialize(url, name, version, **meta) |
| 104 | + super |
| 105 | + end |
| 106 | + |
| 107 | + def parse_url_pattern |
| 108 | + url_pattern = %r{https://github.com/([^/]+)/([^/]+)/releases/download/([^/]+)/(\S+)} |
| 109 | + unless @url =~ url_pattern |
| 110 | + raise CurlDownloadStrategyError, "Invalid url pattern for GitHub Release." |
| 111 | + end |
| 112 | + |
| 113 | + _, @owner, @repo, @tag, @filename = *@url.match(url_pattern) |
| 114 | + end |
| 115 | + |
| 116 | + def download_url |
| 117 | + "https://api.github.com/repos/#{@owner}/#{@repo}/releases/assets/#{asset_id}" |
| 118 | + end |
| 119 | + |
| 120 | + private |
| 121 | + |
| 122 | + def _fetch(url:, resolved_url:, timeout:) |
| 123 | + # HTTP request header `Accept: application/octet-stream` is required. |
| 124 | + # Without this, the GitHub API will respond with metadata, not binary. |
| 125 | + curl_download download_url, "--header", "Accept: application/octet-stream", "--header", "Authorization: token #{@github_token}", to: temporary_path |
| 126 | + end |
| 127 | + |
| 128 | + def asset_id |
| 129 | + @asset_id ||= resolve_asset_id |
| 130 | + end |
| 131 | + |
| 132 | + def resolve_asset_id |
| 133 | + release_metadata = fetch_release_metadata |
| 134 | + assets = release_metadata["assets"].select { |a| a["name"] == @filename } |
| 135 | + raise CurlDownloadStrategyError, "Asset file not found." if assets.empty? |
| 136 | + |
| 137 | + assets.first["id"] |
| 138 | + end |
| 139 | + |
| 140 | + def fetch_release_metadata |
| 141 | + release_url = "https://api.github.com/repos/#{@owner}/#{@repo}/releases/tags/#{@tag}" |
| 142 | + GitHub::API.open_rest(release_url) |
| 143 | + end |
| 144 | +end |
| 145 | + |
| 146 | +# ScpDownloadStrategy downloads files using ssh via scp. To use it, add |
| 147 | +# `:using => :scp` to the URL section of your formula or |
| 148 | +# provide a URL starting with scp://. This strategy uses ssh credentials for |
| 149 | +# authentication. If a public/private keypair is configured, it will not |
| 150 | +# prompt for a password. |
| 151 | +# |
| 152 | +# @example |
| 153 | +# class Abc < Formula |
| 154 | +# url "scp://example.com/src/abc.1.0.tar.gz" |
| 155 | +# ... |
| 156 | +class ScpDownloadStrategy < AbstractFileDownloadStrategy |
| 157 | + def initialize(url, name, version, **meta) |
| 158 | + super |
| 159 | + parse_url_pattern |
| 160 | + end |
| 161 | + |
| 162 | + def parse_url_pattern |
| 163 | + url_pattern = %r{scp://([^@]+@)?([^@:/]+)(:\d+)?/(\S+)} |
| 164 | + if @url !~ url_pattern |
| 165 | + raise ScpDownloadStrategyError, "Invalid URL for scp: #{@url}" |
| 166 | + end |
| 167 | + |
| 168 | + _, @user, @host, @port, @path = *@url.match(url_pattern) |
| 169 | + end |
| 170 | + |
| 171 | + def fetch |
| 172 | + ohai "Downloading #{@url}" |
| 173 | + |
| 174 | + if cached_location.exist? |
| 175 | + puts "Already downloaded: #{cached_location}" |
| 176 | + else |
| 177 | + system_command! "scp", args: [scp_source, temporary_path.to_s] |
| 178 | + ignore_interrupts { temporary_path.rename(cached_location) } |
| 179 | + end |
| 180 | + end |
| 181 | + |
| 182 | + def clear_cache |
| 183 | + super |
| 184 | + rm_rf(temporary_path) |
| 185 | + end |
| 186 | + |
| 187 | + private |
| 188 | + |
| 189 | + def scp_source |
| 190 | + path_prefix = "/" unless @path.start_with?("~") |
| 191 | + port_arg = "-P #{@port[1..-1]} " if @port |
| 192 | + "#{port_arg}#{@user}#{@host}:#{path_prefix}#{@path}" |
| 193 | + end |
| 194 | +end |
| 195 | + |
| 196 | +class DownloadStrategyDetector |
| 197 | + class << self |
| 198 | + module Compat |
| 199 | + def detect(url, using = nil) |
| 200 | + strategy = super |
| 201 | + require_aws_sdk if strategy == S3DownloadStrategy |
| 202 | + strategy |
| 203 | + end |
| 204 | + |
| 205 | + def detect_from_url(url) |
| 206 | + case url |
| 207 | + when %r{^s3://} |
| 208 | + S3DownloadStrategy |
| 209 | + when %r{^scp://} |
| 210 | + ScpDownloadStrategy |
| 211 | + else |
| 212 | + super(url) |
| 213 | + end |
| 214 | + end |
| 215 | + |
| 216 | + def detect_from_symbol(symbol) |
| 217 | + case symbol |
| 218 | + when :github_private_repo |
| 219 | + GitHubPrivateRepositoryDownloadStrategy |
| 220 | + when :github_private_release |
| 221 | + GitHubPrivateRepositoryReleaseDownloadStrategy |
| 222 | + when :s3 |
| 223 | + S3DownloadStrategy |
| 224 | + when :scp |
| 225 | + ScpDownloadStrategy |
| 226 | + else |
| 227 | + super(symbol) |
| 228 | + end |
| 229 | + end |
| 230 | + end |
| 231 | + |
| 232 | + prepend Compat |
| 233 | + end |
| 234 | +end |
0 commit comments