diff --git a/bin/download-tarball b/bin/download-tarball new file mode 100755 index 0000000..c61ac97 --- /dev/null +++ b/bin/download-tarball @@ -0,0 +1,46 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'optparse' + +require_relative '../lib/download_tarball' +require_relative '../lib/errors' + +options = { + repo: 'emacs-mirror/emacs', + output: File.expand_path('../tarballs', __dir__), + log_level: :info +} + +OptionParser.new do |opts| + opts.banner = <<~TXT + Usage: ./download-tarball [options] + + Download a tarball of given GitHub repository branch, tag, or SHA. + + Options: + TXT + + opts.on('-r', '--repo STRING', + "GitHub repository (default: #{options[:repo]})") do |v| + options[:repo] = v + end + + opts.on('-o', '--output DIR', 'Directory to save tarball in ' \ + "(default: #{options[:output]})") do |v| + options[:output] = v + end + + opts.on('-l', '--log-level LEVEL', 'Log level ' \ + "(default: #{options[:log_level]})") do |v| + options[:log_level] = v.to_sym + end +end.parse! + +begin + tarball = DownloadTarball.new(ref: ARGV[0], **options).perform + + puts tarball.to_yaml +rescue Error => e + handle_error(e) +end diff --git a/lib/commit.rb b/lib/commit.rb new file mode 100644 index 0000000..ac608e3 --- /dev/null +++ b/lib/commit.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'yaml' + +class Commit + attr_reader :sha + attr_reader :time + + def initialize(sha:, time:) + @sha = sha + @time = time + end + + def sha_short + sha[0..6] + end + + def to_hash + { + 'sha' => sha, + 'sha_short' => sha_short, + 'time' => time + } + end + + def to_yaml + to_hash.to_yaml + end +end diff --git a/lib/common.rb b/lib/common.rb new file mode 100644 index 0000000..e25b463 --- /dev/null +++ b/lib/common.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'net/http' + +module Common + private + + def run_cmd(*args) + info "executing: #{args.join(' ')}" + system(*args) || err("Exit code: #{$CHILD_STATUS.exitstatus}") + end + + def http_get(url) + response = Net::HTTP.get_response(URI.parse(url)) + return unless response.code == '200' + + response.body + end +end diff --git a/lib/download_tarball.rb b/lib/download_tarball.rb new file mode 100644 index 0000000..ae37824 --- /dev/null +++ b/lib/download_tarball.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'json' +require 'time' + +require_relative './common' +require_relative './commit' +require_relative './output' +require_relative './tarball' + +class DownloadTarball + include Common + include Output + + logger_name 'download-tarball' + + TARBALL_URL = 'https://github.com/%s/tarball/%s' + COMMIT_URL = 'https://api.github.com/repos/%s/commits/%s' + + attr_reader :ref + attr_reader :repo + attr_reader :output + attr_reader :log_level + + def initialize(ref:, repo:, output:, log_level:) + @ref = ref + @repo = repo + @output = output + @log_level = log_level + + err 'branch/tag/sha argument cannot be empty' if ref.nil? || ref.empty? + end + + def perform + FileUtils.mkdir_p(output) + tarball = Tarball.new(file: target, commit: commit) + + if File.exist?(target) + info "#{filename} already exists locally, attempting to use." + return tarball + end + + info 'Downloading tarball from GitHub. This could take a while, ' \ + 'please be patient.' + result = run_cmd('curl', '-L', url, '-o', target) + err 'Download failed.' if !result || !File.exist?(target) + + tarball + end + + def url + @url ||= format(TARBALL_URL, repo, commit.sha) + end + + def filename + @filename ||= "#{repo.gsub(/[^\w]/, '-')}-#{commit.sha_short}.tgz" + end + + def target + @target ||= File.join(output, filename) + end + + def commit + return @commit if @commit + + info "Fetching info for git ref: #{ref}" + url = format(COMMIT_URL, repo, ref) + commit_json = http_get(url) + err "Failed to get commit info about: #{ref}" if commit_json.nil? + + parsed = JSON.parse(commit_json) + commit = Commit.new( + sha: parsed&.dig('sha'), + time: Time.parse(parsed&.dig('commit', 'committer', 'date')) + ) + + err 'Failed to get commit SHA' if commit.sha.nil? || commit.sha.empty? + err 'Failed to get commit time' if commit.time.nil? + + @commit = commit + end +end diff --git a/lib/errors.rb b/lib/errors.rb new file mode 100644 index 0000000..5af62fd --- /dev/null +++ b/lib/errors.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +def handle_error(err) + warn "ERROR: #{err.message}" + Process.exit 1 +end + +class Error < StandardError; end diff --git a/lib/output.rb b/lib/output.rb new file mode 100644 index 0000000..04f227c --- /dev/null +++ b/lib/output.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'forwardable' +require 'logger' + +require_relative './errors' + +module Output + extend Forwardable + + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def logger_name(name = nil) + return @logger_name if name.nil? + + @logger_name = name + end + end + + def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unkonwn + + def err(msg = nil) + raise Error, msg + end + + private + + # override to set custom log level + def log_level + :info + end + + def logger + @logger ||= Logger.new($stderr).tap do |l| + l.progname = self.class.logger_name + l.level = log_level + l.formatter = log_formatter + end + end + + def log_formatter + proc do |severity, _datetime, progname, msg| + "==> [#{progname}] #{severity}: #{msg}\n" + end + end +end diff --git a/lib/tarball.rb b/lib/tarball.rb new file mode 100644 index 0000000..6526797 --- /dev/null +++ b/lib/tarball.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'yaml' + +class Tarball + attr_reader :file + attr_reader :commit + + def initialize(file:, commit:) + @file = file + @commit = commit + end + + def to_hash + { + 'file' => file, + 'commit' => commit.to_hash + } + end + + def to_yaml + to_hash.to_yaml + end +end