class S3AssetDeploy::Manager

Attributes

bucket_name[R]
local_asset_collector[R]
logger[R]
remote_asset_collector[R]

Public Class Methods

new(bucket_name, s3_client_options: {}, logger: nil, local_asset_collector: nil, upload_options: {}, remove_fingerprint: nil) click to toggle source
# File lib/s3_asset_deploy/manager.rb, line 14
def initialize(bucket_name, s3_client_options: {}, logger: nil, local_asset_collector: nil, upload_options: {}, remove_fingerprint: nil)
  @bucket_name = bucket_name.to_s
  @logger = logger || Logger.new(STDOUT)

  @s3_client_options = {
    region: "us-east-1",
    logger: @logger
  }.merge(s3_client_options)
  @upload_options = upload_options

  @local_asset_collector = local_asset_collector || S3AssetDeploy::RailsLocalAssetCollector.new(remove_fingerprint: remove_fingerprint)
  @remote_asset_collector = S3AssetDeploy::RemoteAssetCollector.new(
    bucket_name,
    s3_client_options: @s3_client_options,
    remove_fingerprint: remove_fingerprint
  )
end

Public Instance Methods

clean(version_limit: 2, version_ttl: 3600, removed_ttl: 172800, dry_run: false) click to toggle source

Cleanup old assets on S3. By default it will keep the latest version, 2 older versions (version_limit) and any created within the past hour (version_ttl). When assets are removed completely, they are tagged with a removed_at timestamp and eventually deleted based on the removed_ttl.

# File lib/s3_asset_deploy/manager.rb, line 72
def clean(version_limit: 2, version_ttl: 3600, removed_ttl: 172800, dry_run: false)
  verify_no_duplicate_assets!

  version_ttl = version_ttl.to_i
  removed_ttl = removed_ttl.to_i

  log "Cleaning assets from #{bucket_name} S3 bucket. Dry run: #{dry_run}."
  s3_keys_to_delete = []

  unless local_assets_to_upload.empty?
    log "WARNING: Please upload latest asset versions to remote host before cleaning."
    return s3_keys_to_delete
  end

  removal_manifest.load
  local_asset_map = local_asset_collector.asset_map

  remote_asset_collector.grouped_assets.each do |original_path, versions|
    current_asset = local_asset_map[original_path]

    # Remove current asset version from the list
    versions_to_delete = versions.reject do |version|
      version.path == current_asset.path if current_asset
    end

    # Sort remaining versions from newest to oldest
    versions_to_delete = versions_to_delete.sort_by(&:last_modified).reverse

    # If the asset has been completely removed from our set of locally compiled assets
    # then use removed_at timestamp from manifest and removed_ttl to determine if it
    # should be deleted from remote host.
    # Otherwise, use version_ttl and version_limit to dermine whether version should be kept.
    versions_to_delete = versions_to_delete.each_with_index.drop_while do |version, index|
      if !current_asset
        if (removed_at = removal_manifest[version.path])
          removed_at = Time.parse(removed_at)
          removed_age = Time.now.utc - removed_at
          log "Determining how long ago #{version.path} was removed - removed on #{removed_at} (#{removed_age} seconds ago)."
          drop = removed_age < removed_ttl
          log "Marking removed asset #{version.path} for deletion." unless drop
          removal_manifest.delete(version.path) unless drop || dry_run
          drop
        else
          log "Adding #{version.path} to removal manifest."
          removed_at = Time.now.utc
          removal_manifest[version.path] = removed_at.iso8601 unless dry_run
          true
        end
      else
        # Keep if under age or within the version_limit
        version_age = [0, Time.now - version.last_modified].max
        drop = version_age < version_ttl || index < version_limit
        log "Marking #{version.path} for deletion. Version age: #{version_age}. Version: #{index + 1}." unless drop
        drop
      end
    end.map(&:first)

    s3_keys_to_delete += versions_to_delete.map(&:path)
  end

  unless dry_run
    delete_objects(s3_keys_to_delete)
    removal_manifest.save
  end

  remote_asset_collector.clear_cache
  s3_keys_to_delete
end
deploy(version_limit: 2, version_ttl: 3600, removed_ttl: 172800, clean: true, dry_run: false) { || ... } click to toggle source
# File lib/s3_asset_deploy/manager.rb, line 141
def deploy(version_limit: 2, version_ttl: 3600, removed_ttl: 172800, clean: true, dry_run: false)
  upload(dry_run: dry_run)
  yield if block_given?
  if clean
    clean(
      dry_run: dry_run,
      version_limit: version_limit,
      version_ttl: version_ttl,
      removed_ttl: removed_ttl
    )
  end
end
inspect() click to toggle source
# File lib/s3_asset_deploy/manager.rb, line 158
def inspect
  to_s
end
local_assets_to_upload() click to toggle source
# File lib/s3_asset_deploy/manager.rb, line 39
def local_assets_to_upload
  remote_asset_paths = remote_asset_collector.asset_paths
  local_asset_collector.assets.reject { |asset| remote_asset_paths.include?(asset.path) }
end
removal_manifest() click to toggle source
# File lib/s3_asset_deploy/manager.rb, line 32
def removal_manifest
  @removal_manifest ||= S3AssetDeploy::RemovalManifest.new(
    bucket_name,
    s3_client_options: @s3_client_options
  )
end
to_s() click to toggle source
# File lib/s3_asset_deploy/manager.rb, line 154
def to_s
  "#<#{self.class.name}:#{"0x0000%x" % (object_id << 1)} @bucket_name='#{bucket_name}'>"
end
upload(dry_run: false) click to toggle source
# File lib/s3_asset_deploy/manager.rb, line 44
def upload(dry_run: false)
  verify_no_duplicate_assets!

  removal_manifest.load
  assets_to_upload = local_assets_to_upload

  (removal_manifest.keys & local_asset_collector.asset_paths).each do |path|
    log "#{path} has been re-added. Deleting from removal manifest."
    removal_manifest.delete(path) unless dry_run
  end

  uploaded_assets = []
  assets_to_upload.each do |asset|
    next unless File.file?(asset.full_path)
    log "Uploading #{asset.path}..."
    upload_asset(asset) unless dry_run
    uploaded_assets << asset.path
  end

  removal_manifest.save unless dry_run
  remote_asset_collector.clear_cache
  uploaded_assets
end

Protected Instance Methods

delete_objects(keys = []) click to toggle source
# File lib/s3_asset_deploy/manager.rb, line 194
def delete_objects(keys = [])
  return if keys.empty?
  s3.delete_objects(
    bucket: bucket_name,
    delete: { objects: keys.map { |key| { key: key }} }
  )
end
log(msg) click to toggle source
# File lib/s3_asset_deploy/manager.rb, line 202
def log(msg)
  logger.info("#{self.class.name}: #{msg}")
end
put_object(object) click to toggle source
# File lib/s3_asset_deploy/manager.rb, line 190
def put_object(object)
  s3.put_object(object)
end
s3() click to toggle source
# File lib/s3_asset_deploy/manager.rb, line 180
def s3
  @s3 ||= Aws::S3::Client.new(@s3_client_options)
end
upload_asset(asset) click to toggle source
# File lib/s3_asset_deploy/manager.rb, line 164
def upload_asset(asset)
  file_handle = File.open(asset.full_path)

  params = {
    bucket: bucket_name,
    key: asset.path,
    body: file_handle,
    content_type: asset.mime_type,
    cache_control: "public, max-age=31536000"
  }.merge(@upload_options)

  put_object(params)
ensure
  file_handle.close
end
verify_no_duplicate_assets!() click to toggle source
# File lib/s3_asset_deploy/manager.rb, line 184
def verify_no_duplicate_assets!
  if local_asset_collector.original_asset_paths.uniq.length != local_asset_collector.asset_paths.length
    raise S3AssetDeploy::DuplicateAssetsError
  end
end