module Buckler
Constants
- S3_BUCKET_REGIONS
Public Class Methods
Returns the discovered AWS Access Key ID Prerequisite: `Buckler.discover_aws_credentials!`
# File lib/buckler/aws.rb, line 5 def self.aws_access_key_id @aws_access_key_id end
Returns an Aws::S3::Client in the given `region` Prerequisite: `Buckler.discover_aws_credentials!`
# File lib/buckler/aws.rb, line 11 def self.connect_to_s3!(region:"us-east-1") return @s3 if @s3.present? @s3 = Aws::S3::Client.new( region: region, access_key_id: @aws_access_key_id, secret_access_key: @aws_secret_access_key, ) return @s3 end
# File lib/buckler/actions/create_bucket.rb, line 3 def self.create_bucket!(name:nil, region:nil) unless name.present? alert "No bucket name provided." alert "Usage: bucket create <bucket-name> --region <region>" exit false end region ||= "us-east-1" unless valid_region?(region) log "Invalid region “#{region}”" log "Use `bucket regions` to see a list of all S3 regions" exit false end connect_to_s3!(region:region) @bucket = Aws::S3::Bucket.new(name, client:@s3) if @bucket.exists? alert "Bucket #{@bucket.name} already exists" exit false end log "Creating bucket #{name.bucketize} on #{region}…" options = { acl: "private" } unless region.eql?("us-east-1") options[:create_bucket_configuration] = { location_constraint: region } end @bucket.create(options) @bucket.wait_until_exists log "Bucket #{name.bucketize} is how available for use ✔" exit true rescue Aws::S3::Errors::BucketAlreadyExists alert "The bucket name “#{name}” is already taken." alert "Bucket names must be unique across the entire AWS ecosystem." alert "Select a different bucket name and re-run your command." exit false end
# File lib/buckler/actions/destroy_bucket.rb, line 3 def self.destroy_bucket!(name:, confirmation:nil) connect_to_s3! @bucket = get_bucket!(name) require_confirmation!(name_required:name, confirmation:confirmation) if @bucket.versioning.status == "Enabled" log "The bucket #{name.bucketize} has versioning enabled, it cannot be deleted." log "You must disable versioning in the AWS Management Console." exit false end log "Destroying bucket #{name.bucketize}…" @bucket.delete!(max_attempts:3) log "Bucket #{name.bucketize} was destroyed ✔" exit true end
Attempts to find the AWS Access Key ID and Secret Access Key by searching the command line parameters, the environment, the .env, and Heroku in that order. The parameters are the values of –id and –secret on the command line. The program ends if credentials cannot be discovered.
# File lib/buckler/aws.rb, line 25 def self.discover_aws_credentials!(key_id:nil, key:nil) verbose "Attempting to find AWS credentials…" # Try to find keys as command line parameters, if the invoker has set them directly if key_id.present? && key.present? verbose "The Access Key ID and Secret Access Key were set as command line options ✔" @aws_access_key_id = key_id @aws_secret_access_key = key return true end # Try to find keys in the current environment, if the invoker has set them directly key_id = ENV["AWS_ACCESS_KEY_ID"] key = ENV["AWS_SECRET_ACCESS_KEY"] if key_id.present? && key.present? verbose "AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY found as environment variables ✔" @aws_access_key_id = key_id @aws_secret_access_key = key return true end # Try to find keys in a .env file in this directory Dotenv.load key_id = ENV["AWS_ACCESS_KEY_ID"] key = ENV["AWS_SECRET_ACCESS_KEY"] if key_id.present? && key.present? verbose "AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY found in the .env file ✔" @aws_access_key_id = key_id @aws_secret_access_key = key return true end # Try to find keys by asking Heroku about the project in this directory if heroku_available? key_id = heroku_config_get("AWS_ACCESS_KEY_ID") key = heroku_config_get("AWS_SECRET_ACCESS_KEY") if key_id.present? && key.present? verbose "AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY found on your Heroku application ✔" @aws_access_key_id = key_id @aws_secret_access_key = key return true end end alert "Could not discover any AWS credentials." alert "Set command line options --id and --secret" alert "Or, set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as environment variables." alert "Or, set them in a .env file in this directory." if heroku_available? alert "Or, set them on a Heroku application in this directory with `heroku config:set`." end exit false end
# File lib/buckler/actions/empty_bucket.rb, line 3 def self.empty_bucket!(name:, confirmation:nil) connect_to_s3! @bucket = get_bucket!(name) require_confirmation!(name_required:name, confirmation:confirmation) log "Deleting all objects in bucket #{name.bucketize}…" @bucket.clear! log "Bucket #{name.bucketize} is now empty ✔" exit true end
Generate an Aws::S3::Bucket for the given `name` Also checks that the bucket is real and we have access to it. The only way to truly test bucket access is to try to read an item from it. Exit with a message if there is no access. Prerequisite: `Buckler.discover_aws_credentials!`
# File lib/buckler/actions.rb, line 9 def self.get_bucket!(name) unless name.present? alert "No bucket name provided" exit false end @bucket = Aws::S3::Bucket.new(name, client:@s3) unless @bucket.exists? alert "No such bucket “#{name}”" exit false end @bucket.objects(max_keys:1).first # Tests bucket access return @bucket rescue Aws::S3::Errors::NoSuchBucket alert "No such bucket “#{name}”" exit false rescue Aws::S3::Errors::AccessDenied alert "Access denied for bucket #{name}" exit false end
True if the user has a Heroku and Ruby executable
# File lib/buckler/heroku.rb, line 14 def self.heroku_available? ruby_cmd.present? && heroku_cmd.present? end
Returns the Heroku executable on the user’s $PATH
# File lib/buckler/heroku.rb, line 9 def self.heroku_cmd @heroku_cmd ||= find_executable0("heroku") end
Fetches the given environment `variable_name` from the user’s Heroku project
# File lib/buckler/heroku.rb, line 19 def self.heroku_config_get(variable_name) command_output, command_intake = IO.pipe pid = Kernel.spawn( "#{ruby_cmd} #{heroku_cmd} config:get #{variable_name}", STDOUT => command_intake, STDERR => command_intake ) command_intake.close _, status = Process.wait2(pid) if status.exitstatus == 0 results = command_output.read.to_s.chomp verbose %{`heroku config:get #{variable_name}` returned "#{results}"} return results else verbose %{`heroku config:get #{variable_name}` returned a nonzero exit status} return false end end
# File lib/buckler/actions/list_buckets.rb, line 3 def self.list_buckets! connect_to_s3! verbose "Fetching buckets visible to #{@aws_access_key_id}…" table = [["NAME", "REGION", "VERSIONING"]] @s3.list_buckets.buckets.each do |bucket| region = @s3.get_bucket_location(bucket:bucket.name).location_constraint.presence || "us-east-1" versioning = @s3.get_bucket_versioning(bucket:bucket.name).status.presence || "Not Configured" table << [bucket.name, region, versioning] end puts_table!(table) exit true end
# File lib/buckler/actions/list_regions.rb, line 3 def self.list_regions! table = [["REGION", "NAME", nil]] S3_BUCKET_REGIONS.each do |name, human_name| case name when "us-east-1" table << [name, human_name, "Default region"] when "cn-north-1" table << [name, human_name, "Requires Chinese account"] else table << [name, human_name, nil] end end puts_table!(table) exit true end
Prints a table neatly to the screen. The given `table_array` must be an Array of Arrays of Strings. Each inner array is a single row of the table, strings are cells of the row.
# File lib/buckler/actions.rb, line 72 def self.puts_table!(table_array) column_sizes = [] table_array.first.count.times do |column_index| column_sizes << table_array.collect{ |row| row[column_index] }.collect(&:to_s).collect(&:length).max + 3 end table_array.each do |line| chart_row = "" line.each_with_index do |column, index| chart_row << column.to_s.ljust(column_sizes[index]) end log chart_row end end
Prints a warning message about irreversible changes to the screen. The user is required to confirm by typing the given `name_required` `additional_lines` are printed before the warning. If `confirmation` matches `name_required`, this method is a no-op. The program ends if the confirmation is not provided.
# File lib/buckler/actions.rb, line 44 def self.require_confirmation!(name_required:, confirmation:nil, additional_lines:[]) return true if confirmation == name_required alert "WARNING: Destructive Action" additional_lines.each do |line| log line end log "Depending on your S3 settings, this command may permanently" log "delete objects from the bucket #{name_required.bucketize}." log "To proceed, type “#{name_required}” or re-run this command with --confirm #{name_required}" print "> ".dangerize confirmation = STDIN.gets.chomp if confirmation == name_required return true else alert "Invalid confirmation “#{name_required}”, aborting" exit false end end
Returns the Ruby executable on the user’s $PATH
# File lib/buckler/heroku.rb, line 4 def self.ruby_cmd @ruby_cmd ||= find_executable0("ruby") end
# File lib/buckler/actions/sync_buckets.rb, line 3 def self.sync_buckets!(source_name:, target_name:, confirmation:nil) unless source_name.present? && target_name.present? alert "You must provide both a source bucket and a target bucket" alert "Usage: bucket sync <source-bucket> <target-bucket>" exit false end unless source_name != target_name alert "The source bucket name and target bucket name must be different" exit false end connect_to_s3! @source_bucket = get_bucket!(source_name) @target_bucket = get_bucket!(target_name) source_name = @source_bucket.name.bucketize(:pink).freeze target_name = @target_bucket.name.bucketize.freeze require_confirmation!(name_required:@target_bucket.name, confirmation:confirmation, additional_lines:[ "The contents of #{source_name} will be synced into #{target_name}.", "Objects in #{target_name} that aren’t in the source bucket will be removed.", ]) log "Syncing #{source_name} into #{target_name}…" log "Fetching bucket file lists…" @source_bucket_keys = @source_bucket.objects.collect(&:key) @target_bucket_keys = @target_bucket.objects.collect(&:key) # ------------------------------------------------------------------------- # Delete bucket differences # ------------------------------------------------------------------------- @keys_to_delete = @target_bucket_keys - @source_bucket_keys @dispatch = Buckler::ThreadDispatch.new log "Deleting unshared objects from target bucket…" @keys_to_delete.lazy.each do |key| @dispatch.queue(lambda { log "Deleting #{target_name}/#{key}" @target_bucket.object(key).delete }) end time_elapsed = @dispatch.perform_and_wait log "Unshared objects deleted from target bucket (#{time_elapsed} seconds) ✔" # ------------------------------------------------------------------------- # Sync files # ------------------------------------------------------------------------- @dispatch = Buckler::ThreadDispatch.new @source_bucket_keys.lazy.each do |object_key| @dispatch.queue(lambda { source_object = Aws::S3::Object.new(@source_bucket.name, object_key, client:@s3) target_object = Aws::S3::Object.new(@target_bucket.name, object_key, client:@s3) options = { storage_class: source_object.storage_class, metadata: source_object.metadata, content_encoding: source_object.content_encoding, content_language: source_object.content_language, content_type: source_object.content_type, cache_control: source_object.cache_control, expires: source_object.expires, } if source_object.content_disposition.present? options[:content_disposition] = ActiveSupport::Inflector.transliterate(source_object.content_disposition, "") end if source_object.content_length > 5242882 # 5 megabytes + 2 bytes options[:multipart_copy] = true options[:content_length] = source_object.content_length end target_object.copy_from(source_object, options) target_object.acl.put({ access_control_policy: { grants: source_object.acl.grants, owner: source_object.acl.owner, } }) log "Copied #{source_name} → #{target_name}/#{object_key}" }) end time_elapsed = @dispatch.perform_and_wait log "#{@source_bucket_keys.count} objects synced in #{target_name} (#{time_elapsed} seconds) ✔" exit true end
True if the given name is a valid AWS region.
# File lib/buckler/regions.rb, line 19 def self.valid_region?(name) S3_BUCKET_REGIONS.keys.include?(name) end
Returns Buckler’s version number
# File lib/buckler/version.rb, line 4 def self.version Gem::Version.new("1.0.3") end