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 +73 -0
- data/lib/s3sync.rb +2 -0
- data/lib/s3sync/cli.rb +475 -0
- data/lib/s3sync/config.rb +98 -0
- data/lib/s3sync/exceptions.rb +55 -0
- data/lib/s3sync/sync.rb +366 -0
- data/lib/s3sync/util.rb +29 -0
- data/lib/s3sync/version.rb +27 -0
- metadata +190 -0
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
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
|
data/lib/s3sync/sync.rb
ADDED
@@ -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
|
data/lib/s3sync/util.rb
ADDED
@@ -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: []
|