s3sync 1.2.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/bin/s3sync +67 -726
- 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 +371 -0
- data/lib/s3sync/util.rb +29 -0
- data/lib/s3sync/version.rb +27 -0
- metadata +177 -54
- data/CHANGELOG +0 -175
- data/README +0 -401
- data/README_s3cmd +0 -172
- data/Rakefile +0 -35
- data/bin/s3cmd +0 -245
- data/lib/HTTPStreaming.rb +0 -103
- data/lib/S3.rb +0 -707
- data/lib/S3_s3sync_mod.rb +0 -143
- data/lib/S3encoder.rb +0 -50
- data/lib/s3config.rb +0 -27
- data/lib/s3try.rb +0 -161
- data/lib/thread_generator.rb +0 -383
- data/lib/version.rb +0 -9
- data/setup.rb +0 -1585
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
|