class S3Deployer

Constants

CURRENT_REVISION
DATE_FORMAT
RETRY_TIMES
VERSION

Attributes

config[R]

Public Class Methods

changes(from, to) click to toggle source
# File lib/s3_deployer.rb, line 116
def changes(from, to)
  from_sha = sha_of_revision(from)
  to_sha = sha_of_revision(to)
  if from_sha && to_sha
    `git log --oneline --reverse #{from_sha}...#{to_sha}`.split("\n").map(&:strip)
  else
    []
  end
end
configure(&block) click to toggle source
# File lib/s3_deployer.rb, line 24
def configure(&block)
  @config = Config.new
  @config.instance_eval(&block)
  @config.apply_environment_settings!

  Aws.config.update({
    region: config.region,
    credentials: Aws::Credentials.new(config.access_key_id, config.secret_access_key, config.session_token),
  })
end
current() click to toggle source
# File lib/s3_deployer.rb, line 84
def current
  current_revision = get_current_revision
  if current_revision
    puts "Current revision: #{current_revision} - #{get_datetime_from_revision(current_revision)}"
  else
    puts "There is no information about the current revision"
  end
end
deploy!() click to toggle source
# File lib/s3_deployer.rb, line 40
def deploy!
  revision = config.revision || time_zone.now.strftime(DATE_FORMAT)
  config.before_deploy[revision] if config.before_deploy
  stage!(revision)
  switch!(revision)
  config.after_deploy[revision] if config.after_deploy
end
execute(cmd) click to toggle source
# File lib/s3_deployer.rb, line 35
def execute(cmd)
  puts "Running '#{cmd}'"
  system(cmd, out: $stdout, err: :out)
end
list() click to toggle source
# File lib/s3_deployer.rb, line 104
def list
  puts "Getting the list of deployed revisions..."
  current_revision = get_current_revision
  get_list_of_revisions.each do |rev|
    datetime = get_datetime_from_revision(rev)
    sha = shas_by_revisions[rev]
    title = sha ? `git show -s --format=%s #{sha}`.strip : nil
    string = "#{rev} - #{datetime} #{sha ? " - #{sha[0..7]}" : ""} #{title ? "(#{title})" : ""} #{" <= current" if rev == current_revision}"
    puts string
  end
end
normalize_revision(revision) click to toggle source
# File lib/s3_deployer.rb, line 93
def normalize_revision(revision)
  if revision && !revision.empty?
    datetime = get_datetime_from_revision(revision)
    if datetime
      revision
    else
      shas_by_revisions.detect { |k, v| v.start_with?(revision) }.first
    end
  end
end
sha_of_revision(revision) click to toggle source
# File lib/s3_deployer.rb, line 126
def sha_of_revision(revision)
  shas_by_revisions[revision]
end
stage!(revision = (config.revision || time_zone.now.strftime(DATE_FORMAT))) click to toggle source
# File lib/s3_deployer.rb, line 48
def stage!(revision = (config.revision || time_zone.now.strftime(DATE_FORMAT)))
  puts "Staging #{colorize(:green, revision)}"
  config.before_stage[revision] if config.before_stage
  copy_files_to_s3(revision)
  store_git_hash(revision)
  config.after_stage[revision] if config.after_stage
end
switch!(revision = config.revision) click to toggle source
# File lib/s3_deployer.rb, line 56
def switch!(revision = config.revision)
  current_revision = get_current_revision
  current_sha = sha_of_revision(current_revision)
  sha = sha_of_revision(revision)
  puts "Switching from #{colorize(:green, current_revision)} (#{colorize(:yellow, current_sha && current_sha[0..7])}) " +
    "to #{colorize(:green, revision)} (#{colorize(:yellow, sha && sha[0..7])})"
  if !revision || revision.strip.empty?
    warn "You must specify the revision by REVISION env variable"
    exit(1)
  end
  config.before_switch[current_revision, revision] if config.before_switch
  prefix = config.app_path.empty? ? revision : File.join(revisions_path, revision)
  list_of_objects = []
  Aws::S3::Resource.new.bucket(config.bucket).objects(prefix: prefix).each do |object_summary|
    list_of_objects << object_summary
  end
  Parallel.each(list_of_objects, in_threads: 20) do |object_summary|
    object = object_summary.object
    target_path = config.app_path.empty? ? @config.current_path : File.join(config.app_path, @config.current_path)
    path = object.key.gsub(prefix, target_path)
    value = object.get.body.read
    value = object.content_encoding == "gzip" ? decompress(value) : value
    store_value(path, value)
  end
  store_current_revision(revision)
  config.after_switch[current_revision, revision] if config.after_switch
end

Private Class Methods

app_path_with_bucket() click to toggle source
# File lib/s3_deployer.rb, line 146
def app_path_with_bucket
  File.join(config.bucket, config.app_path)
end
colorize(color, text) click to toggle source
# File lib/s3_deployer.rb, line 261
def colorize(color, text)
  config.colorize ? Color.send(color, text) : text
end
compress(source) click to toggle source
# File lib/s3_deployer.rb, line 241
def compress(source)
  output = Stream.new
  gz = Zlib::GzipWriter.new(output, Zlib::DEFAULT_COMPRESSION, Zlib::DEFAULT_STRATEGY)
  gz.write(source)
  gz.close
  output.string
end
copy_files_to_s3(rev) click to toggle source
# File lib/s3_deployer.rb, line 132
def copy_files_to_s3(rev)
  dir = File.join(revisions_path, rev)
  Parallel.each(source_files_list, in_threads: 20) do |file|
    s3_file = Pathname.new(file).relative_path_from(Pathname.new(config.dist_dir)).to_s
    store_value(File.join(dir, s3_file), File.read(file))
  end
end
current_revision_path() click to toggle source
# File lib/s3_deployer.rb, line 165
def current_revision_path
  File.join(config.app_path, CURRENT_REVISION)
end
decompress(source) click to toggle source
# File lib/s3_deployer.rb, line 249
def decompress(source)
  begin
    Zlib::GzipReader.new(StringIO.new(source)).read
  rescue Zlib::GzipFile::Error
    source
  end
end
get_current_revision() click to toggle source
# File lib/s3_deployer.rb, line 192
def get_current_revision
  get_value(current_revision_path)
rescue Aws::S3::Errors::NoSuchKey
  nil
end
get_datetime_from_revision(revision) click to toggle source
# File lib/s3_deployer.rb, line 150
def get_datetime_from_revision(revision)
  date = Time.strptime(revision, DATE_FORMAT) rescue nil
  date.strftime("%m/%d/%Y %H:%M") if date
end
get_list_of_revisions() click to toggle source
# File lib/s3_deployer.rb, line 140
def get_list_of_revisions
  prefix = revisions_path
  body = Aws::S3::Client.new.list_objects({bucket: config.bucket, delimiter: '/', prefix: prefix + "/"})
  body.common_prefixes.map(&:prefix).map { |e| e.gsub(prefix, "").gsub("/", "") }.sort
end
get_value(key) click to toggle source
# File lib/s3_deployer.rb, line 173
def get_value(key)
  puts "Retrieving value #{key} on S3"
  retry_block(RETRY_TIMES.dup) do
    Aws::S3::Resource.new.bucket(config.bucket).object(key).get.body.read
  end
end
retry_block(sleep_times, &block) click to toggle source
# File lib/s3_deployer.rb, line 215
def retry_block(sleep_times, &block)
  block.call
rescue Exception => e
  puts "#{colorize(:red, "Error!")} #{e}\n\n#{e.backtrace.take(3).join("\n")}"
  no_retry_exceptions = [Aws::S3::Errors::NoSuchKey]
  if no_retry_exceptions.any? { |exc| e.is_a?(exc) }
    raise e
  elsif !sleep_times.empty?
    sleep_time = sleep_times.shift
    puts "Still have #{colorize(:yellow, "#{sleep_times.count} retries")}, so waiting for #{colorize(:yellow, "#{sleep_time} seconds")} and retrying..."
    sleep sleep_time
    retry_block(sleep_times, &block)
  else
    puts "Out of retries, failing..."
    raise e
  end
end
revisions_path() click to toggle source
# File lib/s3_deployer.rb, line 169
def revisions_path
  File.join(config.app_path, "revisions")
end
shas_by_revisions() click to toggle source
# File lib/s3_deployer.rb, line 155
def shas_by_revisions
  @shas_by_revisions ||= get_value(File.join(config.app_path, "SHAS")).split("\n").inject({}) do |memo, line|
    revision, sha = line.split(" - ").map(&:strip)
    memo[revision] = sha
    memo
  end
rescue Aws::S3::Errors::NoSuchKey
  {}
end
should_compress?(key) click to toggle source
# File lib/s3_deployer.rb, line 233
def should_compress?(key)
  if [true, false, nil].include?(config.gzip)
    !!config.gzip
  else
    key != CURRENT_REVISION && Array(config.gzip).any? { |regexp| key.match(regexp) }
  end
end
source_files_list() click to toggle source
# File lib/s3_deployer.rb, line 257
def source_files_list
  Dir.glob(File.join(config.dist_dir, "**/*")).select { |f| File.file?(f) }
end
store_current_revision(revision) click to toggle source
# File lib/s3_deployer.rb, line 180
def store_current_revision(revision)
  store_value(current_revision_path, revision, cache_control: "max-age=0, no-cache")
end
store_git_hash(time) click to toggle source
# File lib/s3_deployer.rb, line 184
def store_git_hash(time)
  value = shas_by_revisions.
    merge(time => `git rev-parse HEAD`.strip).
    map { |sha, rev| "#{sha} - #{rev}" }.join("\n")
  store_value(File.join(config.app_path, "SHAS"), value, cache_control: "max-age=0, no-cache")
  @shas_by_revisions = nil
end
store_value(key, value, options = {}) click to toggle source
# File lib/s3_deployer.rb, line 198
def store_value(key, value, options = {})
  options = {acl: "public-read"}.merge(options)
  if config.cache_control && !config.cache_control.empty?
    options[:cache_control] = config.cache_control
  end
  if should_compress?(key)
    options[:content_encoding] = "gzip"
    value = compress(value)
  end
  mime_type = MIME::Types.of(key).first
  options[:content_type] = mime_type ? mime_type.content_type : "binary/octet-stream"
  retry_block(RETRY_TIMES.dup) do
    puts "Storing value #{colorize(:yellow, key)} on S3#{", #{colorize(:green, 'gzipped')}" if should_compress?(key)}"
    Aws::S3::Resource.new.bucket(config.bucket).object(key).put(options.merge(body: value))
  end
end
time_zone() click to toggle source
# File lib/s3_deployer.rb, line 265
def time_zone
  TZInfo::Timezone.get(config.time_zone)
end