s3sync 1.2.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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