s3sync 0.3.2

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.
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: []