s3sync 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
data/bin/s3sync ADDED
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # s3sync - Tool belt for managing your S3 buckets
4
+ #
5
+ # The MIT License (MIT)
6
+ #
7
+ # Copyright (c) 2013 Lincoln de Sousa <lincoln@clarete.li>
8
+ #
9
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ # of this software and associated documentation files (the "Software"), to deal
11
+ # in the Software without restriction, including without limitation the rights
12
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ # copies of the Software, and to permit persons to whom the Software is
14
+ # furnished to do so, subject to the following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be included in
17
+ # all copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25
+ # THE SOFTWARE.
26
+
27
+ $:.unshift(File.dirname(__FILE__) + '/../lib') unless $:.include?(File.dirname(__FILE__) + '/../lib')
28
+
29
+ require "s3sync/exceptions"
30
+ require "s3sync/config"
31
+ require "s3sync/cli"
32
+
33
+ conf = S3Sync::Config.new
34
+
35
+ # Time to load config and see if we've got everything we need to cook our salad
36
+ begin
37
+ conf.read
38
+ rescue S3Sync::NoConfigFound => exc
39
+ # We can't proceed without having those two vars set
40
+ $stderr.puts "You didn't set up the following environment variables:"
41
+ $stderr.puts
42
+ exc.missing_vars.each {|var| $stderr.puts " * #{var}"}
43
+ $stderr.puts
44
+
45
+ $stderr.puts "I tried to load a config file from the following paths:"
46
+ $stderr.puts
47
+ exc.paths_checked.each {|path| $stderr.puts " * #{path}"}
48
+ $stderr.puts
49
+
50
+ $stderr.puts "You could try to set the `S3SYNC_PATH' environment variable"
51
+ $stderr.puts "pointing to a file to be loaded as your config file or just"
52
+ $stderr.puts "export those variables to your environment like this:"
53
+ $stderr.puts
54
+ exc.missing_vars.each {|var|
55
+ $stderr.puts " $ export #{var}=<value-provided-by-amazon>"
56
+ }
57
+ $stderr.puts
58
+ $stderr.puts "Learn how to do that here: https://github.com/clarete/s3sync"
59
+ exit
60
+ end
61
+
62
+ # Step aside, the star of this show is here. Let's try to create the
63
+ # environment to run the requested command. And feed the user back if
64
+ # information needed was not enough
65
+ begin
66
+ S3Sync::CLI::run conf
67
+ rescue S3Sync::FailureFeedback => exc
68
+ $stderr.puts exc.message
69
+ exit 1
70
+ rescue S3Sync::WrongUsage => exc
71
+ $stderr.puts "Error:\n #{exc.msg}\n" if exc.msg
72
+ exit exc.error_code
73
+ end
data/lib/s3sync.rb ADDED
@@ -0,0 +1,2 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
data/lib/s3sync/cli.rb ADDED
@@ -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
@@ -0,0 +1,98 @@
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
+ # Part of this software was inspired by the original s3sync, so here's their
26
+ # copyright notice:
27
+
28
+ # This software code is made available "AS IS" without warranties of any
29
+ # kind. You may copy, display, modify and redistribute the software
30
+ # code either by itself or as incorporated into your code; provided that
31
+ # you do not remove any proprietary notices. Your use of this software
32
+ # code is at your own risk and you waive any claim against the author
33
+ # with respect to your use of this software code.
34
+ # (c) 2007 alastair brunton
35
+ #
36
+ # modified to search out the yaml in several places, thanks wkharold.
37
+
38
+ require 'yaml'
39
+ require 's3sync/exceptions'
40
+
41
+
42
+ module S3Sync
43
+
44
+ class Config < Hash
45
+
46
+ REQUIRED_VARS = [:AWS_ACCESS_KEY_ID, :AWS_SECRET_ACCESS_KEY]
47
+
48
+ CONFIG_PATHS = ["#{ENV['S3SYNC_PATH']}", "#{ENV['HOME']}/.s3sync.yml", "/etc/s3sync.yml"]
49
+
50
+ def read_from_file
51
+ paths_checked = []
52
+
53
+ CONFIG_PATHS.each do |path|
54
+
55
+ # Filtering some garbage
56
+ next if path.nil? or path.strip.empty?
57
+
58
+ # Feeding the user feedback in case of failure
59
+ paths_checked << path
60
+
61
+ # Time for the dirty work, let's parse the config file and feed our
62
+ # internal hash
63
+ if File.exists? path
64
+ config = YAML.load_file path
65
+ config.each_pair do |key, value|
66
+ self[key.upcase.to_sym] = value
67
+ end
68
+ return
69
+ end
70
+ end
71
+
72
+ return paths_checked
73
+ end
74
+
75
+ def read_from_env
76
+ REQUIRED_VARS.each do |v|
77
+ self[v] = ENV[v.to_s] unless ENV[v.to_s].nil?
78
+ end
79
+ end
80
+
81
+ def read
82
+ # Reading from file and then trying from env
83
+ paths_checked = read_from_file
84
+ read_from_env
85
+
86
+ # Checking which variables we have
87
+ not_found = []
88
+
89
+ REQUIRED_VARS.each {|v|
90
+ not_found << v if self[v].nil?
91
+ }
92
+
93
+ # Cleaning possibly empty env var from CONFIG_PATH
94
+ paths = (paths_checked || CONFIG_PATHS).select {|e| !e.empty?}
95
+ raise NoConfigFound.new(not_found, paths) if not_found.count > 0
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,55 @@
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
+ module S3Sync
26
+
27
+ class SyncException < StandardError
28
+ end
29
+
30
+ class NoConfigFound < SyncException
31
+
32
+ attr_accessor :missing_vars
33
+ attr_accessor :paths_checked
34
+
35
+ def initialize missing_vars, paths_checked
36
+ @missing_vars = missing_vars
37
+ @paths_checked = paths_checked
38
+ end
39
+ end
40
+
41
+ class WrongUsage < SyncException
42
+
43
+ attr_accessor :error_code
44
+ attr_accessor :msg
45
+
46
+ def initialize(error_code, msg)
47
+ @error_code = error_code || 1
48
+ @msg = msg
49
+ end
50
+ end
51
+
52
+ class FailureFeedback < SyncException
53
+ end
54
+
55
+ end
@@ -0,0 +1,366 @@
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
+ # Part of this software was inspired by the original s3sync, so here's their
26
+ # copyright notice:
27
+
28
+ # (c) 2007 s3sync.net
29
+ #
30
+ # This software code is made available "AS IS" without warranties of any
31
+ # kind. You may copy, display, modify and redistribute the software
32
+ # code either by itself or as incorporated into your code; provided that
33
+ # you do not remove any proprietary notices. Your use of this software
34
+ # code is at your own risk and you waive any claim against the author
35
+ # with respect to your use of this software code.
36
+
37
+ require 'find'
38
+ require 'fileutils'
39
+ require 's3sync/util'
40
+
41
+ module S3Sync
42
+
43
+ class Location
44
+ attr_accessor :path
45
+ attr_accessor :bucket
46
+
47
+ def initialize path, bucket=nil
48
+ raise RuntimeError if path.nil?
49
+ @path = path
50
+ @bucket = bucket || nil
51
+ end
52
+
53
+ def to_s
54
+ out = []
55
+ out << @bucket unless @bucket.nil?
56
+ out << @path
57
+ out.join ':'
58
+ end
59
+
60
+ def local?
61
+ @bucket.nil?
62
+ end
63
+
64
+ def == other
65
+ @path == other.path and @bucket == other.bucket
66
+ end
67
+
68
+ alias eql? ==
69
+ end
70
+
71
+ class Node
72
+ include Comparable
73
+
74
+ attr_accessor :base
75
+ attr_accessor :path
76
+ attr_accessor :size
77
+
78
+ def initialize base, path, size
79
+ @base = base
80
+ @path = path
81
+ @size = size
82
+ end
83
+
84
+ def full
85
+ S3Sync.safe_join [@base, @path]
86
+ end
87
+
88
+ def == other
89
+ full == other.full and @size == other.size
90
+ end
91
+
92
+ def <=> other
93
+ if self.size < other.size
94
+ -1
95
+ elsif self.size > other.size
96
+ 1
97
+ else
98
+ 0
99
+ end
100
+ end
101
+
102
+ alias eql? ==
103
+ end
104
+
105
+ class LocalDirectory
106
+ attr_accessor :source
107
+
108
+ def initialize source
109
+ @source = source
110
+ end
111
+
112
+ def list_files
113
+ nodes = {}
114
+ Find.find(@source) do |file|
115
+ st = File.stat file
116
+
117
+ # We don't support following symlinks for now, we don't need to follow
118
+ # folders and I don't think we care about any other thing, right?
119
+ next unless st.file?
120
+
121
+ # Well, we're kinda out of options right now, I'll just yell!
122
+ if not st.readable?
123
+ $stderr.puts "WARNING: Skipping unreadable file #{file}"
124
+ next
125
+ end
126
+
127
+ # We only need the relative path here
128
+ file_name = file.gsub(/^#{@source}\/?/, '').squeeze('/')
129
+ node = Node.new(@source.squeeze('/'), file_name, st.size)
130
+ nodes[node.path] = node
131
+ end
132
+
133
+ return nodes
134
+ end
135
+ end
136
+
137
+ class SyncCommand
138
+
139
+ def self.cmp hash1, hash2
140
+ same, to_add_to_2 = [], []
141
+
142
+ hash1.each do |key, value|
143
+ value2 = hash2.delete key
144
+ if value2.nil?
145
+ to_add_to_2 << value
146
+ elsif value2.size == value.size
147
+ same << value
148
+ else
149
+ to_add_to_2 << value
150
+ end
151
+ end
152
+
153
+ to_remove_from_2 = hash2.values
154
+
155
+ [same, to_add_to_2, to_remove_from_2]
156
+ end
157
+
158
+ def initialize args, source, destination
159
+ @args = args
160
+ @source = source
161
+ @destination = destination
162
+ end
163
+
164
+ def run
165
+ # Reading the source and destination using our helper method
166
+ if (source, destination, bucket = self.class.parse_params [@source, @destination]).nil?
167
+ raise WrongUsage.new(nil, 'Need a source and a destination')
168
+ end
169
+
170
+ # Getting the trees
171
+ source_tree, destination_tree = read_trees source, destination
172
+
173
+ # Getting the list of resources to be exchanged between the two peers
174
+ _, to_add, to_remove = self.class.cmp source_tree, destination_tree
175
+
176
+ # Removing the items matching the exclude pattern if requested
177
+ to_add.select! { |e|
178
+ begin
179
+ (e.path =~ /#{@args.exclude}/).nil?
180
+ rescue RegexpError => exc
181
+ raise WrongUsage.new nil, exc.message
182
+ end
183
+ } if @args.exclude
184
+
185
+ # Calling the methods that perform the actual IO
186
+ if source.local?
187
+ upload_files destination, to_add
188
+ remove_files destination, to_remove unless @args.keep
189
+ else
190
+ download_files destination, source, to_add
191
+ remove_local_files destination, source, to_remove unless @args.keep
192
+ end
193
+ end
194
+
195
+ def self.parse_params args
196
+ # Reading the arbitrary parameters from the command line and getting
197
+ # modifiable copies to parse
198
+ source, destination = args; return nil if source.nil? or destination.nil?
199
+
200
+ # Sync from one s3 to another is currently not supported
201
+ if remote_prefix? source and remote_prefix? destination
202
+ raise WrongUsage.new(nil, 'Both arguments can\'t be on S3')
203
+ end
204
+
205
+ # C'mon, there's rsync out there
206
+ if !remote_prefix? source and !remote_prefix? destination
207
+ raise WrongUsage.new(nil, 'One argument must be on S3')
208
+ end
209
+
210
+ source, destination = process_destination source, destination
211
+ return [Location.new(*source), Location.new(*destination)]
212
+ end
213
+
214
+ def self.remote_prefix?(prefix)
215
+ # allow for dos-like things e.g. C:\ to be treated as local even with
216
+ # colon.
217
+ prefix.include? ':' and not prefix.match '^[A-Za-z]:[\\\\/]'
218
+ end
219
+
220
+ def self.process_file_destination source, destination, file=""
221
+ if not file.empty?
222
+ sub = (remote_prefix? source) ? source.split(":")[1] : source
223
+ file = file.gsub(/^#{sub}/, '')
224
+ end
225
+
226
+ # no slash on end of source means we need to append the last src dir to
227
+ # dst prefix testing for empty isn't good enough here.. needs to be
228
+ # "empty apart from potentially having 'bucket:'"
229
+ if source =~ %r{/$}
230
+ File.join [destination, file]
231
+ else
232
+ if remote_prefix? source
233
+ _, name = source.split ":"
234
+ File.join [destination, File.basename(name || ""), file]
235
+ else
236
+ source = /^\/?(.*)/.match(source)[1]
237
+
238
+ # Corner case: the root of the remote path is empty, we don't want to
239
+ # add an unnecessary slash here.
240
+ if destination.end_with? ':'
241
+ File.join [destination + source, file]
242
+ else
243
+ File.join [destination, source, file]
244
+ end
245
+ end
246
+ end
247
+ end
248
+
249
+ def self.process_destination source, destination
250
+ source, destination = source.dup, destination.dup
251
+
252
+ # don't repeat slashes
253
+ source.squeeze! '/'
254
+ destination.squeeze! '/'
255
+
256
+ # Making sure that local paths won't break our stuff later
257
+ source.gsub!(/^\.\//, '')
258
+ destination.gsub!(/^\.\//, '')
259
+
260
+ # Parsing the final destination
261
+ destination = process_file_destination source, destination, ""
262
+
263
+ # here's where we find out what direction we're going
264
+ source_is_s3 = remote_prefix? source
265
+
266
+ # canonicalize the S3 stuff
267
+ remote_prefix = source_is_s3 ? source : destination
268
+ bucket, remote_prefix = remote_prefix.split ":"
269
+ remote_prefix ||= ""
270
+
271
+ # Just making sure we preserve the direction
272
+ if source_is_s3
273
+ [[remote_prefix, bucket], destination]
274
+ else
275
+ [source, [remote_prefix, bucket]]
276
+ end
277
+ end
278
+
279
+ def read_tree_remote location
280
+ dir = location.path
281
+ dir += '/' if not dir.empty? and not dir.end_with?('/')
282
+
283
+ nodes = {}
284
+ @args.s3.buckets[location.bucket].objects.with_prefix(dir || "").to_a.collect do |obj|
285
+ node = Node.new(location.path, obj.key, obj.content_length)
286
+ nodes[node.path] = node
287
+ end
288
+ return nodes
289
+ end
290
+
291
+ def read_trees source, destination
292
+ if source.local?
293
+ source_tree = LocalDirectory.new(source.path).list_files
294
+ destination_tree = read_tree_remote destination
295
+ else
296
+ source_tree = read_tree_remote source
297
+ destination_tree = LocalDirectory.new(destination.path).list_files
298
+ end
299
+
300
+ [source_tree, destination_tree]
301
+ end
302
+
303
+ def upload_files remote, list
304
+ list.each do |e|
305
+ if @args.verbose
306
+ puts " + #{e.full} => #{remote}#{e.path}"
307
+ end
308
+
309
+ unless @args.dry_run
310
+ remote_path = "#{remote.path}#{e.path}"
311
+ @args.s3.buckets[remote.bucket].objects[remote_path].write Pathname.new(e.full), :acl => @args.acl
312
+ end
313
+ end
314
+ end
315
+
316
+ def remove_files remote, list
317
+ if @args.verbose
318
+ list.each {|e|
319
+ puts " - #{remote}#{e.path}"
320
+ }
321
+ end
322
+
323
+ unless @args.dry_run
324
+ @args.s3.buckets[remote.bucket].objects.delete_if { |obj| list.map(&:path).include? obj.key }
325
+ end
326
+ end
327
+
328
+ def download_files destination, source, list
329
+ list.each {|e|
330
+ path = File.join destination.path, e.path
331
+
332
+ if @args.verbose
333
+ puts " + #{source}#{e.path} => #{path}"
334
+ end
335
+
336
+ unless @args.dry_run
337
+ obj = @args.s3.buckets[source.bucket].objects[e.path]
338
+
339
+ # Making sure this new file will have a safe shelter
340
+ FileUtils.mkdir_p File.dirname(path)
341
+
342
+ # Downloading and saving the files
343
+ File.open(path, 'wb') do |file|
344
+ obj.read do |chunk|
345
+ file.write chunk
346
+ end
347
+ end
348
+ end
349
+ }
350
+ end
351
+
352
+ def remove_local_files destination, source, list
353
+ list.each {|e|
354
+ path = File.join destination.path, e.path
355
+
356
+ if @args.verbose
357
+ puts " * #{e.path} => #{path}"
358
+ end
359
+
360
+ unless @args.dry_run
361
+ FileUtils.rm_rf path
362
+ end
363
+ }
364
+ end
365
+ end
366
+ end
@@ -0,0 +1,29 @@
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
+ module S3Sync
26
+ def S3Sync.safe_join(parts)
27
+ File.join(*(parts.select {|v| !v.nil? && !v.empty? }))
28
+ end
29
+ end
@@ -0,0 +1,27 @@
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
+ module S3Sync
26
+ VERSION = "0.3.2"
27
+ end
metadata ADDED
@@ -0,0 +1,190 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: s3sync
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Lincoln de Sousa
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-09-25 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: aws-sdk
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: cmdparse
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: debugger
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: simplecov
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: bundler
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: '1.3'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: '1.3'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rake
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: bump
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ description: Tool belt for managing your S3 buckets
143
+ email:
144
+ - lincoln@comum.org
145
+ executables:
146
+ - s3sync
147
+ extensions: []
148
+ extra_rdoc_files: []
149
+ files:
150
+ - bin/s3sync
151
+ - lib/s3sync.rb
152
+ - lib/s3sync/cli.rb
153
+ - lib/s3sync/config.rb
154
+ - lib/s3sync/exceptions.rb
155
+ - lib/s3sync/sync.rb
156
+ - lib/s3sync/util.rb
157
+ - lib/s3sync/version.rb
158
+ homepage: https://github.com/clarete/s3sync
159
+ licenses:
160
+ - MIT
161
+ post_install_message:
162
+ rdoc_options: []
163
+ require_paths:
164
+ - lib
165
+ required_ruby_version: !ruby/object:Gem::Requirement
166
+ none: false
167
+ requirements:
168
+ - - ! '>='
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ segments:
172
+ - 0
173
+ hash: 4017142764294384593
174
+ required_rubygems_version: !ruby/object:Gem::Requirement
175
+ none: false
176
+ requirements:
177
+ - - ! '>='
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ segments:
181
+ - 0
182
+ hash: 4017142764294384593
183
+ requirements: []
184
+ rubyforge_project:
185
+ rubygems_version: 1.8.24
186
+ signing_key:
187
+ specification_version: 3
188
+ summary: s3sync is a library that aggregates a good range of features for managing
189
+ your Amazon S3 buckets. It also provides basic interactive client
190
+ test_files: []