class Nanoc::Extra::Deployers::Fog

A deployer that deploys a site using [fog](github.com/geemus/fog).

@example A deployment configuration with public and staging configurations

deploy:
  public:
    kind:       fog
    bucket:     nanoc-site
    cdn_id:     XXXXXX
  preprod:
    kind:       fog
    provider:   local
    local_root: ~/myCloud
    bucket:     nanoc-site
  staging:
    kind:       fog
    provider:   local
    local_root: ~/myCloud
    bucket:     nanoc-site-staging

@api private

Public Instance Methods

run() click to toggle source

@see Nanoc::Extra::Deployer#run

# File lib/nanoc/extra/deployers/fog.rb, line 25
def run
  require 'fog'

  # Get params, unsetting anything we don't want to pass through to fog.
  src      = File.expand_path(source_path)
  bucket   = config.delete(:bucket) || config.delete(:bucket_name)
  path     = config.delete(:path)
  cdn_id   = config.delete(:cdn_id)

  config.delete(:kind)

  # Validate params
  error 'The path requires no trailing slash' if path && path[-1, 1] == '/'

  # Mock if necessary
  if dry_run?
    puts 'Dry run - simulation'
    ::Fog.mock!
  end

  # Get connection
  puts 'Connecting'
  connection = ::Fog::Storage.new(config)

  # Get bucket
  puts 'Getting bucket'
  begin
    directory = connection.directories.get(bucket, prefix: path)
  rescue ::Excon::Errors::NotFound
    should_create_bucket = true
  end
  should_create_bucket = true if directory.nil?

  # Create bucket if necessary
  if should_create_bucket
    puts 'Creating bucket'
    directory = connection.directories.create(key: bucket, prefix: path)
  end

  # Get list of remote files
  files = directory.files
  truncated = files.respond_to?(:is_truncated) && files.is_truncated
  while truncated
    set = directory.files.all(marker: files.last.key)
    truncated = set.is_truncated
    files += set
  end
  keys_to_destroy = files.map(&:key)
  keys_to_invalidate = []
  etags = read_etags(files)

  # Upload all the files in the output folder to the clouds
  puts 'Uploading local files'
  FileUtils.cd(src) do
    files = Dir['**/*'].select { |f| File.file?(f) }
    files.each do |file_path|
      key = path ? File.join(path, file_path) : file_path
      upload(key, file_path, etags, keys_to_destroy, keys_to_invalidate, directory)
    end
  end

  # delete extraneous remote files
  puts 'Removing remote files'
  keys_to_destroy.each do |key|
    directory.files.get(key).destroy
  end

  # invalidate CDN objects
  if cdn_id
    puts 'Invalidating CDN distribution'
    keys_to_invalidate.concat(keys_to_destroy)
    cdn = ::Fog::CDN.new(config)
    # fog cannot mock CDN requests
    unless dry_run?
      distribution = cdn.get_distribution(cdn_id)
      # usual limit per invalidation: 1000 objects
      keys_to_invalidate.each_slice(1000) do |paths|
        cdn.post_invalidation(distribution, paths)
      end
    end
  end

  puts 'Done!'
end

Private Instance Methods

calc_local_etag(file_path) click to toggle source
# File lib/nanoc/extra/deployers/fog.rb, line 143
def calc_local_etag(file_path)
  case config[:provider]
  when 'aws'
    Digest::MD5.file(file_path).hexdigest
  end
end
error(msg) click to toggle source

Prints the given message on stderr and exits.

# File lib/nanoc/extra/deployers/fog.rb, line 151
def error(msg)
  raise RuntimeError.new(msg)
end
needs_upload?(key, file_path, etags) click to toggle source
# File lib/nanoc/extra/deployers/fog.rb, line 124
def needs_upload?(key, file_path, etags)
  remote_etag = etags[key]
  return true if remote_etag.nil?

  local_etag = calc_local_etag(file_path)
  remote_etag != local_etag
end
read_etags(files) click to toggle source
# File lib/nanoc/extra/deployers/fog.rb, line 132
def read_etags(files)
  case config[:provider]
  when 'aws'
    files.each_with_object({}) do |file, etags|
      etags[file.key] = file.etag
    end
  else
    {}
  end
end
upload(key, file_path, etags, keys_to_destroy, keys_to_invalidate, dir) click to toggle source
# File lib/nanoc/extra/deployers/fog.rb, line 112
def upload(key, file_path, etags, keys_to_destroy, keys_to_invalidate, dir)
  keys_to_destroy.delete(key)

  return unless needs_upload?(key, file_path, etags)

  dir.files.create(
    key: key,
    body: File.open(file_path),
    public: true)
  keys_to_invalidate.push(key)
end