lib/nanoc/extra/deployers/fog.rb in nanoc-4.1.6 vs lib/nanoc/extra/deployers/fog.rb in nanoc-4.2.0b1

- old
+ new

@@ -19,108 +19,169 @@ # local_root: ~/myCloud # bucket: nanoc-site-staging # # @api private class Fog < ::Nanoc::Extra::Deployer + class FogWrapper + def initialize(directory, is_dry_run) + @directory = directory + @is_dry_run = is_dry_run + end + + def upload(source_filename, destination_key) + log_effectful("uploading #{source_filename} -> #{destination_key}") + + unless dry_run? + # FIXME: source_filename file is never closed + @directory.files.create( + key: destination_key, + body: File.open(source_filename), + public: true, + ) + end + end + + def remove(keys) + keys.each do |key| + log_effectful("removing #{key}") + + unless dry_run? + @directory.files.get(key).destroy + end + end + end + + def invalidate(keys, cdn, distribution) + keys.each_slice(1000) do |keys_slice| + keys_slice.each do |key| + log_effectful("invalidating #{key}") + end + + unless dry_run? + cdn.post_invalidation(distribution, keys_slice) + end + end + end + + def dry_run? + @is_dry_run + end + + def log_effectful(s) + if @is_dry_run + puts "[dry run] #{s}" + else + puts s + end + end + end + # @see Nanoc::Extra::Deployer#run 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) + bucket = config[:bucket] || config[:bucket_name] + path = config[:path] + cdn_id = config[:cdn_id] - config.delete(:kind) + # FIXME: confusing error message + raise 'The path requires no trailing slash' if path && path[-1, 1] == '/' - # Validate params - error 'The path requires no trailing slash' if path && path[-1, 1] == '/' + connection = connect + directory = get_or_create_bucket(connection, bucket, path) + wrapper = FogWrapper.new(directory, dry_run?) - # Mock if necessary - if dry_run? - puts 'Dry run - simulation' - ::Fog.mock! - end + remote_files = list_remote_files(directory) + etags = read_etags(remote_files) - # Get connection - puts 'Connecting' - connection = ::Fog::Storage.new(config) + modified_keys, retained_keys = upload_all(src, path, etags, wrapper) - # 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? + removed_keys = remote_files.map(&:key) - retained_keys - modified_keys + wrapper.remove(removed_keys) - # Create bucket if necessary - if should_create_bucket - puts 'Creating bucket' - directory = connection.directories.create(key: bucket, prefix: path) + if cdn_id + cdn = ::Fog::CDN.new(config_for_fog) + distribution = cdn.get_distribution(cdn_id) + wrapper.invalidate(modified_keys + removed_keys, cdn, distribution) end + 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 + private + + def config_for_fog + config.dup.tap do |c| + c.delete(:bucket) + c.delete(:bucket_name) + c.delete(:path) + c.delete(:cdn_id) + c.delete(:kind) end - keys_to_destroy = files.map(&:key) - keys_to_invalidate = [] - etags = read_etags(files) + end - # 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) + def connect + ::Fog::Storage.new(config_for_fog) + end + + def get_or_create_bucket(connection, bucket, path) + directory = + begin + connection.directories.get(bucket, prefix: path) + rescue ::Excon::Errors::NotFound + nil end + + if directory + directory + elsif dry_run? + puts '[dry run] creating bucket' + else + puts 'creating bucket' + connection.directories.create(key: bucket, prefix: path) end + end - # delete extraneous remote files - puts 'Removing remote files' - keys_to_destroy.each do |key| - directory.files.get(key).destroy + def remote_key_for_local_filename(local_filename, src, path) + relative_local_filename = local_filename.sub(/\A#{src}\//, '') + + if path + File.join(path, relative_local_filename) + else + relative_local_filename end + 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 + def list_remote_files(directory) + if directory + [].tap do |files| + directory.files.each { |file| files << file } end + else + [] end + end - puts 'Done!' + def list_local_files(src) + Dir[src + '/**/*'].select { |f| File.file?(f) } end - private + def upload_all(src, path, etags, wrapper) + modified_keys = [] + retained_keys = [] - def upload(key, file_path, etags, keys_to_destroy, keys_to_invalidate, dir) - keys_to_destroy.delete(key) + local_files = list_local_files(src) + local_files.each do |file_path| + key = remote_key_for_local_filename(file_path, src, path) + if needs_upload?(key, file_path, etags) + wrapper.upload(file_path, key) + modified_keys.push(key) + else + retained_keys.push(key) + end + end - 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) + [modified_keys, retained_keys] end def needs_upload?(key, file_path, etags) remote_etag = etags[key] return true if remote_etag.nil? @@ -143,13 +204,8 @@ def calc_local_etag(file_path) case config[:provider] when 'aws' Digest::MD5.file(file_path).hexdigest end - end - - # Prints the given message on stderr and exits. - def error(msg) - raise RuntimeError.new(msg) end end end