buckler 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,22 @@
1
+ module Buckler
2
+
3
+ def self.destroy_bucket!(name:, confirmation:nil)
4
+
5
+ connect_to_s3!
6
+ @bucket = get_bucket!(name)
7
+ require_confirmation!(name_required:name, confirmation:confirmation)
8
+
9
+ if @bucket.versioning.status == "Enabled"
10
+ log "The bucket #{name.bucketize} has versioning enabled, it cannot be deleted."
11
+ log "You must disable versioning in the AWS Mangement Console."
12
+ exit false
13
+ end
14
+
15
+ log "Destroying bucket #{name.bucketize}…"
16
+ @bucket.delete!(max_attempts:3)
17
+ log "Bucket #{name.bucketize} was destroyed ✔"
18
+ exit true
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,16 @@
1
+ module Buckler
2
+
3
+ def self.empty_bucket!(name:, confirmation:nil)
4
+
5
+ connect_to_s3!
6
+ @bucket = get_bucket!(name)
7
+ require_confirmation!(name_required:name, confirmation:confirmation)
8
+
9
+ log "Deleting all objects in bucket #{name.bucketize}…"
10
+ @bucket.clear!
11
+ log "Bucket #{name.bucketize} is now empty ✔"
12
+ exit true
13
+
14
+ end
15
+
16
+ end
File without changes
@@ -0,0 +1,21 @@
1
+ module Buckler
2
+
3
+ def self.list_buckets!
4
+
5
+ connect_to_s3!
6
+
7
+ verbose "Fetching buckets visible to #{@aws_access_key_id}…"
8
+ table = [["NAME", "REGION", "VERSIONING"]]
9
+
10
+ @s3.list_buckets.buckets.each do |bucket|
11
+ region = @s3.get_bucket_location(bucket:bucket.name).location_constraint.presence || "us-east-1"
12
+ versioning = @s3.get_bucket_versioning(bucket:bucket.name).status.presence || "Not Configured"
13
+ table << [bucket.name, region, versioning]
14
+ end
15
+
16
+ puts_table!(table)
17
+ exit true
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,23 @@
1
+ module Buckler
2
+
3
+ def self.list_regions!
4
+
5
+ table = [["REGION", "NAME", nil]]
6
+
7
+ S3_BUCKET_REGIONS.each do |name, human_name|
8
+ case name
9
+ when "us-east-1"
10
+ table << [name, human_name, "Default region"]
11
+ when "cn-north-1"
12
+ table << [name, human_name, "Requires chinese account"]
13
+ else
14
+ table << [name, human_name, nil]
15
+ end
16
+ end
17
+
18
+ puts_table!(table)
19
+ exit true
20
+
21
+ end
22
+
23
+ end
@@ -0,0 +1,106 @@
1
+ module Buckler
2
+
3
+ def self.sync_buckets!(source_name:, target_name:, confirmation:nil)
4
+
5
+ unless source_name.present? && target_name.present?
6
+ alert "You must provide both a source bucket and a target bucket"
7
+ alert "Usage: bucket sync <source-bucket> <target-bucket>"
8
+ exit false
9
+ end
10
+
11
+ unless source_name != target_name
12
+ alert "The source bucket name and target bucket name must be different"
13
+ exit false
14
+ end
15
+
16
+ connect_to_s3!
17
+ @source_bucket = get_bucket!(source_name)
18
+ @target_bucket = get_bucket!(target_name)
19
+
20
+ source_name = @source_bucket.name.bucketize(:pink).freeze
21
+ target_name = @target_bucket.name.bucketize.freeze
22
+
23
+ require_confirmation!(name_required:@target_bucket.name, confirmation:confirmation, additional_lines:[
24
+ "The contents of #{source_name} will be synced into #{target_name}.",
25
+ "Objects in #{target_name} that aren’t in the source bucket will be removed.",
26
+ ])
27
+
28
+ log "Syncing #{source_name} into #{target_name}…"
29
+ log "Fetching bucket file lists…"
30
+
31
+ @source_bucket_keys = @source_bucket.objects.collect(&:key)
32
+ @target_bucket_keys = @target_bucket.objects.collect(&:key)
33
+
34
+ # -------------------------------------------------------------------------
35
+ # Delete bucket differences
36
+ # -------------------------------------------------------------------------
37
+
38
+ @keys_to_delete = @target_bucket_keys - @source_bucket_keys
39
+
40
+ @dispatch = Buckler::ThreadDispatch.new
41
+
42
+ log "Deleting unshared objects from target bucket…"
43
+
44
+ @keys_to_delete.lazy.each do |key|
45
+ @dispatch.queue(lambda {
46
+ log "Deleting #{target_name}/#{key}"
47
+ @target_bucket.object(key).delete
48
+ })
49
+ end
50
+
51
+ time_elapsed = @dispatch.perform_and_wait
52
+ log "Unshared objects deleted from target bucket (#{time_elapsed} seconds) ✔"
53
+
54
+ # -------------------------------------------------------------------------
55
+ # Sync files
56
+ # -------------------------------------------------------------------------
57
+
58
+ @dispatch = Buckler::ThreadDispatch.new
59
+
60
+ @source_bucket_keys.lazy.each do |object_key|
61
+
62
+ @dispatch.queue(lambda {
63
+
64
+ source_object = Aws::S3::Object.new(@source_bucket.name, object_key, client:@s3)
65
+ target_object = Aws::S3::Object.new(@target_bucket.name, object_key, client:@s3)
66
+
67
+ options = {
68
+ storage_class: source_object.storage_class,
69
+ metadata: source_object.metadata,
70
+ content_encoding: source_object.content_encoding,
71
+ content_language: source_object.content_language,
72
+ content_type: source_object.content_type,
73
+ cache_control: source_object.cache_control,
74
+ expires: source_object.expires,
75
+ }
76
+
77
+ if source_object.content_disposition.present?
78
+ options[:content_disposition] = ActiveSupport::Inflector.transliterate(source_object.content_disposition, "")
79
+ end
80
+
81
+ if source_object.content_length > 5242882 # 5 megabytes + 2 bytes
82
+ options[:multipart_copy] = true
83
+ options[:content_length] = source_object.content_length
84
+ end
85
+
86
+ target_object.copy_from(source_object, options)
87
+ target_object.acl.put({
88
+ access_control_policy: {
89
+ grants: source_object.acl.grants,
90
+ owner: source_object.acl.owner,
91
+ }
92
+ })
93
+
94
+ log "Copied #{source_name} → #{target_name}/#{object_key}"
95
+
96
+ })
97
+
98
+ end
99
+
100
+ time_elapsed = @dispatch.perform_and_wait
101
+ log "#{@source_bucket_keys.count} objects synced in #{target_name} (#{time_elapsed} seconds) ✔"
102
+ exit true
103
+
104
+ end
105
+
106
+ end
@@ -0,0 +1,81 @@
1
+ module Buckler
2
+
3
+ # Returns the discovered AWS Access Key ID
4
+ # Prerequisite: `Buckler.discover_aws_credentials!`
5
+ def self.aws_access_key_id
6
+ @aws_access_key_id
7
+ end
8
+
9
+ # Returns an Aws::S3::Client in the given `region`
10
+ # Prerequisite: `Buckler.discover_aws_credentials!`
11
+ def self.connect_to_s3!(region:"us-east-1")
12
+ return @s3 if @s3.present?
13
+ @s3 = Aws::S3::Client.new(
14
+ region: region,
15
+ access_key_id: @aws_access_key_id,
16
+ secret_access_key: @aws_secret_access_key,
17
+ )
18
+ return @s3
19
+ end
20
+
21
+ # Attempts to find the AWS Access Key ID and Secret Access Key
22
+ # by searching the command line paramters, the environment, the .env, and Heroku in that order.
23
+ # The parameters are the values of --id and --secret on the command line.
24
+ # The program ends if credentials cannot be discovered.
25
+ def self.discover_aws_credentials!(key_id:nil, key:nil)
26
+
27
+ verbose "Attempting to find AWS credentials…"
28
+
29
+ # Try to find keys as command line parameters, if the invoker has set them directly
30
+ if key_id.present? && key.present?
31
+ verbose "The Access Key ID and Secret Access Key were set as command line options ✔"
32
+ @aws_access_key_id = key_id
33
+ @aws_secret_access_key = key
34
+ return true
35
+ end
36
+
37
+ # Try to find keys in the current environment, if the invoker has set them directly
38
+ key_id = ENV["AWS_ACCESS_KEY_ID"]
39
+ key = ENV["AWS_SECRET_ACCESS_KEY"]
40
+ if key_id.present? && key.present?
41
+ verbose "AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY found as environment variables ✔"
42
+ @aws_access_key_id = key_id
43
+ @aws_secret_access_key = key
44
+ return true
45
+ end
46
+
47
+ # Try to find keys in a .env file in this directory
48
+ Dotenv.load
49
+ key_id = ENV["AWS_ACCESS_KEY_ID"]
50
+ key = ENV["AWS_SECRET_ACCESS_KEY"]
51
+ if key_id.present? && key.present?
52
+ verbose "AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY found in the .env file ✔"
53
+ @aws_access_key_id = key_id
54
+ @aws_secret_access_key = key
55
+ return true
56
+ end
57
+
58
+ # Try to find keys by asking Heroku about the project in this directory
59
+ if heroku_available?
60
+ key_id = heroku_config_get("AWS_ACCESS_KEY_ID")
61
+ key = heroku_config_get("AWS_SECRET_ACCESS_KEY")
62
+ if key_id.present? && key.present?
63
+ verbose "AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY found on your Heroku application ✔"
64
+ @aws_access_key_id = key_id
65
+ @aws_secret_access_key = key
66
+ return true
67
+ end
68
+ end
69
+
70
+ alert "Could not discover any AWS credentials."
71
+ alert "Set command line options --id and --secret"
72
+ alert "Or, set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as environment variables."
73
+ alert "Or, set them in a .env file in this directory."
74
+ if heroku_available?
75
+ alert "Or, set them on a Heroku application in this directory with `heroku config:set`."
76
+ end
77
+ exit false
78
+
79
+ end
80
+
81
+ end
@@ -0,0 +1,271 @@
1
+ module Buckler::Commands; end
2
+
3
+ Buckler::Commands::Root = Cri::Command.define do
4
+
5
+ name "bucket"
6
+ usage "bucket <command> [options]"
7
+ summary "peform common actions on Amazon S3 buckets"
8
+ description %{
9
+ Buckler allows you perform common actions on your Amazon S3 buckets.
10
+
11
+ Run #{"bucket help <command>".bold} for more information about the commands below.
12
+
13
+ You will need a AWS Access Key ID and AWS Secret Access Key pair with
14
+ permission to mange your S3 buckets. Do not use your root keys.
15
+ Generate a new set of keys with S3 permissions only.
16
+ https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#lock-away-credentials
17
+
18
+ When you run the bucket command, Buckler tries to automatically
19
+ discover AWS credentials around your working directory.
20
+
21
+ #{"Dotenv:".bold} If the current folder has a file named .env, Buckler will look
22
+ for variables called AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in the file.
23
+ See Heroku’s documentation on this environment file format.
24
+ https://devcenter.heroku.com/articles/heroku-local#set-up-your-local-environment-variables
25
+
26
+ #{"Heroku:".bold} If the current folder has a Git repository with a Heroku remote,
27
+ Buckler will ask your Heroku application for variables
28
+ named AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY using heroku config:get
29
+
30
+ #{"Command Line:".bold} You can set the pair directly by providing the
31
+ command line options --id and --secret:
32
+
33
+ bucket list --id YOUR_AWS_ID --secret YOUR_AWS_SECRET
34
+
35
+ #{"Environment:".bold} You can set the pair directly as environment variables.
36
+
37
+ AWS_ACCESS_KEY_ID=your-id AWS_SECRET_ACCESS_KEY=your-secret bucket list
38
+ }
39
+
40
+ flag :v, :verbose, "Output additional information for all commands"
41
+ option nil, :id, "Your AWS Access Key ID", argument: :required
42
+ option nil, :secret, "Your AWS Secret Access Key", argument: :required
43
+
44
+ run do |opts, args, cmd|
45
+ puts Buckler::Commands::Root.help
46
+ end
47
+
48
+ end
49
+
50
+ Buckler::Commands::Version = Cri::Command.define do
51
+
52
+ name "version"
53
+ usage "version"
54
+ summary "Print Buckler’s version"
55
+ description %{
56
+ Prints Buckler’s version number.
57
+ }
58
+
59
+ run do |opts, args, cmd|
60
+ log Buckler::VERSION::STRING
61
+ end
62
+
63
+ end
64
+
65
+ Buckler::Commands::Regions = Cri::Command.define do
66
+
67
+ name "regions"
68
+ usage "regions"
69
+ summary "list all Amazon S3 regions"
70
+ description %{
71
+ Prints a list of all Amazon S3 bucket region names and where they are on Earth.
72
+
73
+ When creating buckets, you should select the region that is
74
+ closest to the users or computers that will be using the bucket.
75
+
76
+ Heroku applications in Heroku’s America region should use Amazon region us-east-1.
77
+
78
+ Heroku applications in Heroku’s Europe region should use Amazon region eu-west-1.
79
+ }
80
+
81
+ run do |opts, args, cmd|
82
+ Buckler.list_regions!
83
+ end
84
+
85
+ end
86
+
87
+ Buckler::Commands::List = Cri::Command.define do
88
+
89
+ name "list"
90
+ usage "list"
91
+ summary "List all buckets on your account"
92
+ description %{
93
+ Prints a list of all Amazon S3 buckets on your account.
94
+
95
+ “All” may be releative, AWS access keys can be generated without
96
+ the power to see every bucket available to the master keys.
97
+ }
98
+
99
+ run do |opts, args, cmd|
100
+ $buckler_verbose_mode = true if opts[:verbose].present?
101
+ Buckler.discover_aws_credentials!(key_id:opts[:id], key:opts[:secret])
102
+ Buckler.list_buckets!
103
+ end
104
+
105
+ end
106
+
107
+ Buckler::Commands::Create = Cri::Command.define do
108
+
109
+ name "create"
110
+ usage "create <bucket-name>"
111
+ summary "Create a new bucket"
112
+ description %{
113
+ Creates a new bucket on your Amazon S3 account with the given name.
114
+ The bucket will be empty. The bucket is created in AWS region us-east-1 unless
115
+ you specify a different region with the --region option.
116
+ Get a list of all valid regions with `Buckler regions`.
117
+
118
+ For maximum compatability, Amazon always recomends that you use
119
+ DNS and TLS-safe bucket names,
120
+ which contain only the ASCII characters a-z, 0-9, or the hypen (-)
121
+
122
+ Bucket names with dots or uppercase letters may be unable
123
+ to use certain Amazon Web Service features.
124
+
125
+ Some example good bucket names:
126
+ your-project-name,
127
+ your-project-name-staging,
128
+ and your-project-name-dev1
129
+
130
+ For more information:
131
+ http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
132
+ }
133
+
134
+ option :r, :region, "The AWS region where the bucket should be created (default: us-east-1)", argument: :required
135
+
136
+ run do |opts, args, cmd|
137
+ $buckler_verbose_mode = true if opts[:verbose].present?
138
+ Buckler.discover_aws_credentials!(key_id:opts[:id], key:opts[:secret])
139
+ Buckler.create_bucket!(name:args.first.to_s, region:opts[:region])
140
+ end
141
+
142
+ end
143
+
144
+ Buckler::Commands::Empty = Cri::Command.define do
145
+
146
+ name "empty"
147
+ usage "empty <bucket-name>"
148
+ summary "Delete all files in a bucket"
149
+ description %{
150
+ Discards all objects in the target bucket. The result will be an empty bucket.
151
+
152
+ If you have not set up versioning for your bucket,
153
+ or you have not set up Amazon Glacier backups for your bucket,
154
+ you will be unable to recover any deleted objects.
155
+
156
+ For more information:
157
+ https://docs.aws.amazon.com/AmazonS3/latest/dev/delete-or-empty-bucket.html
158
+ }
159
+
160
+ option :c, :confirm, "Confirm destructive actions without prompting", argument: :required
161
+
162
+ run do |opts, args, cmd|
163
+ $buckler_verbose_mode = true if opts[:verbose].present?
164
+ Buckler.discover_aws_credentials!(key_id:opts[:id], key:opts[:secret])
165
+ Buckler.empty_bucket!(name:args.first.to_s, confirmation:opts[:confirm])
166
+ end
167
+
168
+ end
169
+
170
+ Buckler::Commands::Destroy = Cri::Command.define do
171
+
172
+ name "destroy"
173
+ usage "destroy <bucket-name>"
174
+ summary "Delete all files in a bucket and remove it"
175
+ description %{
176
+ Discards all objects in the target bucket and then removes
177
+ the bucket from Amazon S3.
178
+
179
+ Amazon may not immediately make the bucket name
180
+ available for use again.
181
+
182
+ You CANNOT delete a bucket that has versioning enabled.
183
+ You must first disable versioning on the AWS Mangement Console.
184
+
185
+ For more information:
186
+ https://docs.aws.amazon.com/AmazonS3/latest/dev/delete-or-empty-bucket.html
187
+ https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
188
+ }
189
+
190
+ option :c, :confirm, "Confirm destructive actions without prompting", argument: :required
191
+
192
+ run do |opts, args, cmd|
193
+ $buckler_verbose_mode = true if opts[:verbose].present?
194
+ Buckler.discover_aws_credentials!(key_id:opts[:id], key:opts[:secret])
195
+ Buckler.destroy_bucket!(name:args.first.to_s, confirmation:opts[:confirm])
196
+ end
197
+
198
+ end
199
+
200
+ Buckler::Commands::Sync = Cri::Command.define do
201
+
202
+ name "sync"
203
+ usage "sync <source-bucket> <target-bucket>"
204
+ summary "Copy the contents of one bucket into another"
205
+ description %{
206
+ Copies all objects in the source bucket into the target bucket.
207
+ Objects in the target bucket that don’t exist in the source bucket will be deleted.
208
+ The end result will be two buckets with identical objects.
209
+
210
+ The following properties on each object are also transfered:
211
+ ACLs,
212
+ Amazon S3 metadata,
213
+ Amazon S3 storage class,
214
+ Cache-Control header,
215
+ Content-Type header,
216
+ Content-Encoding header,
217
+ Content-Disposition header,
218
+ Content-Language header,
219
+ and Expires header.
220
+
221
+ The source bucket will not be changed.
222
+
223
+ If you have not set up versioning for your bucket,
224
+ or you have not set up Amazon Glacier archives for your bucket,
225
+ you will be unable to recover any objects deleted from the target bucket.
226
+ }
227
+
228
+ option :c, :confirm, "Confirm destructive actions without prompting", argument: :required
229
+
230
+ run do |opts, args, cmd|
231
+ $buckler_verbose_mode = true if opts[:verbose].present?
232
+ Buckler.discover_aws_credentials!(key_id:opts[:id], key:opts[:secret])
233
+ Buckler.sync_buckets!(source_name:args.first.to_s, target_name:args.second.to_s, confirmation:opts[:confirm])
234
+ end
235
+
236
+ end
237
+
238
+ Buckler::Commands::Help = Cri::Command.define do
239
+ name "help"
240
+ usage "buckler help"
241
+ summary "Show help for a command"
242
+ description "x"
243
+ run do |opts, args, cmd|
244
+ case args.first.to_s
245
+ when "list"
246
+ puts Buckler::Commands::List.help
247
+ when "regions"
248
+ puts Buckler::Commands::Regions.help
249
+ when "create"
250
+ puts Buckler::Commands::Create.help
251
+ when "empty"
252
+ puts Buckler::Commands::Empty.help
253
+ when "destroy"
254
+ puts Buckler::Commands::Destroy.help
255
+ when "sync"
256
+ puts Buckler::Commands::Sync.help
257
+ else
258
+ puts Buckler::Commands::Root.help
259
+ end
260
+ exit true
261
+ end
262
+ end
263
+
264
+ Buckler::Commands::Root.add_command(Buckler::Commands::Version)
265
+ Buckler::Commands::Root.add_command(Buckler::Commands::Help)
266
+ Buckler::Commands::Root.add_command(Buckler::Commands::Regions)
267
+ Buckler::Commands::Root.add_command(Buckler::Commands::List)
268
+ Buckler::Commands::Root.add_command(Buckler::Commands::Create)
269
+ Buckler::Commands::Root.add_command(Buckler::Commands::Empty)
270
+ Buckler::Commands::Root.add_command(Buckler::Commands::Destroy)
271
+ Buckler::Commands::Root.add_command(Buckler::Commands::Sync)