kerryb-right_aws 1.7.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,990 @@
1
+ #
2
+ # Copyright (c) 2007-2008 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #
23
+
24
+ module RightAws
25
+
26
+ class S3Interface < RightAwsBase
27
+
28
+ USE_100_CONTINUE_PUT_SIZE = 1_000_000
29
+
30
+ include RightAwsBaseInterface
31
+
32
+ DEFAULT_HOST = 's3.amazonaws.com'
33
+ DEFAULT_PORT = 443
34
+ DEFAULT_PROTOCOL = 'https'
35
+ REQUEST_TTL = 30
36
+ DEFAULT_EXPIRES_AFTER = 1 * 24 * 60 * 60 # One day's worth of seconds
37
+ ONE_YEAR_IN_SECONDS = 365 * 24 * 60 * 60
38
+ AMAZON_HEADER_PREFIX = 'x-amz-'
39
+ AMAZON_METADATA_PREFIX = 'x-amz-meta-'
40
+
41
+ @@bench = AwsBenchmarkingBlock.new
42
+ def self.bench_xml
43
+ @@bench.xml
44
+ end
45
+ def self.bench_s3
46
+ @@bench.service
47
+ end
48
+
49
+
50
+ # Creates new RightS3 instance.
51
+ #
52
+ # s3 = RightAws::S3Interface.new('1E3GDYEOGFJPIT7XXXXXX','hgTHt68JY07JKUY08ftHYtERkjgtfERn57XXXXXX', {:multi_thread => true, :logger => Logger.new('/tmp/x.log')}) #=> #<RightS3:0xb7b3c27c>
53
+ #
54
+ # Params is a hash:
55
+ #
56
+ # {:server => 's3.amazonaws.com' # Amazon service host: 's3.amazonaws.com'(default)
57
+ # :port => 443 # Amazon service port: 80 or 443(default)
58
+ # :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default)
59
+ # :multi_thread => true|false # Multi-threaded (connection per each thread): true or false(default)
60
+ # :logger => Logger Object} # Logger instance: logs to STDOUT if omitted }
61
+ #
62
+ def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
63
+ init({ :name => 'S3',
64
+ :default_host => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).host : DEFAULT_HOST,
65
+ :default_port => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).port : DEFAULT_PORT,
66
+ :default_protocol => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).scheme : DEFAULT_PROTOCOL },
67
+ aws_access_key_id || ENV['AWS_ACCESS_KEY_ID'],
68
+ aws_secret_access_key || ENV['AWS_SECRET_ACCESS_KEY'],
69
+ params)
70
+ end
71
+
72
+
73
+ #-----------------------------------------------------------------
74
+ # Requests
75
+ #-----------------------------------------------------------------
76
+ # Produces canonical string for signing.
77
+ def canonical_string(method, path, headers={}, expires=nil) # :nodoc:
78
+ s3_headers = {}
79
+ headers.each do |key, value|
80
+ key = key.downcase
81
+ s3_headers[key] = value.to_s.strip if key[/^#{AMAZON_HEADER_PREFIX}|^content-md5$|^content-type$|^date$/o]
82
+ end
83
+ s3_headers['content-type'] ||= ''
84
+ s3_headers['content-md5'] ||= ''
85
+ s3_headers['date'] = '' if s3_headers.has_key? 'x-amz-date'
86
+ s3_headers['date'] = expires if expires
87
+ # prepare output string
88
+ out_string = "#{method}\n"
89
+ s3_headers.sort { |a, b| a[0] <=> b[0] }.each do |key, value|
90
+ out_string << (key[/^#{AMAZON_HEADER_PREFIX}/o] ? "#{key}:#{value}\n" : "#{value}\n")
91
+ end
92
+ # ignore everything after the question mark...
93
+ out_string << path.gsub(/\?.*$/, '')
94
+ # ...unless there is an acl or torrent parameter
95
+ out_string << '?acl' if path[/[&?]acl($|&|=)/]
96
+ out_string << '?torrent' if path[/[&?]torrent($|&|=)/]
97
+ out_string << '?location' if path[/[&?]location($|&|=)/]
98
+ # out_string << '?logging' if path[/[&?]logging($|&|=)/] # this one is beta, no support for now
99
+ out_string
100
+ end
101
+
102
+ def is_dns_bucket?(bucket_name)
103
+ bucket_name = bucket_name.to_s
104
+ return nil unless (3..63) === bucket_name.size
105
+ bucket_name.split('.').each do |component|
106
+ return nil unless component[/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/]
107
+ end
108
+ true
109
+ end
110
+
111
+ # Generates request hash for REST API.
112
+ # Assumes that headers[:url] is URL encoded (use CGI::escape)
113
+ def generate_rest_request(method, headers) # :nodoc:
114
+ # default server to use
115
+ server = @params[:server]
116
+ # fix path
117
+ path_to_sign = headers[:url]
118
+ path_to_sign = "/#{path_to_sign}" unless path_to_sign[/^\//]
119
+ # extract bucket name and check it's dns compartibility
120
+ path_to_sign[%r{^/([a-z0-9._-]*)(/[^?]*)?(\?.+)?}i]
121
+ bucket_name, key_path, params_list = $1, $2, $3
122
+ # select request model
123
+ if is_dns_bucket?(bucket_name)
124
+ # add backet to a server name
125
+ server = "#{bucket_name}.#{server}"
126
+ # remove bucket from the path
127
+ path = "#{key_path || '/'}#{params_list}"
128
+ # refactor the path (add '/' before params_list if the key is empty)
129
+ path_to_sign = "/#{bucket_name}#{path}"
130
+ else
131
+ path = path_to_sign
132
+ end
133
+ data = headers[:data]
134
+ # remove unset(==optional) and symbolyc keys
135
+ headers.each{ |key, value| headers.delete(key) if (value.nil? || key.is_a?(Symbol)) }
136
+ #
137
+ headers['content-type'] ||= ''
138
+ headers['date'] = Time.now.httpdate
139
+ # create request
140
+ request = "Net::HTTP::#{method.capitalize}".constantize.new(path)
141
+ request.body = data if data
142
+ # set request headers and meta headers
143
+ headers.each { |key, value| request[key.to_s] = value }
144
+ #generate auth strings
145
+ auth_string = canonical_string(request.method, path_to_sign, request.to_hash)
146
+ signature = AwsUtils::sign(@aws_secret_access_key, auth_string)
147
+ # set other headers
148
+ request['Authorization'] = "AWS #{@aws_access_key_id}:#{signature}"
149
+ # prepare output hash
150
+ { :request => request,
151
+ :server => server,
152
+ :port => @params[:port],
153
+ :protocol => @params[:protocol] }
154
+ end
155
+
156
+ # Sends request to Amazon and parses the response.
157
+ # Raises AwsError if any banana happened.
158
+ def request_info(request, parser, &block) # :nodoc:
159
+ thread = @params[:multi_thread] ? Thread.current : Thread.main
160
+ thread[:s3_connection] ||= Rightscale::HttpConnection.new(:exception => RightAws::AwsError, :logger => @logger)
161
+ request_info_impl(thread[:s3_connection], @@bench, request, parser, &block)
162
+ end
163
+
164
+
165
+ # Returns an array of customer's buckets. Each item is a +hash+.
166
+ #
167
+ # s3.list_all_my_buckets #=>
168
+ # [{:owner_id => "00000000009314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a",
169
+ # :owner_display_name => "root",
170
+ # :name => "bucket_name",
171
+ # :creation_date => "2007-04-19T18:47:43.000Z"}, ..., {...}]
172
+ #
173
+ def list_all_my_buckets(headers={})
174
+ req_hash = generate_rest_request('GET', headers.merge(:url=>''))
175
+ request_info(req_hash, S3ListAllMyBucketsParser.new(:logger => @logger))
176
+ rescue
177
+ on_exception
178
+ end
179
+
180
+ # Creates new bucket. Returns +true+ or an exception.
181
+ #
182
+ # # create a bucket at American server
183
+ # s3.create_bucket('my-awesome-bucket-us') #=> true
184
+ # # create a bucket at European server
185
+ # s3.create_bucket('my-awesome-bucket-eu', :location => :eu) #=> true
186
+ #
187
+ def create_bucket(bucket, headers={})
188
+ data = nil
189
+ unless headers[:location].blank?
190
+ data = "<CreateBucketConfiguration><LocationConstraint>#{headers[:location].to_s.upcase}</LocationConstraint></CreateBucketConfiguration>"
191
+ end
192
+ req_hash = generate_rest_request('PUT', headers.merge(:url=>bucket, :data => data, 'content-length' => ((data && data.size) || 0).to_s))
193
+ request_info(req_hash, S3TrueParser.new)
194
+ rescue Exception => e
195
+ # if the bucket exists AWS returns an error for the location constraint interface. Drop it
196
+ e.is_a?(RightAws::AwsError) && e.message.include?('BucketAlreadyOwnedByYou') ? true : on_exception
197
+ end
198
+
199
+ # Retrieve bucket location
200
+ #
201
+ # s3.create_bucket('my-awesome-bucket-us') #=> true
202
+ # puts s3.bucket_location('my-awesome-bucket-us') #=> '' (Amazon's default value assumed)
203
+ #
204
+ # s3.create_bucket('my-awesome-bucket-eu', :location => :eu) #=> true
205
+ # puts s3.bucket_location('my-awesome-bucket-eu') #=> 'EU'
206
+ #
207
+ def bucket_location(bucket, headers={})
208
+ req_hash = generate_rest_request('GET', headers.merge(:url=>"#{bucket}?location"))
209
+ request_info(req_hash, S3BucketLocationParser.new)
210
+ rescue
211
+ on_exception
212
+ end
213
+
214
+ # Deletes new bucket. Bucket must be empty! Returns +true+ or an exception.
215
+ #
216
+ # s3.delete_bucket('my_awesome_bucket') #=> true
217
+ #
218
+ # See also: force_delete_bucket method
219
+ #
220
+ def delete_bucket(bucket, headers={})
221
+ req_hash = generate_rest_request('DELETE', headers.merge(:url=>bucket))
222
+ request_info(req_hash, S3TrueParser.new)
223
+ rescue
224
+ on_exception
225
+ end
226
+
227
+ # Returns an array of bucket's keys. Each array item (key data) is a +hash+.
228
+ #
229
+ # s3.list_bucket('my_awesome_bucket', { 'prefix'=>'t', 'marker'=>'', 'max-keys'=>5, delimiter=>'' }) #=>
230
+ # [{:key => "test1",
231
+ # :last_modified => "2007-05-18T07:00:59.000Z",
232
+ # :owner_id => "00000000009314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a",
233
+ # :owner_display_name => "root",
234
+ # :e_tag => "000000000059075b964b07152d234b70",
235
+ # :storage_class => "STANDARD",
236
+ # :size => 3,
237
+ # :service=> {'is_truncated' => false,
238
+ # 'prefix' => "t",
239
+ # 'marker' => "",
240
+ # 'name' => "my_awesome_bucket",
241
+ # 'max-keys' => "5"}, ..., {...}]
242
+ #
243
+ def list_bucket(bucket, options={}, headers={})
244
+ bucket += '?'+options.map{|k, v| "#{k.to_s}=#{CGI::escape v.to_s}"}.join('&') unless options.blank?
245
+ req_hash = generate_rest_request('GET', headers.merge(:url=>bucket))
246
+ request_info(req_hash, S3ListBucketParser.new(:logger => @logger))
247
+ rescue
248
+ on_exception
249
+ end
250
+
251
+ # Incrementally list the contents of a bucket. Yields the following hash to a block:
252
+ # s3.incrementally_list_bucket('my_awesome_bucket', { 'prefix'=>'t', 'marker'=>'', 'max-keys'=>5, delimiter=>'' }) yields
253
+ # {
254
+ # :name => 'bucketname',
255
+ # :prefix => 'subfolder/',
256
+ # :marker => 'fileN.jpg',
257
+ # :max_keys => 234,
258
+ # :delimiter => '/',
259
+ # :is_truncated => true,
260
+ # :next_marker => 'fileX.jpg',
261
+ # :contents => [
262
+ # { :key => "file1",
263
+ # :last_modified => "2007-05-18T07:00:59.000Z",
264
+ # :e_tag => "000000000059075b964b07152d234b70",
265
+ # :size => 3,
266
+ # :storage_class => "STANDARD",
267
+ # :owner_id => "00000000009314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a",
268
+ # :owner_display_name => "root"
269
+ # }, { :key, ...}, ... {:key, ...}
270
+ # ]
271
+ # :common_prefixes => [
272
+ # "prefix1",
273
+ # "prefix2",
274
+ # ...,
275
+ # "prefixN"
276
+ # ]
277
+ # }
278
+ def incrementally_list_bucket(bucket, options={}, headers={}, &block)
279
+ internal_options = options.symbolize_keys
280
+ begin
281
+ internal_bucket = bucket.dup
282
+ internal_bucket += '?'+internal_options.map{|k, v| "#{k.to_s}=#{CGI::escape v.to_s}"}.join('&') unless internal_options.blank?
283
+ req_hash = generate_rest_request('GET', headers.merge(:url=>internal_bucket))
284
+ response = request_info(req_hash, S3ImprovedListBucketParser.new(:logger => @logger))
285
+ there_are_more_keys = response[:is_truncated]
286
+ if(there_are_more_keys)
287
+ internal_options[:marker] = decide_marker(response)
288
+ total_results = response[:contents].length + response[:common_prefixes].length
289
+ internal_options[:'max-keys'] ? (internal_options[:'max-keys'] -= total_results) : nil
290
+ end
291
+ yield response
292
+ end while there_are_more_keys && under_max_keys(internal_options)
293
+ true
294
+ rescue
295
+ on_exception
296
+ end
297
+
298
+
299
+ private
300
+ def decide_marker(response)
301
+ return response[:next_marker].dup if response[:next_marker]
302
+ last_key = response[:contents].last[:key]
303
+ last_prefix = response[:common_prefixes].last
304
+ if(!last_key)
305
+ return nil if(!last_prefix)
306
+ last_prefix.dup
307
+ elsif(!last_prefix)
308
+ last_key.dup
309
+ else
310
+ last_key > last_prefix ? last_key.dup : last_prefix.dup
311
+ end
312
+ end
313
+
314
+ def under_max_keys(internal_options)
315
+ internal_options[:'max-keys'] ? internal_options[:'max-keys'] > 0 : true
316
+ end
317
+
318
+ public
319
+ # Saves object to Amazon. Returns +true+ or an exception.
320
+ # Any header starting with AMAZON_METADATA_PREFIX is considered
321
+ # user metadata. It will be stored with the object and returned
322
+ # when you retrieve the object. The total size of the HTTP
323
+ # request, not including the body, must be less than 4 KB.
324
+ #
325
+ # s3.put('my_awesome_bucket', 'log/current/1.log', 'Ola-la!', 'x-amz-meta-family'=>'Woho556!') #=> true
326
+ #
327
+ # This method is capable of 'streaming' uploads; that is, it can upload
328
+ # data from a file or other IO object without first reading all the data
329
+ # into memory. This is most useful for large PUTs - it is difficult to read
330
+ # a 2 GB file entirely into memory before sending it to S3.
331
+ # To stream an upload, pass an object that responds to 'read' (like the read
332
+ # method of IO) and to either 'lstat' or 'size'. For files, this means
333
+ # streaming is enabled by simply making the call:
334
+ #
335
+ # s3.put(bucket_name, 'S3keyname.forthisfile', File.open('localfilename.dat'))
336
+ #
337
+ # If the IO object you wish to stream from responds to the read method but
338
+ # doesn't implement lstat or size, you can extend the object dynamically
339
+ # to implement these methods, or define your own class which defines these
340
+ # methods. Be sure that your class returns 'nil' from read() after having
341
+ # read 'size' bytes. Otherwise S3 will drop the socket after
342
+ # 'Content-Length' bytes have been uploaded, and HttpConnection will
343
+ # interpret this as an error.
344
+ #
345
+ # This method now supports very large PUTs, where very large
346
+ # is > 2 GB.
347
+ #
348
+ # For Win32 users: Files and IO objects should be opened in binary mode. If
349
+ # a text mode IO object is passed to PUT, it will be converted to binary
350
+ # mode.
351
+ #
352
+ def put(bucket, key, data=nil, headers={})
353
+ # On Windows, if someone opens a file in text mode, we must reset it so
354
+ # to binary mode for streaming to work properly
355
+ if(data.respond_to?(:binmode))
356
+ data.binmode
357
+ end
358
+ if (data.respond_to?(:lstat) && data.lstat.size >= USE_100_CONTINUE_PUT_SIZE) ||
359
+ (data.respond_to?(:size) && data.size >= USE_100_CONTINUE_PUT_SIZE)
360
+ headers['expect'] = '100-continue'
361
+ end
362
+ req_hash = generate_rest_request('PUT', headers.merge(:url=>"#{bucket}/#{CGI::escape key}", :data=>data))
363
+ request_info(req_hash, S3TrueParser.new)
364
+ rescue
365
+ on_exception
366
+ end
367
+
368
+ # Retrieves object data from Amazon. Returns a +hash+ or an exception.
369
+ #
370
+ # s3.get('my_awesome_bucket', 'log/curent/1.log') #=>
371
+ #
372
+ # {:object => "Ola-la!",
373
+ # :headers => {"last-modified" => "Wed, 23 May 2007 09:08:04 GMT",
374
+ # "content-type" => "",
375
+ # "etag" => "\"000000000096f4ee74bc4596443ef2a4\"",
376
+ # "date" => "Wed, 23 May 2007 09:08:03 GMT",
377
+ # "x-amz-id-2" => "ZZZZZZZZZZZZZZZZZZZZ1HJXZoehfrS4QxcxTdNGldR7w/FVqblP50fU8cuIMLiu",
378
+ # "x-amz-meta-family" => "Woho556!",
379
+ # "x-amz-request-id" => "0000000C246D770C",
380
+ # "server" => "AmazonS3",
381
+ # "content-length" => "7"}}
382
+ #
383
+ # If a block is provided, yields incrementally to the block as
384
+ # the response is read. For large responses, this function is ideal as
385
+ # the response can be 'streamed'. The hash containing header fields is
386
+ # still returned.
387
+ # Example:
388
+ # foo = File.new('./chunder.txt', File::CREAT|File::RDWR)
389
+ # rhdr = s3.get('aws-test', 'Cent5V1_7_1.img.part.00') do |chunk|
390
+ # foo.write(chunk)
391
+ # end
392
+ # foo.close
393
+ #
394
+
395
+ def get(bucket, key, headers={}, &block)
396
+ req_hash = generate_rest_request('GET', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"))
397
+ request_info(req_hash, S3HttpResponseBodyParser.new, &block)
398
+ rescue
399
+ on_exception
400
+ end
401
+
402
+ # Retrieves object metadata. Returns a +hash+ of http_response_headers.
403
+ #
404
+ # s3.head('my_awesome_bucket', 'log/curent/1.log') #=>
405
+ # {"last-modified" => "Wed, 23 May 2007 09:08:04 GMT",
406
+ # "content-type" => "",
407
+ # "etag" => "\"000000000096f4ee74bc4596443ef2a4\"",
408
+ # "date" => "Wed, 23 May 2007 09:08:03 GMT",
409
+ # "x-amz-id-2" => "ZZZZZZZZZZZZZZZZZZZZ1HJXZoehfrS4QxcxTdNGldR7w/FVqblP50fU8cuIMLiu",
410
+ # "x-amz-meta-family" => "Woho556!",
411
+ # "x-amz-request-id" => "0000000C246D770C",
412
+ # "server" => "AmazonS3",
413
+ # "content-length" => "7"}
414
+ #
415
+ def head(bucket, key, headers={})
416
+ req_hash = generate_rest_request('HEAD', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"))
417
+ request_info(req_hash, S3HttpResponseHeadParser.new)
418
+ rescue
419
+ on_exception
420
+ end
421
+
422
+ # Deletes key. Returns +true+ or an exception.
423
+ #
424
+ # s3.delete('my_awesome_bucket', 'log/curent/1.log') #=> true
425
+ #
426
+ def delete(bucket, key='', headers={})
427
+ req_hash = generate_rest_request('DELETE', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"))
428
+ request_info(req_hash, S3TrueParser.new)
429
+ rescue
430
+ on_exception
431
+ end
432
+
433
+ # Copy an object.
434
+ # directive: :copy - copy meta-headers from source (default value)
435
+ # :replace - replace meta-headers by passed ones
436
+ #
437
+ # # copy a key with meta-headers
438
+ # s3.copy('b1', 'key1', 'b1', 'key1_copy') #=> {:e_tag=>"\"e8b...8d\"", :last_modified=>"2008-05-11T10:25:22.000Z"}
439
+ #
440
+ # # copy a key, overwrite meta-headers
441
+ # s3.copy('b1', 'key2', 'b1', 'key2_copy', :replace, 'x-amz-meta-family'=>'Woho555!') #=> {:e_tag=>"\"e8b...8d\"", :last_modified=>"2008-05-11T10:26:22.000Z"}
442
+ #
443
+ # see: http://docs.amazonwebservices.com/AmazonS3/2006-03-01/UsingCopyingObjects.html
444
+ # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTObjectCOPY.html
445
+ #
446
+ def copy(src_bucket, src_key, dest_bucket, dest_key=nil, directive=:copy, headers={})
447
+ dest_key ||= src_key
448
+ headers['x-amz-metadata-directive'] = directive.to_s.upcase
449
+ headers['x-amz-copy-source'] = "#{src_bucket}/#{src_key}"
450
+ req_hash = generate_rest_request('PUT', headers.merge(:url=>"#{dest_bucket}/#{CGI::escape dest_key}"))
451
+ request_info(req_hash, S3CopyParser.new)
452
+ rescue
453
+ on_exception
454
+ end
455
+
456
+ # Move an object.
457
+ # directive: :copy - copy meta-headers from source (default value)
458
+ # :replace - replace meta-headers by passed ones
459
+ #
460
+ # # move bucket1/key1 to bucket1/key2
461
+ # s3.move('bucket1', 'key1', 'bucket1', 'key2') #=> {:e_tag=>"\"e8b...8d\"", :last_modified=>"2008-05-11T10:27:22.000Z"}
462
+ #
463
+ # # move bucket1/key1 to bucket2/key2 with new meta-headers assignment
464
+ # s3.copy('bucket1', 'key1', 'bucket2', 'key2', :replace, 'x-amz-meta-family'=>'Woho555!') #=> {:e_tag=>"\"e8b...8d\"", :last_modified=>"2008-05-11T10:28:22.000Z"}
465
+ #
466
+ def move(src_bucket, src_key, dest_bucket, dest_key=nil, directive=:copy, headers={})
467
+ copy_result = copy(src_bucket, src_key, dest_bucket, dest_key, directive, headers)
468
+ # delete an original key if it differs from a destination one
469
+ delete(src_bucket, src_key) unless src_bucket == dest_bucket && src_key == dest_key
470
+ copy_result
471
+ end
472
+
473
+ # Rename an object.
474
+ #
475
+ # # rename bucket1/key1 to bucket1/key2
476
+ # s3.rename('bucket1', 'key1', 'key2') #=> {:e_tag=>"\"e8b...8d\"", :last_modified=>"2008-05-11T10:29:22.000Z"}
477
+ #
478
+ def rename(src_bucket, src_key, dest_key, headers={})
479
+ move(src_bucket, src_key, src_bucket, dest_key, :copy, headers)
480
+ end
481
+
482
+ # Retieves the ACL (access control policy) for a bucket or object. Returns a hash of headers and xml doc with ACL data. See: http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html.
483
+ #
484
+ # s3.get_acl('my_awesome_bucket', 'log/curent/1.log') #=>
485
+ # {:headers => {"x-amz-id-2"=>"B3BdDMDUz+phFF2mGBH04E46ZD4Qb9HF5PoPHqDRWBv+NVGeA3TOQ3BkVvPBjgxX",
486
+ # "content-type"=>"application/xml;charset=ISO-8859-1",
487
+ # "date"=>"Wed, 23 May 2007 09:40:16 GMT",
488
+ # "x-amz-request-id"=>"B183FA7AB5FBB4DD",
489
+ # "server"=>"AmazonS3",
490
+ # "transfer-encoding"=>"chunked"},
491
+ # :object => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<AccessControlPolicy xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Owner>
492
+ # <ID>16144ab2929314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a</ID><DisplayName>root</DisplayName></Owner>
493
+ # <AccessControlList><Grant><Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\"><ID>
494
+ # 16144ab2929314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a</ID><DisplayName>root</DisplayName></Grantee>
495
+ # <Permission>FULL_CONTROL</Permission></Grant></AccessControlList></AccessControlPolicy>" }
496
+ #
497
+ def get_acl(bucket, key='', headers={})
498
+ key = key.blank? ? '' : "/#{CGI::escape key}"
499
+ req_hash = generate_rest_request('GET', headers.merge(:url=>"#{bucket}#{key}?acl"))
500
+ request_info(req_hash, S3HttpResponseBodyParser.new)
501
+ rescue
502
+ on_exception
503
+ end
504
+
505
+ # Retieves the ACL (access control policy) for a bucket or object.
506
+ # Returns a hash of {:owner, :grantees}
507
+ #
508
+ # s3.get_acl_parse('my_awesome_bucket', 'log/curent/1.log') #=>
509
+ #
510
+ # { :grantees=>
511
+ # { "16...2a"=>
512
+ # { :display_name=>"root",
513
+ # :permissions=>["FULL_CONTROL"],
514
+ # :attributes=>
515
+ # { "xsi:type"=>"CanonicalUser",
516
+ # "xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance"}},
517
+ # "http://acs.amazonaws.com/groups/global/AllUsers"=>
518
+ # { :display_name=>"AllUsers",
519
+ # :permissions=>["READ"],
520
+ # :attributes=>
521
+ # { "xsi:type"=>"Group",
522
+ # "xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance"}}},
523
+ # :owner=>
524
+ # { :id=>"16..2a",
525
+ # :display_name=>"root"}}
526
+ #
527
+ def get_acl_parse(bucket, key='', headers={})
528
+ key = key.blank? ? '' : "/#{CGI::escape key}"
529
+ req_hash = generate_rest_request('GET', headers.merge(:url=>"#{bucket}#{key}?acl"))
530
+ acl = request_info(req_hash, S3AclParser.new(:logger => @logger))
531
+ result = {}
532
+ result[:owner] = acl[:owner]
533
+ result[:grantees] = {}
534
+ acl[:grantees].each do |grantee|
535
+ key = grantee[:id] || grantee[:uri]
536
+ if result[:grantees].key?(key)
537
+ result[:grantees][key][:permissions] << grantee[:permissions]
538
+ else
539
+ result[:grantees][key] =
540
+ { :display_name => grantee[:display_name] || grantee[:uri].to_s[/[^\/]*$/],
541
+ :permissions => grantee[:permissions].to_a,
542
+ :attributes => grantee[:attributes] }
543
+ end
544
+ end
545
+ result
546
+ rescue
547
+ on_exception
548
+ end
549
+
550
+ # Sets the ACL on a bucket or object.
551
+ def put_acl(bucket, key, acl_xml_doc, headers={})
552
+ key = key.blank? ? '' : "/#{CGI::escape key}"
553
+ req_hash = generate_rest_request('PUT', headers.merge(:url=>"#{bucket}#{key}?acl", :data=>acl_xml_doc))
554
+ request_info(req_hash, S3HttpResponseBodyParser.new)
555
+ rescue
556
+ on_exception
557
+ end
558
+
559
+ # Retieves the ACL (access control policy) for a bucket. Returns a hash of headers and xml doc with ACL data.
560
+ def get_bucket_acl(bucket, headers={})
561
+ return get_acl(bucket, '', headers)
562
+ rescue
563
+ on_exception
564
+ end
565
+
566
+ # Sets the ACL on a bucket only.
567
+ def put_bucket_acl(bucket, acl_xml_doc, headers={})
568
+ return put_acl(bucket, '', acl_xml_doc, headers)
569
+ rescue
570
+ on_exception
571
+ end
572
+
573
+
574
+ # Removes all keys from bucket. Returns +true+ or an exception.
575
+ #
576
+ # s3.clear_bucket('my_awesome_bucket') #=> true
577
+ #
578
+ def clear_bucket(bucket)
579
+ incrementally_list_bucket(bucket) do |results|
580
+ results[:contents].each { |key| delete(bucket, key[:key]) }
581
+ end
582
+ true
583
+ rescue
584
+ on_exception
585
+ end
586
+
587
+ # Deletes all keys in bucket then deletes bucket. Returns +true+ or an exception.
588
+ #
589
+ # s3.force_delete_bucket('my_awesome_bucket')
590
+ #
591
+ def force_delete_bucket(bucket)
592
+ clear_bucket(bucket)
593
+ delete_bucket(bucket)
594
+ rescue
595
+ on_exception
596
+ end
597
+
598
+ # Deletes all keys where the 'folder_key' may be assumed as 'folder' name. Returns an array of string keys that have been deleted.
599
+ #
600
+ # s3.list_bucket('my_awesome_bucket').map{|key_data| key_data[:key]} #=> ['test','test/2/34','test/3','test1','test1/logs']
601
+ # s3.delete_folder('my_awesome_bucket','test') #=> ['test','test/2/34','test/3']
602
+ #
603
+ def delete_folder(bucket, folder_key, separator='/')
604
+ folder_key.chomp!(separator)
605
+ allkeys = []
606
+ incrementally_list_bucket(bucket, { 'prefix' => folder_key }) do |results|
607
+ keys = results[:contents].map{ |s3_key| s3_key[:key][/^#{folder_key}($|#{separator}.*)/] ? s3_key[:key] : nil}.compact
608
+ keys.each{ |key| delete(bucket, key) }
609
+ allkeys << keys
610
+ end
611
+ allkeys
612
+ rescue
613
+ on_exception
614
+ end
615
+
616
+ # Retrieves object data only (headers are omitted). Returns +string+ or an exception.
617
+ #
618
+ # s3.get('my_awesome_bucket', 'log/curent/1.log') #=> 'Ola-la!'
619
+ #
620
+ def get_object(bucket, key, headers={})
621
+ get(bucket, key, headers)[:object]
622
+ rescue
623
+ on_exception
624
+ end
625
+
626
+ #-----------------------------------------------------------------
627
+ # Query API: Links
628
+ #-----------------------------------------------------------------
629
+
630
+ # Generates link for QUERY API
631
+ def generate_link(method, headers={}, expires=nil) #:nodoc:
632
+ # default server to use
633
+ server = @params[:server]
634
+ # fix path
635
+ path_to_sign = headers[:url]
636
+ path_to_sign = "/#{path_to_sign}" unless path_to_sign[/^\//]
637
+ # extract bucket name and check it's dns compartibility
638
+ path_to_sign[%r{^/([a-z0-9._-]*)(/[^?]*)?(\?.+)?}i]
639
+ bucket_name, key_path, params_list = $1, $2, $3
640
+ # select request model
641
+ if is_dns_bucket?(bucket_name)
642
+ # add backet to a server name
643
+ server = "#{bucket_name}.#{server}"
644
+ # remove bucket from the path
645
+ path = "#{key_path || '/'}#{params_list}"
646
+ # refactor the path (add '/' before params_list if the key is empty)
647
+ path_to_sign = "/#{bucket_name}#{path}"
648
+ else
649
+ path = path_to_sign
650
+ end
651
+ # expiration time
652
+ expires ||= DEFAULT_EXPIRES_AFTER
653
+ expires = Time.now.utc + expires if expires.is_a?(Fixnum) && (expires < ONE_YEAR_IN_SECONDS)
654
+ expires = expires.to_i
655
+ # remove unset(==optional) and symbolyc keys
656
+ headers.each{ |key, value| headers.delete(key) if (value.nil? || key.is_a?(Symbol)) }
657
+ #generate auth strings
658
+ auth_string = canonical_string(method, path_to_sign, headers, expires)
659
+ signature = CGI::escape(Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new("sha1"), @aws_secret_access_key, auth_string)).strip)
660
+ # path building
661
+ addon = "Signature=#{signature}&Expires=#{expires}&AWSAccessKeyId=#{@aws_access_key_id}"
662
+ path += path[/\?/] ? "&#{addon}" : "?#{addon}"
663
+ "#{@params[:protocol]}://#{server}:#{@params[:port]}#{path}"
664
+ rescue
665
+ on_exception
666
+ end
667
+
668
+ # Generates link for 'ListAllMyBuckets'.
669
+ #
670
+ # s3.list_all_my_buckets_link #=> url string
671
+ #
672
+ def list_all_my_buckets_link(expires=nil, headers={})
673
+ generate_link('GET', headers.merge(:url=>''), expires)
674
+ rescue
675
+ on_exception
676
+ end
677
+
678
+ # Generates link for 'CreateBucket'.
679
+ #
680
+ # s3.create_bucket_link('my_awesome_bucket') #=> url string
681
+ #
682
+ def create_bucket_link(bucket, expires=nil, headers={})
683
+ generate_link('PUT', headers.merge(:url=>bucket), expires)
684
+ rescue
685
+ on_exception
686
+ end
687
+
688
+ # Generates link for 'DeleteBucket'.
689
+ #
690
+ # s3.delete_bucket_link('my_awesome_bucket') #=> url string
691
+ #
692
+ def delete_bucket_link(bucket, expires=nil, headers={})
693
+ generate_link('DELETE', headers.merge(:url=>bucket), expires)
694
+ rescue
695
+ on_exception
696
+ end
697
+
698
+ # Generates link for 'ListBucket'.
699
+ #
700
+ # s3.list_bucket_link('my_awesome_bucket') #=> url string
701
+ #
702
+ def list_bucket_link(bucket, options=nil, expires=nil, headers={})
703
+ bucket += '?' + options.map{|k, v| "#{k.to_s}=#{CGI::escape v.to_s}"}.join('&') unless options.blank?
704
+ generate_link('GET', headers.merge(:url=>bucket), expires)
705
+ rescue
706
+ on_exception
707
+ end
708
+
709
+ # Generates link for 'PutObject'.
710
+ #
711
+ # s3.put_link('my_awesome_bucket',key, object) #=> url string
712
+ #
713
+ def put_link(bucket, key, data=nil, expires=nil, headers={})
714
+ generate_link('PUT', headers.merge(:url=>"#{bucket}/#{CGI::escape key}", :data=>data), expires)
715
+ rescue
716
+ on_exception
717
+ end
718
+
719
+ # Generates link for 'GetObject'.
720
+ #
721
+ # if a bucket comply with virtual hosting naming then retuns a link with the
722
+ # bucket as a part of host name:
723
+ #
724
+ # s3.get_link('my-awesome-bucket',key) #=> https://my-awesome-bucket.s3.amazonaws.com:443/asia%2Fcustomers?Signature=nh7...
725
+ #
726
+ # otherwise returns an old style link (the bucket is a part of path):
727
+ #
728
+ # s3.get_link('my_awesome_bucket',key) #=> https://s3.amazonaws.com:443/my_awesome_bucket/asia%2Fcustomers?Signature=QAO...
729
+ #
730
+ # see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/VirtualHosting.html
731
+ def get_link(bucket, key, expires=nil, headers={})
732
+ generate_link('GET', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"), expires)
733
+ rescue
734
+ on_exception
735
+ end
736
+
737
+ # Generates link for 'HeadObject'.
738
+ #
739
+ # s3.head_link('my_awesome_bucket',key) #=> url string
740
+ #
741
+ def head_link(bucket, key, expires=nil, headers={})
742
+ generate_link('HEAD', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"), expires)
743
+ rescue
744
+ on_exception
745
+ end
746
+
747
+ # Generates link for 'DeleteObject'.
748
+ #
749
+ # s3.delete_link('my_awesome_bucket',key) #=> url string
750
+ #
751
+ def delete_link(bucket, key, expires=nil, headers={})
752
+ generate_link('DELETE', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"), expires)
753
+ rescue
754
+ on_exception
755
+ end
756
+
757
+
758
+ # Generates link for 'GetACL'.
759
+ #
760
+ # s3.get_acl_link('my_awesome_bucket',key) #=> url string
761
+ #
762
+ def get_acl_link(bucket, key='', headers={})
763
+ return generate_link('GET', headers.merge(:url=>"#{bucket}/#{CGI::escape key}?acl"))
764
+ rescue
765
+ on_exception
766
+ end
767
+
768
+ # Generates link for 'PutACL'.
769
+ #
770
+ # s3.put_acl_link('my_awesome_bucket',key) #=> url string
771
+ #
772
+ def put_acl_link(bucket, key='', headers={})
773
+ return generate_link('PUT', headers.merge(:url=>"#{bucket}/#{CGI::escape key}?acl"))
774
+ rescue
775
+ on_exception
776
+ end
777
+
778
+ # Generates link for 'GetBucketACL'.
779
+ #
780
+ # s3.get_acl_link('my_awesome_bucket',key) #=> url string
781
+ #
782
+ def get_bucket_acl_link(bucket, headers={})
783
+ return get_acl_link(bucket, '', headers)
784
+ rescue
785
+ on_exception
786
+ end
787
+
788
+ # Generates link for 'PutBucketACL'.
789
+ #
790
+ # s3.put_acl_link('my_awesome_bucket',key) #=> url string
791
+ #
792
+ def put_bucket_acl_link(bucket, acl_xml_doc, headers={})
793
+ return put_acl_link(bucket, '', acl_xml_doc, headers)
794
+ rescue
795
+ on_exception
796
+ end
797
+
798
+ #-----------------------------------------------------------------
799
+ # PARSERS:
800
+ #-----------------------------------------------------------------
801
+
802
+ class S3ListAllMyBucketsParser < RightAWSParser # :nodoc:
803
+ def reset
804
+ @result = []
805
+ @owner = {}
806
+ end
807
+ def tagstart(name, attributes)
808
+ @current_bucket = {} if name == 'Bucket'
809
+ end
810
+ def tagend(name)
811
+ case name
812
+ when 'ID' ; @owner[:owner_id] = @text
813
+ when 'DisplayName' ; @owner[:owner_display_name] = @text
814
+ when 'Name' ; @current_bucket[:name] = @text
815
+ when 'CreationDate'; @current_bucket[:creation_date] = @text
816
+ when 'Bucket' ; @result << @current_bucket.merge(@owner)
817
+ end
818
+ end
819
+ end
820
+
821
+ class S3ListBucketParser < RightAWSParser # :nodoc:
822
+ def reset
823
+ @result = []
824
+ @service = {}
825
+ @current_key = {}
826
+ end
827
+ def tagstart(name, attributes)
828
+ @current_key = {} if name == 'Contents'
829
+ end
830
+ def tagend(name)
831
+ case name
832
+ # service info
833
+ when 'Name' ; @service['name'] = @text
834
+ when 'Prefix' ; @service['prefix'] = @text
835
+ when 'Marker' ; @service['marker'] = @text
836
+ when 'MaxKeys' ; @service['max-keys'] = @text
837
+ when 'Delimiter' ; @service['delimiter'] = @text
838
+ when 'IsTruncated' ; @service['is_truncated'] = (@text =~ /false/ ? false : true)
839
+ # key data
840
+ when 'Key' ; @current_key[:key] = @text
841
+ when 'LastModified'; @current_key[:last_modified] = @text
842
+ when 'ETag' ; @current_key[:e_tag] = @text
843
+ when 'Size' ; @current_key[:size] = @text.to_i
844
+ when 'StorageClass'; @current_key[:storage_class] = @text
845
+ when 'ID' ; @current_key[:owner_id] = @text
846
+ when 'DisplayName' ; @current_key[:owner_display_name] = @text
847
+ when 'Contents' ; @current_key[:service] = @service; @result << @current_key
848
+ end
849
+ end
850
+ end
851
+
852
+ class S3ImprovedListBucketParser < RightAWSParser # :nodoc:
853
+ def reset
854
+ @result = {}
855
+ @result[:contents] = []
856
+ @result[:common_prefixes] = []
857
+ @contents = []
858
+ @current_key = {}
859
+ @common_prefixes = []
860
+ @in_common_prefixes = false
861
+ end
862
+ def tagstart(name, attributes)
863
+ @current_key = {} if name == 'Contents'
864
+ @in_common_prefixes = true if name == 'CommonPrefixes'
865
+ end
866
+ def tagend(name)
867
+ case name
868
+ # service info
869
+ when 'Name' ; @result[:name] = @text
870
+ # Amazon uses the same tag for the search prefix and for the entries
871
+ # in common prefix...so use our simple flag to see which element
872
+ # we are parsing
873
+ when 'Prefix' ; @in_common_prefixes ? @common_prefixes << @text : @result[:prefix] = @text
874
+ when 'Marker' ; @result[:marker] = @text
875
+ when 'MaxKeys' ; @result[:max_keys] = @text
876
+ when 'Delimiter' ; @result[:delimiter] = @text
877
+ when 'IsTruncated' ; @result[:is_truncated] = (@text =~ /false/ ? false : true)
878
+ when 'NextMarker' ; @result[:next_marker] = @text
879
+ # key data
880
+ when 'Key' ; @current_key[:key] = @text
881
+ when 'LastModified'; @current_key[:last_modified] = @text
882
+ when 'ETag' ; @current_key[:e_tag] = @text
883
+ when 'Size' ; @current_key[:size] = @text.to_i
884
+ when 'StorageClass'; @current_key[:storage_class] = @text
885
+ when 'ID' ; @current_key[:owner_id] = @text
886
+ when 'DisplayName' ; @current_key[:owner_display_name] = @text
887
+ when 'Contents' ; @result[:contents] << @current_key
888
+ # Common Prefix stuff
889
+ when 'CommonPrefixes' ; @result[:common_prefixes] = @common_prefixes; @in_common_prefixes = false
890
+ end
891
+ end
892
+ end
893
+
894
+ class S3BucketLocationParser < RightAWSParser # :nodoc:
895
+ def reset
896
+ @result = ''
897
+ end
898
+ def tagend(name)
899
+ @result = @text if name == 'LocationConstraint'
900
+ end
901
+ end
902
+
903
+ class S3AclParser < RightAWSParser # :nodoc:
904
+ def reset
905
+ @result = {:grantees=>[], :owner=>{}}
906
+ @current_grantee = {}
907
+ end
908
+ def tagstart(name, attributes)
909
+ @current_grantee = { :attributes => attributes } if name=='Grantee'
910
+ end
911
+ def tagend(name)
912
+ case name
913
+ # service info
914
+ when 'ID'
915
+ if @xmlpath == 'AccessControlPolicy/Owner'
916
+ @result[:owner][:id] = @text
917
+ else
918
+ @current_grantee[:id] = @text
919
+ end
920
+ when 'DisplayName'
921
+ if @xmlpath == 'AccessControlPolicy/Owner'
922
+ @result[:owner][:display_name] = @text
923
+ else
924
+ @current_grantee[:display_name] = @text
925
+ end
926
+ when 'URI'
927
+ @current_grantee[:uri] = @text
928
+ when 'Permission'
929
+ @current_grantee[:permissions] = @text
930
+ when 'Grant'
931
+ @result[:grantees] << @current_grantee
932
+ end
933
+ end
934
+ end
935
+
936
+ class S3CopyParser < RightAWSParser # :nodoc:
937
+ def reset
938
+ @result = {}
939
+ end
940
+ def tagend(name)
941
+ case name
942
+ when 'LastModified' : @result[:last_modified] = @text
943
+ when 'ETag' : @result[:e_tag] = @text
944
+ end
945
+ end
946
+ end
947
+
948
+ #-----------------------------------------------------------------
949
+ # PARSERS: Non XML
950
+ #-----------------------------------------------------------------
951
+
952
+ class S3HttpResponseParser # :nodoc:
953
+ attr_reader :result
954
+ def parse(response)
955
+ @result = response
956
+ end
957
+ def headers_to_string(headers)
958
+ result = {}
959
+ headers.each do |key, value|
960
+ value = value.to_s if value.is_a?(Array) && value.size<2
961
+ result[key] = value
962
+ end
963
+ result
964
+ end
965
+ end
966
+
967
+ class S3TrueParser < S3HttpResponseParser # :nodoc:
968
+ def parse(response)
969
+ @result = response.is_a?(Net::HTTPSuccess)
970
+ end
971
+ end
972
+
973
+ class S3HttpResponseBodyParser < S3HttpResponseParser # :nodoc:
974
+ def parse(response)
975
+ @result = {
976
+ :object => response.body,
977
+ :headers => headers_to_string(response.to_hash)
978
+ }
979
+ end
980
+ end
981
+
982
+ class S3HttpResponseHeadParser < S3HttpResponseParser # :nodoc:
983
+ def parse(response)
984
+ @result = headers_to_string(response.to_hash)
985
+ end
986
+ end
987
+
988
+ end
989
+
990
+ end