kerryb-right_aws 1.7.3

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.
@@ -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