# frozen_string_literal: true require 'net/http' require 'uri' require 'find' module Dpl module Providers class Bintray < Provider register :bintray status :stable description sq(<<-STR) tbd STR gem 'json' env :bintray opt '--user USER', 'Bintray user', required: true opt '--key KEY', 'Bintray API key', required: true, secret: true opt '--file FILE', 'Path to a descriptor file for the Bintray upload', required: true opt '--passphrase PHRASE', 'Passphrase as configured on Bintray (if GPG signing is used)' opt '--url URL', default: 'https://api.bintray.com', internal: true msgs missing_file: 'Missing descriptor file: %{file}', invalid_file: 'Failed to parse descriptor file %{file}', create_package: 'Creating package %{package_name}', package_attrs: 'Adding attributes for package %{package_name}', create_version: 'Creating version %{version_name}', version_attrs: 'Adding attributes for version %{version_name}', upload_file: 'Uploading file %{source} to %{target}', sign_version: 'Signing version %s passphrase', publish_version: 'Publishing version %{version_name} of package %{package_name}', missing_path: 'Path: %{path} does not exist.', list_download: 'Listing %{path} in downloads', retrying: '%{code} response from Bintray. It may take some time for a version to be published, retrying in %{pause} sec ... (%{count}/%{max})', giveup_retries: 'Too many retries failed, giving up, something went wrong.', unexpected_code: 'Unexpected HTTP response code %s while checking if the %s exists', request_failed: '%s %s returned unexpected HTTP response code %s', request_success: 'Bintray response: %s %s. %s' PATHS = { packages: '/packages/%{subject}/%{repo}', package: '/packages/%{subject}/%{repo}/%{package_name}', package_attrs: '/packages/%{subject}/%{repo}/%{package_name}/attributes', versions: '/packages/%{subject}/%{repo}/%{package_name}/versions', version: '/packages/%{subject}/%{repo}/%{package_name}/versions/%{version_name}', version_attrs: '/packages/%{subject}/%{repo}/%{package_name}/versions/%{version_name}/attributes', version_sign: '/gpg/%{subject}/%{repo}/%{package_name}/versions/%{version_name}', version_publish: '/content/%{subject}/%{repo}/%{package_name}/%{version_name}/publish', version_file: '/content/%{subject}/%{repo}/%{package_name}/%{version_name}/%{target}', file_metadata: '/file_metadata/%{subject}/%{repo}/%{target}' }.freeze MAP = { package: %i[name desc licenses labels vcs_url website_url issue_tracker_url public_download_numbers public_stats], version: %i[name desc released vcs_tag github_release_notes_file github_use_tag_release_notes attributes] }.freeze def install require 'json' end def validate error :missing_file unless File.exist?(file) # validate that the repo exists, and we have access end def deploy create_package unless package_exists? create_version unless version_exists? upload_files sign_version if sign_version? publish_version && update_files if publish_version? end def package_exists? exists?(:package) end def create_package info :create_package post(path(:packages), compact(only(package, *MAP[:package]))) return unless package_attrs info :package_attrs post(path(:package_attrs), package_attrs) end def version_exists? exists?(:version) end def create_version info :create_version post(path(:versions), compact(only(version, *MAP[:version]))) return unless version_attrs info :version_attrs post(path(:version_attrs), version_attrs) end def upload_files files.each do |file| info :upload_file, source: file.source, target: file.target put(path(:version_file, target: file.target), file.read, file.params) end end def sign_version body = compact(passphrase: passphrase) info :sign_version, (passphrase? ? 'with' : 'without') post(path(:version_sign), body) end def publish_version info :publish_version post(path(:version_publish)) end def update_files files.select(&:download).each do |file| info :list_download, path: file.target update_file(file) end end def update_file(file) retrying(max: 10, pause: 5) do body = { list_in_downloads: file.download }.to_json headers = { 'Content-Type': 'application/json' } put(path(:file_metadata, target: file.target), body, {}, headers) end end def retrying(opts) 1.upto(opts[:max]) do |count| code = yield return if code < 400 info :retrying, opts.merge(count: count, code: code) sleep opts[:pause] end error :giveup_retries end def files return {} unless files = descriptor[:files] return @files if @files keys = %i[path includePattern excludePattern uploadPattern matrixParams listInDownloads] files = files.map { |file| file if file[:path] = path_for(file[:includePattern]) } @files = files.compact.map { |file| find(*file.values_at(*keys)) }.flatten end def find(path, includes, excludes, uploads, params, download) paths = Find.find(path).select { |path| File.file?(path) } paths = paths.reject { |path| excluded?(path, excludes) } paths = paths.map { |path| [path, path.match(/#{includes}/)] } paths = paths.select(&:last) paths.map { |path, match| Upload.new(path, fmt(uploads, match.captures), params, download) } end def fmt(pattern, captures) captures.each.with_index.inject(pattern) do |pattern, (capture, ix)| pattern.gsub("$#{ix + 1}", capture) end end def excluded?(path, pattern) !pattern.to_s.empty? && path.match(/#{pattern}/) end def path_for(str) ix = str.index('(') path = ix.to_i.zero? ? str : str[0, ix] return path if File.exist?(path) warn(:missing_path, path: path) nil end def exists?(type) case code = head(path(type), raise: false, silent: true) when 200, 201 then true when 404 then false else error :unexpected_code, code, type end end def head(path, opts = {}) req = Net::HTTP::Head.new(path) req.basic_auth(user, key) request(req, opts) end def post(path, body = nil) req = Net::HTTP::Post.new(path) req.add_field('Content-Type', 'application/json') req.basic_auth(user, key) req.body = JSON.dump(body) if body request(req) end def put(path, body, params = {}, headers = {}) req = Net::HTTP::Put.new(append_params(path, params)) headers.each { |key, value| req.add_field(key.to_s, value) } req.basic_auth(user, key) req.body = body request(req) end def request(req, opts = {}) res = http.request(req) handle(req, res, opts) res.code.to_i end def http http = Net::HTTP.new(url.host, url.port) http.use_ssl = true http end def append_params(path, params) [path, *Array(params).map { |pair| pair.join('=') }].join(';') end def handle(req, res, opts = { raise: true }) error :request_failed, req.method, req.uri, res.code if opts[:raise] && !success?(res.code) info :request_success, res.code, res.message, parse(res)['message'] unless opts[:silent] res.code.to_i end def success?(code) code.to_s[0].to_i == 2 end def descriptor @descriptor ||= symbolize(JSON.parse(File.read(file))) rescue StandardError error :invalid_file end def url @url ||= URI.parse(super || URL) end def package descriptor[:package] end def package_name package[:name] end def package_attrs package[:attributes] end def subject package[:subject] end def repo package[:repo] end def version descriptor[:version] end def version_name version[:name] end def version_attrs version[:attributes] end def sign_version? version[:gpgSign] end def publish_version? descriptor[:publish] end def path(resource, args = {}) interpolate(PATHS[resource], args, secure: true) end def parse(json) hash = JSON.parse(json) hash.is_a?(Hash) ? hash : {} rescue StandardError {} end def compact(hash) hash.reject { |_, value| value.nil? } end def only(hash, *keys) hash.select { |key, _| keys.include?(key) } end class Upload < Struct.new(:source, :target, :params, :download) def read IO.read(source) end def eql?(other) source == other.source end end end end end