s3sync 1.2.5 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
@@ -0,0 +1,475 @@
1
+ # s3sync - Tool belt for managing your S3 buckets
2
+ #
3
+ # The MIT License (MIT)
4
+ #
5
+ # Copyright (c) 2013 Lincoln de Sousa <lincoln@clarete.li>
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"), to deal
9
+ # in the Software without restriction, including without limitation the rights
10
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ # copies of the Software, and to permit persons to whom the Software is
12
+ # furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included in
15
+ # all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ # THE SOFTWARE.
24
+
25
+ require 's3sync/version'
26
+ require 's3sync/exceptions'
27
+ require 's3sync/sync'
28
+ require 'aws/s3'
29
+ require 'cmdparse'
30
+
31
+
32
+ module S3Sync
33
+ module CLI
34
+
35
+ AVAILABLE_ACLS = [:public_read, :public_read_write, :private]
36
+
37
+ AVAILABLE_METHODS = ['read', 'get', 'put', 'write', 'delete']
38
+
39
+ class BaseCmd < CmdParse::Command
40
+
41
+ @has_prefix = false
42
+
43
+ def has_options?
44
+ not options.instance_variables.empty?
45
+ end
46
+
47
+ def has_prefix?
48
+ @has_prefix
49
+ end
50
+
51
+ def usage
52
+ u = []
53
+ u << "Usage: #{File.basename commandparser.program_name} #{name} "
54
+ u << "[options] " if has_options?
55
+ u << "bucket" if has_args?
56
+
57
+ if has_prefix? == 'required'
58
+ u << ':prefix'
59
+ elsif has_prefix?
60
+ u << "[:prefix]"
61
+ end
62
+
63
+ u.join ''
64
+ end
65
+
66
+ protected
67
+
68
+ def parse_acl(opt)
69
+ @acl = nil
70
+ opt.on("-a", "--acl=ACL", "Options: #{AVAILABLE_ACLS.join ', '}") {|acl|
71
+ @acl = acl.to_sym
72
+ }
73
+ end
74
+ end
75
+
76
+ class ListBuckets < BaseCmd
77
+ def initialize
78
+ super 'listbuckets', false, false, false
79
+
80
+ @short_desc = "List all available buckets for your user"
81
+ end
82
+
83
+ def run s3, bucket, key, file, args
84
+ s3.buckets.each do |bkt|
85
+ puts "#{bkt.name}"
86
+ end
87
+ end
88
+ end
89
+
90
+ class CreateBucket < BaseCmd
91
+ attr_accessor :acl
92
+
93
+ def initialize
94
+ super 'createbucket', false, false
95
+
96
+ @short_desc = "Create a new bucket under your user account"
97
+
98
+ self.options = CmdParse::OptionParserWrapper.new do |opt|
99
+ parse_acl(opt)
100
+ end
101
+ end
102
+
103
+ def run s3, bucket, key, file, args
104
+ raise WrongUsage.new(nil, "You need to inform a bucket") if not bucket
105
+
106
+ begin
107
+ params = {}
108
+ if @acl
109
+ raise WrongUsage.new(nil, "Invalid ACL `#{@acl}'. Should be any of #{AVAILABLE_ACLS.join ', '}") if not AVAILABLE_ACLS.include? @acl
110
+ params.merge!({:acl => @acl})
111
+ end
112
+
113
+ s3.buckets.create bucket, params
114
+ rescue AWS::S3::Errors::BucketAlreadyExists
115
+ raise FailureFeedback.new("Bucket `#{bucket}' already exists")
116
+ end
117
+ end
118
+ end
119
+
120
+ class DeleteBucket < BaseCmd
121
+ attr_accessor :force
122
+
123
+ def initialize
124
+ super 'deletebucket', false, false
125
+
126
+ @short_desc = "Remove a bucket from your account"
127
+
128
+ @force = false
129
+
130
+ self.options = CmdParse::OptionParserWrapper.new do |opt|
131
+ opt.on("-f", "--force", "Clean the bucket then deletes it") {|f|
132
+ @force = f
133
+ }
134
+ end
135
+ end
136
+
137
+ def run s3, bucket, key, file, args
138
+ raise WrongUsage.new(nil, "You need to inform a bucket") if not bucket
139
+
140
+ # Getting the bucket
141
+ bucket_obj = s3.buckets[bucket]
142
+
143
+ # Do not kill buckets with content unless explicitly asked
144
+ if not @force and bucket_obj.objects.count > 0
145
+ raise FailureFeedback.new("Cowardly refusing to remove non-empty bucket `#{bucket}'. Try with -f.")
146
+ end
147
+
148
+ bucket_obj.delete!
149
+ end
150
+ end
151
+
152
+ class List < BaseCmd
153
+ attr_accessor :max_entries
154
+
155
+ def initialize
156
+ super 'list', false, false
157
+
158
+ @short_desc = "List items filed under a given bucket"
159
+
160
+ @max_entries = 0
161
+
162
+ @delimiter = "\t"
163
+
164
+ @has_prefix = true
165
+
166
+ self.options = CmdParse::OptionParserWrapper.new do |opt|
167
+ opt.on("-m", "--max-entries=NUM", "Limit the number of entries to output") {|m|
168
+ @max_entries = m
169
+ }
170
+
171
+ opt.on("-d", "--delimiter=D", "Charactere used to separate columns") {|d|
172
+ @delimiter = d
173
+ }
174
+ end
175
+ end
176
+
177
+ def run s3, bucket, key, file, args
178
+ raise WrongUsage.new(nil, "You need to inform a bucket") if not bucket
179
+
180
+ collection = s3.buckets[bucket].objects.with_prefix(key || "")
181
+
182
+ if @max_entries > 0
183
+ collection = collection.page(:per_page => @max_entries)
184
+ end
185
+
186
+ collection.each {|object|
187
+ o = []
188
+ o << object.key
189
+ o << @delimiter
190
+ o << object.content_length
191
+ o << @delimiter
192
+ o << object.last_modified
193
+ puts o.join
194
+ }
195
+ end
196
+ end
197
+
198
+ class Delete < BaseCmd
199
+ def initialize
200
+ super 'delete', false, false
201
+
202
+ @short_desc = "Delete a key from a bucket"
203
+
204
+ @has_prefix = 'required'
205
+ end
206
+
207
+ def run s3, bucket, key, file, args
208
+ raise WrongUsage.new(nil, "You need to inform a bucket") if not bucket
209
+ raise WrongUsage.new(nil, "You need to inform a key") if not key
210
+ s3.buckets[bucket].objects[key].delete
211
+ end
212
+ end
213
+
214
+ class Url < BaseCmd
215
+ attr_accessor :method
216
+ attr_accessor :secure
217
+
218
+ def initialize
219
+ super 'url', false, false
220
+
221
+ @short_desc = "Generates public urls or authenticated endpoints for the object"
222
+ @description = "Notice that --method and --public are mutually exclusive"
223
+ @method = false
224
+ @public = false
225
+ @secure = true
226
+ @expires_in = false
227
+ @has_prefix = 'required'
228
+
229
+ self.options = CmdParse::OptionParserWrapper.new do |opt|
230
+ opt.on("-m", "--method=METHOD", "Options: #{AVAILABLE_METHODS.join ', '}") {|m|
231
+ @method = m
232
+ }
233
+
234
+ opt.on("-p", "--public", "Generates a public (not authenticated) URL for the object") {|p|
235
+ @public = p
236
+ }
237
+
238
+ opt.on("--no-ssl", "Generate an HTTP link, no HTTPS") {
239
+ @secure = false
240
+ }
241
+
242
+ opt.on("--expires-in=EXPR", "How long the link takes to expire. Format: <# of seconds> | [#d|#h|#m|#s]") { |expr|
243
+ val = 0
244
+ expr.scan(/(\d+\w)/) do |track|
245
+ _, num, what = /(\d+)(\w)/.match(track[0]).to_a
246
+ num = num.to_i
247
+
248
+ case what
249
+ when "d"; val += num * 86400
250
+ when "h"; val += num * 3600
251
+ when "m"; val += num * 60
252
+ when "s"; val += num
253
+ end
254
+ end
255
+ @expires_in = val > 0 ? val : expr.to_i
256
+ }
257
+ end
258
+ end
259
+
260
+ def run s3, bucket, key, file, args
261
+ raise WrongUsage.new(nil, "You need to inform a bucket") if not bucket
262
+ raise WrongUsage.new(nil, "You need to inform a key") if not key
263
+ raise WrongUsage.new(nil, "Params --method and --public are mutually exclusive") if (@method and @public)
264
+ if not AVAILABLE_METHODS.include? method = @method || 'read'
265
+ raise WrongUsage.new(nil, "Unknown method #{method}")
266
+ end
267
+
268
+ opts = {}
269
+ opts.merge!({:secure => @secure})
270
+
271
+ if @public
272
+ puts s3.buckets[bucket].objects[key].public_url(opts).to_s
273
+ else
274
+ opts.merge!({:expires => @expires_in}) if @expires_in
275
+ puts s3.buckets[bucket].objects[key].url_for(method.to_sym, opts).to_s
276
+ end
277
+ end
278
+ end
279
+
280
+ class Put < BaseCmd
281
+ def initialize
282
+ super 'put', false, false
283
+
284
+ @short_desc = 'Upload a file to a bucket under a certain prefix'
285
+ @has_prefix = true
286
+ end
287
+
288
+ def usage
289
+ "#{super} path/to/local/destination"
290
+ end
291
+
292
+ def run s3, bucket, key, file, args
293
+ raise WrongUsage.new(nil, "You need to inform a bucket") if not bucket
294
+ raise WrongUsage.new(nil, "You need to inform a file") if not file
295
+
296
+ name = S3Sync.safe_join [key, File.basename(file)]
297
+ s3.buckets[bucket].objects[name].write Pathname.new(file)
298
+ end
299
+ end
300
+
301
+ class Get < BaseCmd
302
+ def initialize
303
+ super 'get', false, false
304
+ @short_desc = "Retrieve an object and save to the specified file"
305
+ @has_prefix = 'required'
306
+ end
307
+
308
+ def usage
309
+ "#{super} path/to/local/destination"
310
+ end
311
+
312
+ def run s3, bucket, key, file, args
313
+ raise WrongUsage.new(nil, "You need to inform a bucket") if not bucket
314
+ raise WrongUsage.new(nil, "You need to inform a key") if not key
315
+ raise WrongUsage.new(nil, "You need to inform a file") if not file
316
+
317
+ # Saving the content to be downloaded to the current directory if the
318
+ # destination is a directory
319
+ path = File.absolute_path file
320
+ path = S3Sync.safe_join [path, File.basename(key)] if File.directory? path
321
+ File.open(path, 'wb') do |f|
322
+ s3.buckets[bucket].objects[key].read do |chunk| f.write(chunk) end
323
+ end
324
+ end
325
+ end
326
+
327
+ class Sync < BaseCmd
328
+ attr_accessor :s3
329
+ attr_accessor :exclude
330
+ attr_accessor :keep
331
+ attr_accessor :dry_run
332
+ attr_accessor :verbose
333
+ attr_accessor :acl
334
+
335
+ def initialize
336
+ super 'sync', false, false
337
+
338
+ @short_desc = "Synchronize an S3 and a local folder"
339
+ @s3 = nil
340
+ @exclude = nil
341
+ @keep = false
342
+ @dry_run = false
343
+ @verbose = false
344
+
345
+ self.options = CmdParse::OptionParserWrapper.new do |opt|
346
+ opt.on("-x EXPR", "--exclude=EXPR", "Skip copying files that matches this pattern. (Ruby REs)") {|v|
347
+ @exclude = v
348
+ }
349
+
350
+ opt.on("-k", "--keep", "Keep files even if they don't exist in source") {
351
+ @keep = true
352
+ }
353
+
354
+ parse_acl(opt)
355
+
356
+ opt.on("-d", "--dry-run", "Do not download or exclude anything, just show what was planned. Implies `verbose`.") {
357
+ @dry_run = true
358
+ @verbose = true
359
+ }
360
+
361
+ opt.on("-v", "--verbose", "Show file names") {
362
+ @verbose = true
363
+ }
364
+ end
365
+ end
366
+
367
+ def usage
368
+ "Usage: #{File.basename commandparser.program_name} #{name} source destination"
369
+ end
370
+
371
+ def description
372
+ @description =<<END.strip
373
+
374
+ Where `source' and `description' might be either local or remote
375
+ addresses. A local address is simply a path in your local file
376
+ system. e.g:
377
+
378
+ /tmp/notes.txt
379
+
380
+ A remote address is a combination of the `bucket` name and
381
+ an optional `prefix`:
382
+
383
+ disc.company.com:reports/2013/08/30.html
384
+
385
+ So, a full example would be something like this
386
+
387
+ $ #{File.basename commandparser.program_name} sync Work/reports disc.company.com:reports/2013/08
388
+
389
+ The above line will update the remote folder `reports/2013/08` with the
390
+ contents of the local folder `Work/reports`.
391
+ END
392
+ end
393
+
394
+ def run s3, bucket, key, file, args
395
+ @s3 = s3
396
+ cmd = SyncCommand.new self, *args
397
+ cmd.run
398
+ end
399
+ end
400
+
401
+ def run conf
402
+ cmd = CmdParse::CommandParser.new true
403
+ cmd.program_name = File.basename $0
404
+ cmd.program_version = S3Sync::VERSION
405
+
406
+ cmd.options = CmdParse::OptionParserWrapper.new do |opt|
407
+ opt.separator "Global options:"
408
+ end
409
+
410
+ cmd.main_command.short_desc = 'Tool belt for managing your S3 buckets'
411
+ cmd.main_command.description =<<END.strip
412
+ S3Sync provides a list of commands that will allow you to manage your content
413
+ stored in S3 buckets. To learn about each feature, please use the `help`
414
+ command:
415
+
416
+ $ #{File.basename $0} help sync"
417
+ END
418
+ # Commands used more often
419
+ cmd.add_command List.new
420
+ cmd.add_command Delete.new
421
+ cmd.add_command Url.new
422
+ cmd.add_command Put.new
423
+ cmd.add_command Get.new
424
+ cmd.add_command Sync.new
425
+
426
+ # Bucket related options
427
+ cmd.add_command ListBuckets.new
428
+ cmd.add_command CreateBucket.new
429
+ cmd.add_command DeleteBucket.new
430
+
431
+ # Built-in commands
432
+ cmd.add_command CmdParse::HelpCommand.new
433
+ cmd.add_command CmdParse::VersionCommand.new
434
+
435
+ # Defining the `execute` method as a closure, so we can forward the
436
+ # arguments needed to run the instance of the chosen command.
437
+ CmdParse::Command.class_eval do
438
+ define_method :execute, lambda { |args|
439
+
440
+ # Connecting to amazon
441
+ s3 = AWS::S3.new(
442
+ :access_key_id => conf[:AWS_ACCESS_KEY_ID],
443
+ :secret_access_key => conf[:AWS_SECRET_ACCESS_KEY],
444
+ )
445
+
446
+ # From the command line
447
+ key, file = args
448
+
449
+ # Parsing the bucket name
450
+ bucket = nil
451
+ bucket, key = key.split(':') if key
452
+
453
+ # Running our custom method inside of the command class, taking care
454
+ # of the common errors here, saving duplications in each command;
455
+ begin
456
+ run s3, bucket, key, file, args
457
+ rescue AWS::S3::Errors::AccessDenied
458
+ raise FailureFeedback.new("Access Denied")
459
+ rescue AWS::S3::Errors::NoSuchBucket
460
+ raise FailureFeedback.new("There's no bucket named `#{bucket}'")
461
+ rescue AWS::S3::Errors::NoSuchKey
462
+ raise FailureFeedback.new("There's no key named `#{key}' in the bucket `#{bucket}'")
463
+ rescue AWS::S3::Errors::Base => exc
464
+ raise FailureFeedback.new("Error: `#{exc.message}'")
465
+ end
466
+ }
467
+ end
468
+
469
+ cmd.parse
470
+ end
471
+
472
+ module_function :run
473
+
474
+ end
475
+ end