aws 2.3.8 → 2.3.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -759,7 +759,8 @@ module Aws
759
759
  @request_data = request_data
760
760
  @response = response
761
761
  msg = @errors.is_a?(Array) ? @errors.map{|code, msg| "#{code}: #{msg}"}.join("; ") : @errors.to_s
762
- msg += "\nREQUEST(#{@request_data})" unless @request_data.nil?
762
+ msg += "\nREQUEST=#{@request_data} " unless @request_data.nil?
763
+ msg += "\nREQUEST ID=#{@request_id} " unless @request_id.nil?
763
764
  super(msg)
764
765
  end
765
766
 
@@ -837,6 +838,7 @@ module Aws
837
838
  else
838
839
  msg = "#{@http_code}: REQUEST(#{@request_data})"
839
840
  end
841
+ msg += "\nREQUEST ID=#{@request_id} " unless @request_id.nil?
840
842
  super(msg)
841
843
  end
842
844
 
@@ -23,1157 +23,1209 @@
23
23
 
24
24
  module Aws
25
25
 
26
- class S3Interface < AwsBase
27
-
28
- USE_100_CONTINUE_PUT_SIZE = 1_000_000
29
-
30
- include AwsBaseInterface
31
-
32
- DEFAULT_HOST = 's3.amazonaws.com'
33
- DEFAULT_PORT = 443
34
- DEFAULT_PROTOCOL = 'https'
35
- DEFAULT_SERVICE = '/'
36
- REQUEST_TTL = 30
37
- DEFAULT_EXPIRES_AFTER = 1 * 24 * 60 * 60 # One day's worth of seconds
38
- ONE_YEAR_IN_SECONDS = 365 * 24 * 60 * 60
39
- AMAZON_HEADER_PREFIX = 'x-amz-'
40
- AMAZON_METADATA_PREFIX = 'x-amz-meta-'
41
-
42
- @@bench = AwsBenchmarkingBlock.new
43
- def self.bench_xml
44
- @@bench.xml
45
- end
46
- def self.bench_s3
47
- @@bench.service
48
- end
26
+ class S3Interface < AwsBase
49
27
 
28
+ USE_100_CONTINUE_PUT_SIZE = 1_000_000
50
29
 
51
- # Creates new RightS3 instance.
52
- #
53
- # s3 = Aws::S3Interface.new('1E3GDYEOGFJPIT7XXXXXX','hgTHt68JY07JKUY08ftHYtERkjgtfERn57XXXXXX', {:multi_thread => true, :logger => Logger.new('/tmp/x.log')}) #=> #<Aws::S3Interface:0xb7b3c27c>
54
- #
55
- # Params is a hash:
56
- #
57
- # {:server => 's3.amazonaws.com' # Amazon service host: 's3.amazonaws.com'(default)
58
- # :port => 443 # Amazon service port: 80 or 443(default)
59
- # :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default)
60
- # :multi_thread => true|false # Multi-threaded (connection per each thread): true or false(default)
61
- # :logger => Logger Object} # Logger instance: logs to STDOUT if omitted }
62
- #
63
- def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
64
- init({ :name => 'S3',
65
- :default_host => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).host : DEFAULT_HOST,
66
- :default_port => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).port : DEFAULT_PORT,
67
- :default_service => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).path : DEFAULT_SERVICE,
68
- :default_protocol => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).scheme : DEFAULT_PROTOCOL },
69
- aws_access_key_id || ENV['AWS_ACCESS_KEY_ID'],
70
- aws_secret_access_key || ENV['AWS_SECRET_ACCESS_KEY'],
71
- params)
72
- end
30
+ include AwsBaseInterface
73
31
 
32
+ DEFAULT_HOST = 's3.amazonaws.com'
33
+ DEFAULT_PORT = 443
34
+ DEFAULT_PROTOCOL = 'https'
35
+ DEFAULT_SERVICE = '/'
36
+ REQUEST_TTL = 30
37
+ DEFAULT_EXPIRES_AFTER = 1 * 24 * 60 * 60 # One day's worth of seconds
38
+ ONE_YEAR_IN_SECONDS = 365 * 24 * 60 * 60
39
+ AMAZON_HEADER_PREFIX = 'x-amz-'
40
+ AMAZON_METADATA_PREFIX = 'x-amz-meta-'
74
41
 
75
- def close_connection
76
- close_conn :s3_connection
77
- end
42
+ @@bench = AwsBenchmarkingBlock.new
78
43
 
79
- #-----------------------------------------------------------------
80
- # Requests
81
- #-----------------------------------------------------------------
82
- # Produces canonical string for signing.
83
- def canonical_string(method, path, headers={}, expires=nil) # :nodoc:
84
- s3_headers = {}
85
- headers.each do |key, value|
86
- key = key.downcase
87
- s3_headers[key] = value.join( "" ).strip if key[/^#{AMAZON_HEADER_PREFIX}|^content-md5$|^content-type$|^date$/o]
88
- end
89
- s3_headers['content-type'] ||= ''
90
- s3_headers['content-md5'] ||= ''
91
- s3_headers['date'] = '' if s3_headers.has_key? 'x-amz-date'
92
- s3_headers['date'] = expires if expires
93
- # prepare output string
94
- out_string = "#{method}\n"
95
- s3_headers.sort { |a, b| a[0] <=> b[0] }.each do |key, value|
96
- out_string << (key[/^#{AMAZON_HEADER_PREFIX}/o] ? "#{key}:#{value}\n" : "#{value}\n")
97
- end
98
- # ignore everything after the question mark...
99
- out_string << path.gsub(/\?.*$/, '')
100
- # ...unless there is an acl or torrent parameter
101
- out_string << '?acl' if path[/[&?]acl($|&|=)/]
102
- out_string << '?torrent' if path[/[&?]torrent($|&|=)/]
103
- out_string << '?location' if path[/[&?]location($|&|=)/]
104
- out_string << '?logging' if path[/[&?]logging($|&|=)/] # this one is beta, no support for now
105
- out_string
106
- end
44
+ def self.bench_xml
45
+ @@bench.xml
46
+ end
107
47
 
108
- # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/index.html?BucketRestrictions.html
109
- def is_dns_bucket?(bucket_name)
110
- bucket_name = bucket_name.to_s
111
- return nil unless (3..63) === bucket_name.size
112
- bucket_name.split('.').each do |component|
113
- return nil unless component[/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/]
114
- end
115
- true
116
- end
48
+ def self.bench_s3
49
+ @@bench.service
50
+ end
117
51
 
118
- def fetch_request_params(headers) #:nodoc:
119
- # default server to use
120
- server = @params[:server]
121
- service = @params[:service].to_s
122
- service.chop! if service[%r{/$}] # remove trailing '/' from service
123
- # extract bucket name and check it's dns compartibility
124
- headers[:url].to_s[%r{^([a-z0-9._-]*)(/[^?]*)?(\?.+)?}i]
125
- bucket_name, key_path, params_list = $1, $2, $3
126
- # select request model
127
- if is_dns_bucket?(bucket_name)
128
- # fix a path
129
- server = "#{bucket_name}.#{server}"
130
- key_path ||= '/'
131
- path = "#{service}#{key_path}#{params_list}"
132
- else
133
- path = "#{service}/#{bucket_name}#{key_path}#{params_list}"
134
- end
135
- path_to_sign = "#{service}/#{bucket_name}#{key_path}#{params_list}"
136
- # path_to_sign = "/#{bucket_name}#{key_path}#{params_list}"
137
- [ server, path, path_to_sign ]
138
- end
139
52
 
140
- # Generates request hash for REST API.
141
- # Assumes that headers[:url] is URL encoded (use CGI::escape)
142
- def generate_rest_request(method, headers) # :nodoc:
143
- # calculate request data
144
- server, path, path_to_sign = fetch_request_params(headers)
145
- data = headers[:data]
146
- # remove unset(==optional) and symbolyc keys
147
- headers.each{ |key, value| headers.delete(key) if (value.nil? || key.is_a?(Symbol)) }
148
- #
149
- headers['content-type'] ||= ''
150
- headers['date'] = Time.now.httpdate
151
- # create request
152
- request = "Net::HTTP::#{method.capitalize}".constantize.new(path)
153
- request.body = data if data
154
- # set request headers and meta headers
155
- headers.each { |key, value| request[key.to_s] = value }
156
- #generate auth strings
157
- auth_string = canonical_string(request.method, path_to_sign, request.to_hash)
158
- signature = AwsUtils::sign(@aws_secret_access_key, auth_string)
159
- # set other headers
160
- request['Authorization'] = "AWS #{@aws_access_key_id}:#{signature}"
161
- # prepare output hash
162
- { :request => request,
163
- :server => server,
164
- :port => @params[:port],
165
- :protocol => @params[:protocol] }
166
- end
167
-
168
- # Sends request to Amazon and parses the response.
169
- # Raises AwsError if any banana happened.
170
- def request_info(request, parser, &block) # :nodoc:
171
- request_info2(request, parser, @params, :s3_connection, @logger, @@bench, &block)
53
+ # Creates new RightS3 instance.
54
+ #
55
+ # s3 = Aws::S3Interface.new('1E3GDYEOGFJPIT7XXXXXX','hgTHt68JY07JKUY08ftHYtERkjgtfERn57XXXXXX', {:multi_thread => true, :logger => Logger.new('/tmp/x.log')}) #=> #<Aws::S3Interface:0xb7b3c27c>
56
+ #
57
+ # Params is a hash:
58
+ #
59
+ # {:server => 's3.amazonaws.com' # Amazon service host: 's3.amazonaws.com'(default)
60
+ # :port => 443 # Amazon service port: 80 or 443(default)
61
+ # :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default)
62
+ # :multi_thread => true|false # Multi-threaded (connection per each thread): true or false(default)
63
+ # :logger => Logger Object} # Logger instance: logs to STDOUT if omitted }
64
+ #
65
+ def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
66
+ init({:name => 'S3',
67
+ :default_host => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).host : DEFAULT_HOST,
68
+ :default_port => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).port : DEFAULT_PORT,
69
+ :default_service => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).path : DEFAULT_SERVICE,
70
+ :default_protocol => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).scheme : DEFAULT_PROTOCOL},
71
+ aws_access_key_id || ENV['AWS_ACCESS_KEY_ID'],
72
+ aws_secret_access_key || ENV['AWS_SECRET_ACCESS_KEY'],
73
+ params)
74
+ end
172
75
 
173
- end
174
76
 
77
+ def close_connection
78
+ close_conn :s3_connection
79
+ end
175
80
 
176
- # Returns an array of customer's buckets. Each item is a +hash+.
177
- #
178
- # s3.list_all_my_buckets #=>
179
- # [{:owner_id => "00000000009314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a",
180
- # :owner_display_name => "root",
181
- # :name => "bucket_name",
182
- # :creation_date => "2007-04-19T18:47:43.000Z"}, ..., {...}]
183
- #
184
- def list_all_my_buckets(headers={})
185
- req_hash = generate_rest_request('GET', headers.merge(:url=>''))
186
- request_info(req_hash, S3ListAllMyBucketsParser.new(:logger => @logger))
187
- rescue
188
- on_exception
189
- end
190
-
191
- # Creates new bucket. Returns +true+ or an exception.
192
- #
193
- # # create a bucket at American server
194
- # s3.create_bucket('my-awesome-bucket-us') #=> true
195
- # # create a bucket at European server
196
- # s3.create_bucket('my-awesome-bucket-eu', :location => :eu) #=> true
197
- #
198
- def create_bucket(bucket, headers={})
199
- data = nil
200
- unless headers[:location].blank?
201
- data = "<CreateBucketConfiguration><LocationConstraint>#{headers[:location].to_s.upcase}</LocationConstraint></CreateBucketConfiguration>"
202
- end
203
- req_hash = generate_rest_request('PUT', headers.merge(:url=>bucket, :data => data))
204
- request_info(req_hash, RightHttp2xxParser.new)
205
- rescue Exception => e
206
- # if the bucket exists AWS returns an error for the location constraint interface. Drop it
207
- e.is_a?(Aws::AwsError) && e.message.include?('BucketAlreadyOwnedByYou') ? true : on_exception
208
- end
209
-
210
- # Retrieve bucket location
211
- #
212
- # s3.create_bucket('my-awesome-bucket-us') #=> true
213
- # puts s3.bucket_location('my-awesome-bucket-us') #=> '' (Amazon's default value assumed)
214
- #
215
- # s3.create_bucket('my-awesome-bucket-eu', :location => :eu) #=> true
216
- # puts s3.bucket_location('my-awesome-bucket-eu') #=> 'EU'
217
- #
218
- def bucket_location(bucket, headers={})
219
- req_hash = generate_rest_request('GET', headers.merge(:url=>"#{bucket}?location"))
220
- request_info(req_hash, S3BucketLocationParser.new)
221
- rescue
222
- on_exception
223
- end
224
-
225
- # Retrieves the logging configuration for a bucket.
226
- # Returns a hash of {:enabled, :targetbucket, :targetprefix}
227
- #
228
- # s3.interface.get_logging_parse(:bucket => "asset_bucket")
229
- # => {:enabled=>true, :targetbucket=>"mylogbucket", :targetprefix=>"loggylogs/"}
230
- #
231
- #
232
- def get_logging_parse(params)
233
- AwsUtils.mandatory_arguments([:bucket], params)
234
- AwsUtils.allow_only([:bucket, :headers], params)
235
- params[:headers] = {} unless params[:headers]
236
- req_hash = generate_rest_request('GET', params[:headers].merge(:url=>"#{params[:bucket]}?logging"))
237
- request_info(req_hash, S3LoggingParser.new)
238
- rescue
239
- on_exception
240
- end
241
-
242
- # Sets logging configuration for a bucket from the XML configuration document.
243
- # params:
244
- # :bucket
245
- # :xmldoc
246
- def put_logging(params)
247
- AwsUtils.mandatory_arguments([:bucket,:xmldoc], params)
248
- AwsUtils.allow_only([:bucket,:xmldoc, :headers], params)
249
- params[:headers] = {} unless params[:headers]
250
- req_hash = generate_rest_request('PUT', params[:headers].merge(:url=>"#{params[:bucket]}?logging", :data => params[:xmldoc]))
251
- request_info(req_hash, S3TrueParser.new)
252
- rescue
253
- on_exception
254
- end
81
+ #-----------------------------------------------------------------
82
+ # Requests
83
+ #-----------------------------------------------------------------
84
+ # Produces canonical string for signing.
85
+ def canonical_string(method, path, headers={}, expires=nil) # :nodoc:
86
+ s3_headers = {}
87
+ headers.each do |key, value|
88
+ key = key.downcase
89
+ s3_headers[key] = value.join("").strip if key[/^#{AMAZON_HEADER_PREFIX}|^content-md5$|^content-type$|^date$/o]
90
+ end
91
+ s3_headers['content-type'] ||= ''
92
+ s3_headers['content-md5'] ||= ''
93
+ s3_headers['date'] = '' if s3_headers.has_key? 'x-amz-date'
94
+ s3_headers['date'] = expires if expires
95
+ # prepare output string
96
+ out_string = "#{method}\n"
97
+ s3_headers.sort { |a, b| a[0] <=> b[0] }.each do |key, value|
98
+ out_string << (key[/^#{AMAZON_HEADER_PREFIX}/o] ? "#{key}:#{value}\n" : "#{value}\n")
99
+ end
100
+ # ignore everything after the question mark...
101
+ out_string << path.gsub(/\?.*$/, '')
102
+ # ...unless there is an acl or torrent parameter
103
+ out_string << '?acl' if path[/[&?]acl($|&|=)/]
104
+ out_string << '?torrent' if path[/[&?]torrent($|&|=)/]
105
+ out_string << '?location' if path[/[&?]location($|&|=)/]
106
+ out_string << '?logging' if path[/[&?]logging($|&|=)/] # this one is beta, no support for now
107
+ out_string
108
+ end
255
109
 
256
- # Deletes new bucket. Bucket must be empty! Returns +true+ or an exception.
257
- #
258
- # s3.delete_bucket('my_awesome_bucket') #=> true
259
- #
260
- # See also: force_delete_bucket method
261
- #
262
- def delete_bucket(bucket, headers={})
263
- req_hash = generate_rest_request('DELETE', headers.merge(:url=>bucket))
264
- request_info(req_hash, RightHttp2xxParser.new)
265
- rescue
266
- on_exception
267
- end
268
-
269
- # Returns an array of bucket's keys. Each array item (key data) is a +hash+.
270
- #
271
- # s3.list_bucket('my_awesome_bucket', { 'prefix'=>'t', 'marker'=>'', 'max-keys'=>5, delimiter=>'' }) #=>
272
- # [{:key => "test1",
273
- # :last_modified => "2007-05-18T07:00:59.000Z",
274
- # :owner_id => "00000000009314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a",
275
- # :owner_display_name => "root",
276
- # :e_tag => "000000000059075b964b07152d234b70",
277
- # :storage_class => "STANDARD",
278
- # :size => 3,
279
- # :service=> {'is_truncated' => false,
280
- # 'prefix' => "t",
281
- # 'marker' => "",
282
- # 'name' => "my_awesome_bucket",
283
- # 'max-keys' => "5"}, ..., {...}]
284
- #
285
- def list_bucket(bucket, options={}, headers={})
286
- bucket += '?'+options.map{|k, v| "#{k.to_s}=#{CGI::escape v.to_s}"}.join('&') unless options.blank?
287
- req_hash = generate_rest_request('GET', headers.merge(:url=>bucket))
288
- request_info(req_hash, S3ListBucketParser.new(:logger => @logger))
289
- rescue
290
- on_exception
291
- end
110
+ # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/index.html?BucketRestrictions.html
111
+ def is_dns_bucket?(bucket_name)
112
+ bucket_name = bucket_name.to_s
113
+ return nil unless (3..63) === bucket_name.size
114
+ bucket_name.split('.').each do |component|
115
+ return nil unless component[/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/]
116
+ end
117
+ true
118
+ end
292
119
 
293
- # Incrementally list the contents of a bucket. Yields the following hash to a block:
294
- # s3.incrementally_list_bucket('my_awesome_bucket', { 'prefix'=>'t', 'marker'=>'', 'max-keys'=>5, delimiter=>'' }) yields
295
- # {
296
- # :name => 'bucketname',
297
- # :prefix => 'subfolder/',
298
- # :marker => 'fileN.jpg',
299
- # :max_keys => 234,
300
- # :delimiter => '/',
301
- # :is_truncated => true,
302
- # :next_marker => 'fileX.jpg',
303
- # :contents => [
304
- # { :key => "file1",
305
- # :last_modified => "2007-05-18T07:00:59.000Z",
306
- # :e_tag => "000000000059075b964b07152d234b70",
307
- # :size => 3,
308
- # :storage_class => "STANDARD",
309
- # :owner_id => "00000000009314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a",
310
- # :owner_display_name => "root"
311
- # }, { :key, ...}, ... {:key, ...}
312
- # ]
313
- # :common_prefixes => [
314
- # "prefix1",
315
- # "prefix2",
316
- # ...,
317
- # "prefixN"
318
- # ]
319
- # }
320
- def incrementally_list_bucket(bucket, options={}, headers={}, &block)
321
- internal_options = options.symbolize_keys
322
- begin
323
- internal_bucket = bucket.dup
324
- internal_bucket += '?'+internal_options.map{|k, v| "#{k.to_s}=#{CGI::escape v.to_s}"}.join('&') unless internal_options.blank?
325
- req_hash = generate_rest_request('GET', headers.merge(:url=>internal_bucket))
326
- response = request_info(req_hash, S3ImprovedListBucketParser.new(:logger => @logger))
327
- there_are_more_keys = response[:is_truncated]
328
- if(there_are_more_keys)
329
- internal_options[:marker] = decide_marker(response)
330
- total_results = response[:contents].length + response[:common_prefixes].length
331
- internal_options[:'max-keys'] ? (internal_options[:'max-keys'] -= total_results) : nil
120
+ def fetch_request_params(headers) #:nodoc:
121
+ # default server to use
122
+ server = @params[:server]
123
+ service = @params[:service].to_s
124
+ service.chop! if service[%r{/$}] # remove trailing '/' from service
125
+ # extract bucket name and check it's dns compartibility
126
+ headers[:url].to_s[%r{^([a-z0-9._-]*)(/[^?]*)?(\?.+)?}i]
127
+ bucket_name, key_path, params_list = $1, $2, $3
128
+ # select request model
129
+ if is_dns_bucket?(bucket_name)
130
+ # fix a path
131
+ server = "#{bucket_name}.#{server}"
132
+ key_path ||= '/'
133
+ path = "#{service}#{key_path}#{params_list}"
134
+ else
135
+ path = "#{service}/#{bucket_name}#{key_path}#{params_list}"
136
+ end
137
+ path_to_sign = "#{service}/#{bucket_name}#{key_path}#{params_list}"
138
+ # path_to_sign = "/#{bucket_name}#{key_path}#{params_list}"
139
+ [server, path, path_to_sign]
332
140
  end
333
- yield response
334
- end while there_are_more_keys && under_max_keys(internal_options)
335
- true
336
- rescue
337
- on_exception
338
- end
339
-
340
-
341
- private
342
- def decide_marker(response)
343
- return response[:next_marker].dup if response[:next_marker]
344
- last_key = response[:contents].last[:key]
345
- last_prefix = response[:common_prefixes].last
346
- if(!last_key)
347
- return nil if(!last_prefix)
348
- last_prefix.dup
349
- elsif(!last_prefix)
350
- last_key.dup
351
- else
352
- last_key > last_prefix ? last_key.dup : last_prefix.dup
353
- end
354
- end
355
-
356
- def under_max_keys(internal_options)
357
- internal_options[:'max-keys'] ? internal_options[:'max-keys'] > 0 : true
358
- end
359
-
360
- public
361
- # Saves object to Amazon. Returns +true+ or an exception.
362
- # Any header starting with AMAZON_METADATA_PREFIX is considered
363
- # user metadata. It will be stored with the object and returned
364
- # when you retrieve the object. The total size of the HTTP
365
- # request, not including the body, must be less than 4 KB.
366
- #
367
- # s3.put('my_awesome_bucket', 'log/current/1.log', 'Ola-la!', 'x-amz-meta-family'=>'Woho556!') #=> true
368
- #
369
- # This method is capable of 'streaming' uploads; that is, it can upload
370
- # data from a file or other IO object without first reading all the data
371
- # into memory. This is most useful for large PUTs - it is difficult to read
372
- # a 2 GB file entirely into memory before sending it to S3.
373
- # To stream an upload, pass an object that responds to 'read' (like the read
374
- # method of IO) and to either 'lstat' or 'size'. For files, this means
375
- # streaming is enabled by simply making the call:
376
- #
377
- # s3.put(bucket_name, 'S3keyname.forthisfile', File.open('localfilename.dat'))
378
- #
379
- # If the IO object you wish to stream from responds to the read method but
380
- # doesn't implement lstat or size, you can extend the object dynamically
381
- # to implement these methods, or define your own class which defines these
382
- # methods. Be sure that your class returns 'nil' from read() after having
383
- # read 'size' bytes. Otherwise S3 will drop the socket after
384
- # 'Content-Length' bytes have been uploaded, and HttpConnection will
385
- # interpret this as an error.
386
- #
387
- # This method now supports very large PUTs, where very large
388
- # is > 2 GB.
389
- #
390
- # For Win32 users: Files and IO objects should be opened in binary mode. If
391
- # a text mode IO object is passed to PUT, it will be converted to binary
392
- # mode.
393
- #
394
-
395
- def put(bucket, key, data=nil, headers={})
396
- # On Windows, if someone opens a file in text mode, we must reset it so
397
- # to binary mode for streaming to work properly
398
- if(data.respond_to?(:binmode))
399
- data.binmode
400
- end
401
- if (data.respond_to?(:lstat) && data.lstat.size >= USE_100_CONTINUE_PUT_SIZE) ||
402
- (data.respond_to?(:size) && data.size >= USE_100_CONTINUE_PUT_SIZE)
403
- headers['expect'] = '100-continue'
404
- end
405
- req_hash = generate_rest_request('PUT', headers.merge(:url=>"#{bucket}/#{CGI::escape key}", :data=>data,
406
- 'Content-Length' => ((data && data.size) || 0).to_s))
407
- request_info(req_hash, RightHttp2xxParser.new)
408
- rescue
409
- on_exception
410
- end
411
-
412
-
413
-
414
- # New experimental API for uploading objects, introduced in Aws 1.8.1.
415
- # store_object is similar in function to the older function put, but returns the full response metadata. It also allows for optional verification
416
- # of object md5 checksums on upload. Parameters are passed as hash entries and are checked for completeness as well as for spurious arguments.
417
- # The hash of the response headers contains useful information like the Amazon request ID and the object ETag (MD5 checksum).
418
- #
419
- # If the optional :md5 argument is provided, store_object verifies that the given md5 matches the md5 returned by S3. The :verified_md5 field in the response hash is
420
- # set true or false depending on the outcome of this check. If no :md5 argument is given, :verified_md5 will be false in the response.
421
- #
422
- # The optional argument of :headers allows the caller to specify arbitrary request header values.
423
- #
424
- # s3.store_object(:bucket => "foobucket", :key => "foo", :md5 => "a507841b1bc8115094b00bbe8c1b2954", :data => "polemonium" )
425
- # => {"x-amz-id-2"=>"SVsnS2nfDaR+ixyJUlRKM8GndRyEMS16+oZRieamuL61pPxPaTuWrWtlYaEhYrI/",
426
- # "etag"=>"\"a507841b1bc8115094b00bbe8c1b2954\"",
427
- # "date"=>"Mon, 29 Sep 2008 18:57:46 GMT",
428
- # :verified_md5=>true,
429
- # "x-amz-request-id"=>"63916465939995BA",
430
- # "server"=>"AmazonS3",
431
- # "content-length"=>"0"}
432
- #
433
- # s3.store_object(:bucket => "foobucket", :key => "foo", :data => "polemonium" )
434
- # => {"x-amz-id-2"=>"MAt9PLjgLX9UYJ5tV2fI/5dBZdpFjlzRVpWgBDpvZpl+V+gJFcBMW2L+LBstYpbR",
435
- # "etag"=>"\"a507841b1bc8115094b00bbe8c1b2954\"",
436
- # "date"=>"Mon, 29 Sep 2008 18:58:56 GMT",
437
- # :verified_md5=>false,
438
- # "x-amz-request-id"=>"3B25A996BC2CDD3B",
439
- # "server"=>"AmazonS3",
440
- # "content-length"=>"0"}
441
-
442
- def store_object(params)
443
- AwsUtils.allow_only([:bucket, :key, :data, :headers, :md5], params)
444
- AwsUtils.mandatory_arguments([:bucket, :key, :data], params)
445
- params[:headers] = {} unless params[:headers]
446
-
447
- params[:data].binmode if(params[:data].respond_to?(:binmode)) # On Windows, if someone opens a file in text mode, we must reset it to binary mode for streaming to work properly
448
- if (params[:data].respond_to?(:lstat) && params[:data].lstat.size >= USE_100_CONTINUE_PUT_SIZE) ||
449
- (params[:data].respond_to?(:size) && params[:data].size >= USE_100_CONTINUE_PUT_SIZE)
450
- params[:headers]['expect'] = '100-continue'
451
- end
452
-
453
- req_hash = generate_rest_request('PUT', params[:headers].merge(:url=>"#{params[:bucket]}/#{CGI::escape params[:key]}", :data=>params[:data]))
454
- resp = request_info(req_hash, S3HttpResponseHeadParser.new)
455
- if(params[:md5])
456
- resp[:verified_md5] = (resp['etag'].gsub(/\"/, '') == params[:md5]) ? true : false
457
- else
458
- resp[:verified_md5] = false
459
- end
460
- resp
461
- rescue
462
- on_exception
463
- end
464
-
465
- # Identical in function to store_object, but requires verification that the returned ETag is identical to the checksum passed in by the user as the 'md5' argument.
466
- # If the check passes, returns the response metadata with the "verified_md5" field set true. Raises an exception if the checksums conflict.
467
- # This call is implemented as a wrapper around store_object and the user may gain different semantics by creating a custom wrapper.
468
- #
469
- # s3.store_object_and_verify(:bucket => "foobucket", :key => "foo", :md5 => "a507841b1bc8115094b00bbe8c1b2954", :data => "polemonium" )
470
- # => {"x-amz-id-2"=>"IZN3XsH4FlBU0+XYkFTfHwaiF1tNzrm6dIW2EM/cthKvl71nldfVC0oVQyydzWpb",
471
- # "etag"=>"\"a507841b1bc8115094b00bbe8c1b2954\"",
472
- # "date"=>"Mon, 29 Sep 2008 18:38:32 GMT",
473
- # :verified_md5=>true,
474
- # "x-amz-request-id"=>"E8D7EA4FE00F5DF7",
475
- # "server"=>"AmazonS3",
476
- # "content-length"=>"0"}
477
- #
478
- # s3.store_object_and_verify(:bucket => "foobucket", :key => "foo", :md5 => "a507841b1bc8115094b00bbe8c1b2953", :data => "polemonium" )
479
- # Aws::AwsError: Uploaded object failed MD5 checksum verification: {"x-amz-id-2"=>"HTxVtd2bf7UHHDn+WzEH43MkEjFZ26xuYvUzbstkV6nrWvECRWQWFSx91z/bl03n",
480
- # "etag"=>"\"a507841b1bc8115094b00bbe8c1b2954\"",
481
- # "date"=>"Mon, 29 Sep 2008 18:38:41 GMT",
482
- # :verified_md5=>false,
483
- # "x-amz-request-id"=>"0D7ADE09F42606F2",
484
- # "server"=>"AmazonS3",
485
- # "content-length"=>"0"}
486
- def store_object_and_verify(params)
487
- AwsUtils.mandatory_arguments([:md5], params)
488
- r = store_object(params)
489
- r[:verified_md5] ? (return r) : (raise AwsError.new("Uploaded object failed MD5 checksum verification: #{r.inspect}"))
490
- end
491
-
492
- # Retrieves object data from Amazon. Returns a +hash+ or an exception.
493
- #
494
- # s3.get('my_awesome_bucket', 'log/curent/1.log') #=>
495
- #
496
- # {:object => "Ola-la!",
497
- # :headers => {"last-modified" => "Wed, 23 May 2007 09:08:04 GMT",
498
- # "content-type" => "",
499
- # "etag" => "\"000000000096f4ee74bc4596443ef2a4\"",
500
- # "date" => "Wed, 23 May 2007 09:08:03 GMT",
501
- # "x-amz-id-2" => "ZZZZZZZZZZZZZZZZZZZZ1HJXZoehfrS4QxcxTdNGldR7w/FVqblP50fU8cuIMLiu",
502
- # "x-amz-meta-family" => "Woho556!",
503
- # "x-amz-request-id" => "0000000C246D770C",
504
- # "server" => "AmazonS3",
505
- # "content-length" => "7"}}
506
- #
507
- # If a block is provided, yields incrementally to the block as
508
- # the response is read. For large responses, this function is ideal as
509
- # the response can be 'streamed'. The hash containing header fields is
510
- # still returned.
511
- # Example:
512
- # foo = File.new('./chunder.txt', File::CREAT|File::RDWR)
513
- # rhdr = s3.get('aws-test', 'Cent5V1_7_1.img.part.00') do |chunk|
514
- # foo.write(chunk)
515
- # end
516
- # foo.close
517
- #
518
-
519
- def get(bucket, key, headers={}, &block)
520
- req_hash = generate_rest_request('GET', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"))
521
- request_info(req_hash, S3HttpResponseBodyParser.new, &block)
522
- rescue
523
- on_exception
524
- end
525
-
526
- # New experimental API for retrieving objects, introduced in Aws 1.8.1.
527
- # retrieve_object is similar in function to the older function get. It allows for optional verification
528
- # of object md5 checksums on retrieval. Parameters are passed as hash entries and are checked for completeness as well as for spurious arguments.
529
- #
530
- # If the optional :md5 argument is provided, retrieve_object verifies that the given md5 matches the md5 returned by S3. The :verified_md5 field in the response hash is
531
- # set true or false depending on the outcome of this check. If no :md5 argument is given, :verified_md5 will be false in the response.
532
- #
533
- # The optional argument of :headers allows the caller to specify arbitrary request header values.
534
- # Mandatory arguments:
535
- # :bucket - the bucket in which the object is stored
536
- # :key - the object address (or path) within the bucket
537
- # Optional arguments:
538
- # :headers - hash of additional HTTP headers to include with the request
539
- # :md5 - MD5 checksum against which to verify the retrieved object
540
- #
541
- # s3.retrieve_object(:bucket => "foobucket", :key => "foo")
542
- # => {:verified_md5=>false,
543
- # :headers=>{"last-modified"=>"Mon, 29 Sep 2008 18:58:56 GMT",
544
- # "x-amz-id-2"=>"2Aj3TDz6HP5109qly//18uHZ2a1TNHGLns9hyAtq2ved7wmzEXDOPGRHOYEa3Qnp",
545
- # "content-type"=>"",
546
- # "etag"=>"\"a507841b1bc8115094b00bbe8c1b2954\"",
547
- # "date"=>"Tue, 30 Sep 2008 00:52:44 GMT",
548
- # "x-amz-request-id"=>"EE4855DE27A2688C",
549
- # "server"=>"AmazonS3",
550
- # "content-length"=>"10"},
551
- # :object=>"polemonium"}
552
- #
553
- # s3.retrieve_object(:bucket => "foobucket", :key => "foo", :md5=>'a507841b1bc8115094b00bbe8c1b2954')
554
- # => {:verified_md5=>true,
555
- # :headers=>{"last-modified"=>"Mon, 29 Sep 2008 18:58:56 GMT",
556
- # "x-amz-id-2"=>"mLWQcI+VuKVIdpTaPXEo84g0cz+vzmRLbj79TS8eFPfw19cGFOPxuLy4uGYVCvdH",
557
- # "content-type"=>"", "etag"=>"\"a507841b1bc8115094b00bbe8c1b2954\"",
558
- # "date"=>"Tue, 30 Sep 2008 00:53:08 GMT",
559
- # "x-amz-request-id"=>"6E7F317356580599",
560
- # "server"=>"AmazonS3",
561
- # "content-length"=>"10"},
562
- # :object=>"polemonium"}
563
- # If a block is provided, yields incrementally to the block as
564
- # the response is read. For large responses, this function is ideal as
565
- # the response can be 'streamed'. The hash containing header fields is
566
- # still returned.
567
- def retrieve_object(params, &block)
568
- AwsUtils.mandatory_arguments([:bucket, :key], params)
569
- AwsUtils.allow_only([:bucket, :key, :headers, :md5], params)
570
- params[:headers] = {} unless params[:headers]
571
- req_hash = generate_rest_request('GET', params[:headers].merge(:url=>"#{params[:bucket]}/#{CGI::escape params[:key]}"))
572
- resp = request_info(req_hash, S3HttpResponseBodyParser.new, &block)
573
- resp[:verified_md5] = false
574
- if(params[:md5] && (resp[:headers]['etag'].gsub(/\"/,'') == params[:md5]))
575
- resp[:verified_md5] = true
576
- end
577
- resp
578
- rescue
579
- on_exception
580
- end
581
-
582
- # Identical in function to retrieve_object, but requires verification that the returned ETag is identical to the checksum passed in by the user as the 'md5' argument.
583
- # If the check passes, returns the response metadata with the "verified_md5" field set true. Raises an exception if the checksums conflict.
584
- # This call is implemented as a wrapper around retrieve_object and the user may gain different semantics by creating a custom wrapper.
585
- def retrieve_object_and_verify(params, &block)
586
- AwsUtils.mandatory_arguments([:md5], params)
587
- resp = retrieve_object(params, &block)
588
- return resp if resp[:verified_md5]
589
- raise AwsError.new("Retrieved object failed MD5 checksum verification: #{resp.inspect}")
590
- end
591
141
 
592
- # Retrieves object metadata. Returns a +hash+ of http_response_headers.
593
- #
594
- # s3.head('my_awesome_bucket', 'log/curent/1.log') #=>
595
- # {"last-modified" => "Wed, 23 May 2007 09:08:04 GMT",
596
- # "content-type" => "",
597
- # "etag" => "\"000000000096f4ee74bc4596443ef2a4\"",
598
- # "date" => "Wed, 23 May 2007 09:08:03 GMT",
599
- # "x-amz-id-2" => "ZZZZZZZZZZZZZZZZZZZZ1HJXZoehfrS4QxcxTdNGldR7w/FVqblP50fU8cuIMLiu",
600
- # "x-amz-meta-family" => "Woho556!",
601
- # "x-amz-request-id" => "0000000C246D770C",
602
- # "server" => "AmazonS3",
603
- # "content-length" => "7"}
604
- #
605
- def head(bucket, key, headers={})
606
- req_hash = generate_rest_request('HEAD', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"))
607
- request_info(req_hash, S3HttpResponseHeadParser.new)
608
- rescue
609
- on_exception
610
- end
142
+ # Generates request hash for REST API.
143
+ # Assumes that headers[:url] is URL encoded (use CGI::escape)
144
+ def generate_rest_request(method, headers) # :nodoc:
145
+ # calculate request data
146
+ server, path, path_to_sign = fetch_request_params(headers)
147
+ data = headers[:data]
148
+ # remove unset(==optional) and symbolyc keys
149
+ headers.each { |key, value| headers.delete(key) if (value.nil? || key.is_a?(Symbol)) }
150
+ #
151
+ headers['content-type'] ||= ''
152
+ headers['date'] = Time.now.httpdate
153
+ # create request
154
+ request = "Net::HTTP::#{method.capitalize}".constantize.new(path)
155
+ request.body = data if data
156
+ # set request headers and meta headers
157
+ headers.each { |key, value| request[key.to_s] = value }
158
+ #generate auth strings
159
+ auth_string = canonical_string(request.method, path_to_sign, request.to_hash)
160
+ signature = AwsUtils::sign(@aws_secret_access_key, auth_string)
161
+ # set other headers
162
+ request['Authorization'] = "AWS #{@aws_access_key_id}:#{signature}"
163
+ # prepare output hash
164
+ {:request => request,
165
+ :server => server,
166
+ :port => @params[:port],
167
+ :protocol => @params[:protocol]}
168
+ end
611
169
 
612
- # Deletes key. Returns +true+ or an exception.
613
- #
614
- # s3.delete('my_awesome_bucket', 'log/curent/1.log') #=> true
615
- #
616
- def delete(bucket, key='', headers={})
617
- req_hash = generate_rest_request('DELETE', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"))
618
- request_info(req_hash, RightHttp2xxParser.new)
619
- rescue
620
- on_exception
621
- end
170
+ # Sends request to Amazon and parses the response.
171
+ # Raises AwsError if any banana happened.
172
+ def request_info(request, parser, &block) # :nodoc:
173
+ request_info2(request, parser, @params, :s3_connection, @logger, @@bench, &block)
622
174
 
623
- # Copy an object.
624
- # directive: :copy - copy meta-headers from source (default value)
625
- # :replace - replace meta-headers by passed ones
626
- #
627
- # # copy a key with meta-headers
628
- # s3.copy('b1', 'key1', 'b1', 'key1_copy') #=> {:e_tag=>"\"e8b...8d\"", :last_modified=>"2008-05-11T10:25:22.000Z"}
629
- #
630
- # # copy a key, overwrite meta-headers
631
- # 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"}
632
- #
633
- # see: http://docs.amazonwebservices.com/AmazonS3/2006-03-01/UsingCopyingObjects.html
634
- # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTObjectCOPY.html
635
- #
636
- def copy(src_bucket, src_key, dest_bucket, dest_key=nil, directive=:copy, headers={})
637
- dest_key ||= src_key
638
- headers['x-amz-metadata-directive'] = directive.to_s.upcase
639
- headers['x-amz-copy-source'] = "#{src_bucket}/#{CGI::escape src_key}"
640
- req_hash = generate_rest_request('PUT', headers.merge(:url=>"#{dest_bucket}/#{CGI::escape dest_key}"))
641
- request_info(req_hash, S3CopyParser.new)
642
- rescue
643
- on_exception
644
- end
645
-
646
- # Move an object.
647
- # directive: :copy - copy meta-headers from source (default value)
648
- # :replace - replace meta-headers by passed ones
649
- #
650
- # # move bucket1/key1 to bucket1/key2
651
- # s3.move('bucket1', 'key1', 'bucket1', 'key2') #=> {:e_tag=>"\"e8b...8d\"", :last_modified=>"2008-05-11T10:27:22.000Z"}
652
- #
653
- # # move bucket1/key1 to bucket2/key2 with new meta-headers assignment
654
- # s3.copy('bucket1', 'key1', 'bucket2', 'key2', :replace, 'x-amz-meta-family'=>'Woho555!') #=> {:e_tag=>"\"e8b...8d\"", :last_modified=>"2008-05-11T10:28:22.000Z"}
655
- #
656
- def move(src_bucket, src_key, dest_bucket, dest_key=nil, directive=:copy, headers={})
657
- copy_result = copy(src_bucket, src_key, dest_bucket, dest_key, directive, headers)
658
- # delete an original key if it differs from a destination one
659
- delete(src_bucket, src_key) unless src_bucket == dest_bucket && src_key == dest_key
660
- copy_result
661
- end
175
+ end
662
176
 
663
- # Rename an object.
664
- #
665
- # # rename bucket1/key1 to bucket1/key2
666
- # s3.rename('bucket1', 'key1', 'key2') #=> {:e_tag=>"\"e8b...8d\"", :last_modified=>"2008-05-11T10:29:22.000Z"}
667
- #
668
- def rename(src_bucket, src_key, dest_key, headers={})
669
- move(src_bucket, src_key, src_bucket, dest_key, :copy, headers)
670
- end
671
-
672
- # 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.
673
- #
674
- # s3.get_acl('my_awesome_bucket', 'log/curent/1.log') #=>
675
- # {:headers => {"x-amz-id-2"=>"B3BdDMDUz+phFF2mGBH04E46ZD4Qb9HF5PoPHqDRWBv+NVGeA3TOQ3BkVvPBjgxX",
676
- # "content-type"=>"application/xml;charset=ISO-8859-1",
677
- # "date"=>"Wed, 23 May 2007 09:40:16 GMT",
678
- # "x-amz-request-id"=>"B183FA7AB5FBB4DD",
679
- # "server"=>"AmazonS3",
680
- # "transfer-encoding"=>"chunked"},
681
- # :object => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<AccessControlPolicy xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Owner>
682
- # <ID>16144ab2929314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a</ID><DisplayName>root</DisplayName></Owner>
683
- # <AccessControlList><Grant><Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\"><ID>
684
- # 16144ab2929314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a</ID><DisplayName>root</DisplayName></Grantee>
685
- # <Permission>FULL_CONTROL</Permission></Grant></AccessControlList></AccessControlPolicy>" }
686
- #
687
- def get_acl(bucket, key='', headers={})
688
- key = key.blank? ? '' : "/#{CGI::escape key}"
689
- req_hash = generate_rest_request('GET', headers.merge(:url=>"#{bucket}#{key}?acl"))
690
- request_info(req_hash, S3HttpResponseBodyParser.new)
691
- rescue
692
- on_exception
693
- end
694
-
695
- # Retieves the ACL (access control policy) for a bucket or object.
696
- # Returns a hash of {:owner, :grantees}
697
- #
698
- # s3.get_acl_parse('my_awesome_bucket', 'log/curent/1.log') #=>
699
- #
700
- # { :grantees=>
701
- # { "16...2a"=>
702
- # { :display_name=>"root",
703
- # :permissions=>["FULL_CONTROL"],
704
- # :attributes=>
705
- # { "xsi:type"=>"CanonicalUser",
706
- # "xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance"}},
707
- # "http://acs.amazonaws.com/groups/global/AllUsers"=>
708
- # { :display_name=>"AllUsers",
709
- # :permissions=>["READ"],
710
- # :attributes=>
711
- # { "xsi:type"=>"Group",
712
- # "xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance"}}},
713
- # :owner=>
714
- # { :id=>"16..2a",
715
- # :display_name=>"root"}}
716
- #
717
- def get_acl_parse(bucket, key='', headers={})
718
- key = key.blank? ? '' : "/#{CGI::escape key}"
719
- req_hash = generate_rest_request('GET', headers.merge(:url=>"#{bucket}#{key}?acl"))
720
- acl = request_info(req_hash, S3AclParser.new(:logger => @logger))
721
- result = {}
722
- result[:owner] = acl[:owner]
723
- result[:grantees] = {}
724
- acl[:grantees].each do |grantee|
725
- key = grantee[:id] || grantee[:uri]
726
- if result[:grantees].key?(key)
727
- result[:grantees][key][:permissions] << grantee[:permissions]
728
- else
729
- result[:grantees][key] =
730
- { :display_name => grantee[:display_name] || grantee[:uri].to_s[/[^\/]*$/],
731
- :permissions => grantee[:permissions].lines.to_a,
732
- :attributes => grantee[:attributes] }
177
+
178
+ # Returns an array of customer's buckets. Each item is a +hash+.
179
+ #
180
+ # s3.list_all_my_buckets #=>
181
+ # [{:owner_id => "00000000009314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a",
182
+ # :owner_display_name => "root",
183
+ # :name => "bucket_name",
184
+ # :creation_date => "2007-04-19T18:47:43.000Z"}, ..., {...}]
185
+ #
186
+ def list_all_my_buckets(headers={})
187
+ req_hash = generate_rest_request('GET', headers.merge(:url=>''))
188
+ request_info(req_hash, S3ListAllMyBucketsParser.new(:logger => @logger))
189
+ rescue
190
+ on_exception
733
191
  end
734
- end
735
- result
736
- rescue
737
- on_exception
738
- end
739
-
740
- # Sets the ACL on a bucket or object.
741
- def put_acl(bucket, key, acl_xml_doc, headers={})
742
- key = key.blank? ? '' : "/#{CGI::escape key}"
743
- req_hash = generate_rest_request('PUT', headers.merge(:url=>"#{bucket}#{key}?acl", :data=>acl_xml_doc))
744
- request_info(req_hash, S3HttpResponseBodyParser.new)
745
- rescue
746
- on_exception
747
- end
748
-
749
- # Retieves the ACL (access control policy) for a bucket. Returns a hash of headers and xml doc with ACL data.
750
- def get_bucket_acl(bucket, headers={})
751
- return get_acl(bucket, '', headers)
752
- rescue
753
- on_exception
754
- end
755
-
756
- # Sets the ACL on a bucket only.
757
- def put_bucket_acl(bucket, acl_xml_doc, headers={})
758
- return put_acl(bucket, '', acl_xml_doc, headers)
759
- rescue
760
- on_exception
761
- end
762
192
 
193
+ # Creates new bucket. Returns +true+ or an exception.
194
+ #
195
+ # # create a bucket at American server
196
+ # s3.create_bucket('my-awesome-bucket-us') #=> true
197
+ # # create a bucket at European server
198
+ # s3.create_bucket('my-awesome-bucket-eu', :location => :eu) #=> true
199
+ #
200
+ def create_bucket(bucket, headers={})
201
+ data = nil
202
+ unless headers[:location].blank?
203
+ data = "<CreateBucketConfiguration><LocationConstraint>#{headers[:location].to_s.upcase}</LocationConstraint></CreateBucketConfiguration>"
204
+ end
205
+ req_hash = generate_rest_request('PUT', headers.merge(:url=>bucket, :data => data))
206
+ request_info(req_hash, RightHttp2xxParser.new)
207
+ rescue Exception => e
208
+ # if the bucket exists AWS returns an error for the location constraint interface. Drop it
209
+ e.is_a?(Aws::AwsError) && e.message.include?('BucketAlreadyOwnedByYou') ? true : on_exception
210
+ end
763
211
 
764
- # Removes all keys from bucket. Returns +true+ or an exception.
765
- #
766
- # s3.clear_bucket('my_awesome_bucket') #=> true
767
- #
768
- def clear_bucket(bucket)
769
- incrementally_list_bucket(bucket) do |results|
770
- results[:contents].each { |key| delete(bucket, key[:key]) }
771
- end
772
- true
773
- rescue
774
- on_exception
775
- end
776
-
777
- # Deletes all keys in bucket then deletes bucket. Returns +true+ or an exception.
778
- #
779
- # s3.force_delete_bucket('my_awesome_bucket')
780
- #
781
- def force_delete_bucket(bucket)
782
- clear_bucket(bucket)
783
- delete_bucket(bucket)
784
- rescue
785
- on_exception
786
- end
787
-
788
- # Deletes all keys where the 'folder_key' may be assumed as 'folder' name. Returns an array of string keys that have been deleted.
789
- #
790
- # s3.list_bucket('my_awesome_bucket').map{|key_data| key_data[:key]} #=> ['test','test/2/34','test/3','test1','test1/logs']
791
- # s3.delete_folder('my_awesome_bucket','test') #=> ['test','test/2/34','test/3']
792
- #
793
- def delete_folder(bucket, folder_key, separator='/')
794
- folder_key.chomp!(separator)
795
- allkeys = []
796
- incrementally_list_bucket(bucket, { 'prefix' => folder_key }) do |results|
797
- keys = results[:contents].map{ |s3_key| s3_key[:key][/^#{folder_key}($|#{separator}.*)/] ? s3_key[:key] : nil}.compact
798
- keys.each{ |key| delete(bucket, key) }
799
- allkeys << keys
800
- end
801
- allkeys
802
- rescue
803
- on_exception
804
- end
805
-
806
- # Retrieves object data only (headers are omitted). Returns +string+ or an exception.
807
- #
808
- # s3.get('my_awesome_bucket', 'log/curent/1.log') #=> 'Ola-la!'
809
- #
810
- def get_object(bucket, key, headers={})
811
- get(bucket, key, headers)[:object]
812
- rescue
813
- on_exception
814
- end
815
-
816
- #-----------------------------------------------------------------
817
- # Query API: Links
818
- #-----------------------------------------------------------------
819
-
820
- # Generates link for QUERY API
821
- def generate_link(method, headers={}, expires=nil) #:nodoc:
822
- # calculate request data
823
- server, path, path_to_sign = fetch_request_params(headers)
824
- # expiration time
825
- expires ||= DEFAULT_EXPIRES_AFTER
826
- expires = Time.now.utc + expires if expires.is_a?(Fixnum) && (expires < ONE_YEAR_IN_SECONDS)
827
- expires = expires.to_i
828
- # remove unset(==optional) and symbolyc keys
829
- headers.each{ |key, value| headers.delete(key) if (value.nil? || key.is_a?(Symbol)) }
830
- #generate auth strings
831
- auth_string = canonical_string(method, path_to_sign, headers, expires)
832
- signature = CGI::escape(Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new("sha1"), @aws_secret_access_key, auth_string)).strip)
833
- # path building
834
- addon = "Signature=#{signature}&Expires=#{expires}&AWSAccessKeyId=#{@aws_access_key_id}"
835
- path += path[/\?/] ? "&#{addon}" : "?#{addon}"
836
- "#{@params[:protocol]}://#{server}:#{@params[:port]}#{path}"
837
- rescue
838
- on_exception
839
- end
840
-
841
- # Generates link for 'ListAllMyBuckets'.
842
- #
843
- # s3.list_all_my_buckets_link #=> url string
844
- #
845
- def list_all_my_buckets_link(expires=nil, headers={})
846
- generate_link('GET', headers.merge(:url=>''), expires)
847
- rescue
848
- on_exception
849
- end
850
-
851
- # Generates link for 'CreateBucket'.
852
- #
853
- # s3.create_bucket_link('my_awesome_bucket') #=> url string
854
- #
855
- def create_bucket_link(bucket, expires=nil, headers={})
856
- generate_link('PUT', headers.merge(:url=>bucket), expires)
857
- rescue
858
- on_exception
859
- end
860
-
861
- # Generates link for 'DeleteBucket'.
862
- #
863
- # s3.delete_bucket_link('my_awesome_bucket') #=> url string
864
- #
865
- def delete_bucket_link(bucket, expires=nil, headers={})
866
- generate_link('DELETE', headers.merge(:url=>bucket), expires)
867
- rescue
868
- on_exception
869
- end
870
-
871
- # Generates link for 'ListBucket'.
872
- #
873
- # s3.list_bucket_link('my_awesome_bucket') #=> url string
874
- #
875
- def list_bucket_link(bucket, options=nil, expires=nil, headers={})
876
- bucket += '?' + options.map{|k, v| "#{k.to_s}=#{CGI::escape v.to_s}"}.join('&') unless options.blank?
877
- generate_link('GET', headers.merge(:url=>bucket), expires)
878
- rescue
879
- on_exception
880
- end
881
-
882
- # Generates link for 'PutObject'.
883
- #
884
- # s3.put_link('my_awesome_bucket',key, object) #=> url string
885
- #
886
- def put_link(bucket, key, data=nil, expires=nil, headers={})
887
- generate_link('PUT', headers.merge(:url=>"#{bucket}/#{AwsUtils::URLencode key}", :data=>data), expires)
888
- rescue
889
- on_exception
890
- end
891
-
892
- # Generates link for 'GetObject'.
893
- #
894
- # if a bucket comply with virtual hosting naming then retuns a link with the
895
- # bucket as a part of host name:
896
- #
897
- # s3.get_link('my-awesome-bucket',key) #=> https://my-awesome-bucket.s3.amazonaws.com:443/asia%2Fcustomers?Signature=nh7...
898
- #
899
- # otherwise returns an old style link (the bucket is a part of path):
900
- #
901
- # s3.get_link('my_awesome_bucket',key) #=> https://s3.amazonaws.com:443/my_awesome_bucket/asia%2Fcustomers?Signature=QAO...
902
- #
903
- # see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/VirtualHosting.html
904
- def get_link(bucket, key, expires=nil, headers={})
905
- generate_link('GET', headers.merge(:url=>"#{bucket}/#{AwsUtils::URLencode key}"), expires)
906
- rescue
907
- on_exception
908
- end
909
-
910
- # Generates link for 'HeadObject'.
911
- #
912
- # s3.head_link('my_awesome_bucket',key) #=> url string
913
- #
914
- def head_link(bucket, key, expires=nil, headers={})
915
- generate_link('HEAD', headers.merge(:url=>"#{bucket}/#{AwsUtils::URLencode key}"), expires)
916
- rescue
917
- on_exception
918
- end
919
-
920
- # Generates link for 'DeleteObject'.
921
- #
922
- # s3.delete_link('my_awesome_bucket',key) #=> url string
923
- #
924
- def delete_link(bucket, key, expires=nil, headers={})
925
- generate_link('DELETE', headers.merge(:url=>"#{bucket}/#{AwsUtils::URLencode key}"), expires)
926
- rescue
927
- on_exception
928
- end
929
-
930
-
931
- # Generates link for 'GetACL'.
932
- #
933
- # s3.get_acl_link('my_awesome_bucket',key) #=> url string
934
- #
935
- def get_acl_link(bucket, key='', headers={})
936
- return generate_link('GET', headers.merge(:url=>"#{bucket}/#{AwsUtils::URLencode key}?acl"))
937
- rescue
938
- on_exception
939
- end
940
-
941
- # Generates link for 'PutACL'.
942
- #
943
- # s3.put_acl_link('my_awesome_bucket',key) #=> url string
944
- #
945
- def put_acl_link(bucket, key='', headers={})
946
- return generate_link('PUT', headers.merge(:url=>"#{bucket}/#{AwsUtils::URLencode key}?acl"))
947
- rescue
948
- on_exception
949
- end
950
-
951
- # Generates link for 'GetBucketACL'.
952
- #
953
- # s3.get_acl_link('my_awesome_bucket',key) #=> url string
954
- #
955
- def get_bucket_acl_link(bucket, headers={})
956
- return get_acl_link(bucket, '', headers)
957
- rescue
958
- on_exception
959
- end
960
-
961
- # Generates link for 'PutBucketACL'.
962
- #
963
- # s3.put_acl_link('my_awesome_bucket',key) #=> url string
964
- #
965
- def put_bucket_acl_link(bucket, acl_xml_doc, headers={})
966
- return put_acl_link(bucket, '', acl_xml_doc, headers)
967
- rescue
968
- on_exception
969
- end
212
+ # Retrieve bucket location
213
+ #
214
+ # s3.create_bucket('my-awesome-bucket-us') #=> true
215
+ # puts s3.bucket_location('my-awesome-bucket-us') #=> '' (Amazon's default value assumed)
216
+ #
217
+ # s3.create_bucket('my-awesome-bucket-eu', :location => :eu) #=> true
218
+ # puts s3.bucket_location('my-awesome-bucket-eu') #=> 'EU'
219
+ #
220
+ def bucket_location(bucket, headers={})
221
+ req_hash = generate_rest_request('GET', headers.merge(:url=>"#{bucket}?location"))
222
+ request_info(req_hash, S3BucketLocationParser.new)
223
+ rescue
224
+ on_exception
225
+ end
970
226
 
971
- #-----------------------------------------------------------------
972
- # PARSERS:
973
- #-----------------------------------------------------------------
974
-
975
- class S3ListAllMyBucketsParser < AwsParser # :nodoc:
976
- def reset
977
- @result = []
978
- @owner = {}
979
- end
980
- def tagstart(name, attributes)
981
- @current_bucket = {} if name == 'Bucket'
982
- end
983
- def tagend(name)
984
- case name
985
- when 'ID' ; @owner[:owner_id] = @text
986
- when 'DisplayName' ; @owner[:owner_display_name] = @text
987
- when 'Name' ; @current_bucket[:name] = @text
988
- when 'CreationDate'; @current_bucket[:creation_date] = @text
989
- when 'Bucket' ; @result << @current_bucket.merge(@owner)
227
+ # Retrieves the logging configuration for a bucket.
228
+ # Returns a hash of {:enabled, :targetbucket, :targetprefix}
229
+ #
230
+ # s3.interface.get_logging_parse(:bucket => "asset_bucket")
231
+ # => {:enabled=>true, :targetbucket=>"mylogbucket", :targetprefix=>"loggylogs/"}
232
+ #
233
+ #
234
+ def get_logging_parse(params)
235
+ AwsUtils.mandatory_arguments([:bucket], params)
236
+ AwsUtils.allow_only([:bucket, :headers], params)
237
+ params[:headers] = {} unless params[:headers]
238
+ req_hash = generate_rest_request('GET', params[:headers].merge(:url=>"#{params[:bucket]}?logging"))
239
+ request_info(req_hash, S3LoggingParser.new)
240
+ rescue
241
+ on_exception
990
242
  end
991
- end
992
- end
993
243
 
994
- class S3ListBucketParser < AwsParser # :nodoc:
995
- def reset
996
- @result = []
997
- @service = {}
998
- @current_key = {}
999
- end
1000
- def tagstart(name, attributes)
1001
- @current_key = {} if name == 'Contents'
1002
- end
1003
- def tagend(name)
1004
- case name
1005
- # service info
1006
- when 'Name' ; @service['name'] = @text
1007
- when 'Prefix' ; @service['prefix'] = @text
1008
- when 'Marker' ; @service['marker'] = @text
1009
- when 'MaxKeys' ; @service['max-keys'] = @text
1010
- when 'Delimiter' ; @service['delimiter'] = @text
1011
- when 'IsTruncated' ; @service['is_truncated'] = (@text =~ /false/ ? false : true)
1012
- # key data
1013
- when 'Key' ; @current_key[:key] = @text
1014
- when 'LastModified'; @current_key[:last_modified] = @text
1015
- when 'ETag' ; @current_key[:e_tag] = @text
1016
- when 'Size' ; @current_key[:size] = @text.to_i
1017
- when 'StorageClass'; @current_key[:storage_class] = @text
1018
- when 'ID' ; @current_key[:owner_id] = @text
1019
- when 'DisplayName' ; @current_key[:owner_display_name] = @text
1020
- when 'Contents' ; @current_key[:service] = @service; @result << @current_key
244
+ # Sets logging configuration for a bucket from the XML configuration document.
245
+ # params:
246
+ # :bucket
247
+ # :xmldoc
248
+ def put_logging(params)
249
+ AwsUtils.mandatory_arguments([:bucket, :xmldoc], params)
250
+ AwsUtils.allow_only([:bucket, :xmldoc, :headers], params)
251
+ params[:headers] = {} unless params[:headers]
252
+ req_hash = generate_rest_request('PUT', params[:headers].merge(:url=>"#{params[:bucket]}?logging", :data => params[:xmldoc]))
253
+ request_info(req_hash, S3TrueParser.new)
254
+ rescue
255
+ on_exception
256
+ end
257
+
258
+ # Deletes new bucket. Bucket must be empty! Returns +true+ or an exception.
259
+ #
260
+ # s3.delete_bucket('my_awesome_bucket') #=> true
261
+ #
262
+ # See also: force_delete_bucket method
263
+ #
264
+ def delete_bucket(bucket, headers={})
265
+ req_hash = generate_rest_request('DELETE', headers.merge(:url=>bucket))
266
+ request_info(req_hash, RightHttp2xxParser.new)
267
+ rescue
268
+ on_exception
269
+ end
270
+
271
+ # Returns an array of bucket's keys. Each array item (key data) is a +hash+.
272
+ #
273
+ # s3.list_bucket('my_awesome_bucket', { 'prefix'=>'t', 'marker'=>'', 'max-keys'=>5, delimiter=>'' }) #=>
274
+ # [{:key => "test1",
275
+ # :last_modified => "2007-05-18T07:00:59.000Z",
276
+ # :owner_id => "00000000009314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a",
277
+ # :owner_display_name => "root",
278
+ # :e_tag => "000000000059075b964b07152d234b70",
279
+ # :storage_class => "STANDARD",
280
+ # :size => 3,
281
+ # :service=> {'is_truncated' => false,
282
+ # 'prefix' => "t",
283
+ # 'marker' => "",
284
+ # 'name' => "my_awesome_bucket",
285
+ # 'max-keys' => "5"}, ..., {...}]
286
+ #
287
+ def list_bucket(bucket, options={}, headers={})
288
+ bucket += '?'+options.map { |k, v| "#{k.to_s}=#{CGI::escape v.to_s}" }.join('&') unless options.blank?
289
+ req_hash = generate_rest_request('GET', headers.merge(:url=>bucket))
290
+ request_info(req_hash, S3ListBucketParser.new(:logger => @logger))
291
+ rescue
292
+ on_exception
1021
293
  end
1022
- end
1023
- end
1024
294
 
1025
- class S3ImprovedListBucketParser < AwsParser # :nodoc:
1026
- def reset
1027
- @result = {}
1028
- @result[:contents] = []
1029
- @result[:common_prefixes] = []
1030
- @contents = []
1031
- @current_key = {}
1032
- @common_prefixes = []
1033
- @in_common_prefixes = false
1034
- end
1035
- def tagstart(name, attributes)
1036
- @current_key = {} if name == 'Contents'
1037
- @in_common_prefixes = true if name == 'CommonPrefixes'
1038
- end
1039
- def tagend(name)
1040
- case name
1041
- # service info
1042
- when 'Name' ; @result[:name] = @text
1043
- # Amazon uses the same tag for the search prefix and for the entries
1044
- # in common prefix...so use our simple flag to see which element
1045
- # we are parsing
1046
- when 'Prefix' ; @in_common_prefixes ? @common_prefixes << @text : @result[:prefix] = @text
1047
- when 'Marker' ; @result[:marker] = @text
1048
- when 'MaxKeys' ; @result[:max_keys] = @text
1049
- when 'Delimiter' ; @result[:delimiter] = @text
1050
- when 'IsTruncated' ; @result[:is_truncated] = (@text =~ /false/ ? false : true)
1051
- when 'NextMarker' ; @result[:next_marker] = @text
1052
- # key data
1053
- when 'Key' ; @current_key[:key] = @text
1054
- when 'LastModified'; @current_key[:last_modified] = @text
1055
- when 'ETag' ; @current_key[:e_tag] = @text
1056
- when 'Size' ; @current_key[:size] = @text.to_i
1057
- when 'StorageClass'; @current_key[:storage_class] = @text
1058
- when 'ID' ; @current_key[:owner_id] = @text
1059
- when 'DisplayName' ; @current_key[:owner_display_name] = @text
1060
- when 'Contents' ; @result[:contents] << @current_key
1061
- # Common Prefix stuff
1062
- when 'CommonPrefixes' ; @result[:common_prefixes] = @common_prefixes; @in_common_prefixes = false
295
+ # Incrementally list the contents of a bucket. Yields the following hash to a block:
296
+ # s3.incrementally_list_bucket('my_awesome_bucket', { 'prefix'=>'t', 'marker'=>'', 'max-keys'=>5, delimiter=>'' }) yields
297
+ # {
298
+ # :name => 'bucketname',
299
+ # :prefix => 'subfolder/',
300
+ # :marker => 'fileN.jpg',
301
+ # :max_keys => 234,
302
+ # :delimiter => '/',
303
+ # :is_truncated => true,
304
+ # :next_marker => 'fileX.jpg',
305
+ # :contents => [
306
+ # { :key => "file1",
307
+ # :last_modified => "2007-05-18T07:00:59.000Z",
308
+ # :e_tag => "000000000059075b964b07152d234b70",
309
+ # :size => 3,
310
+ # :storage_class => "STANDARD",
311
+ # :owner_id => "00000000009314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a",
312
+ # :owner_display_name => "root"
313
+ # }, { :key, ...}, ... {:key, ...}
314
+ # ]
315
+ # :common_prefixes => [
316
+ # "prefix1",
317
+ # "prefix2",
318
+ # ...,
319
+ # "prefixN"
320
+ # ]
321
+ # }
322
+ def incrementally_list_bucket(bucket, options={}, headers={}, &block)
323
+ internal_options = options.symbolize_keys
324
+ begin
325
+ internal_bucket = bucket.dup
326
+ internal_bucket += '?'+internal_options.map { |k, v| "#{k.to_s}=#{CGI::escape v.to_s}" }.join('&') unless internal_options.blank?
327
+ req_hash = generate_rest_request('GET', headers.merge(:url=>internal_bucket))
328
+ response = request_info(req_hash, S3ImprovedListBucketParser.new(:logger => @logger))
329
+ there_are_more_keys = response[:is_truncated]
330
+ if (there_are_more_keys)
331
+ internal_options[:marker] = decide_marker(response)
332
+ total_results = response[:contents].length + response[:common_prefixes].length
333
+ internal_options[:'max-keys'] ? (internal_options[:'max-keys'] -= total_results) : nil
334
+ end
335
+ yield response
336
+ end while there_are_more_keys && under_max_keys(internal_options)
337
+ true
338
+ rescue
339
+ on_exception
1063
340
  end
1064
- end
1065
- end
1066
341
 
1067
- class S3BucketLocationParser < AwsParser # :nodoc:
1068
- def reset
1069
- @result = ''
1070
- end
1071
- def tagend(name)
1072
- @result = @text if name == 'LocationConstraint'
1073
- end
1074
- end
1075
342
 
1076
- class S3AclParser < AwsParser # :nodoc:
1077
- def reset
1078
- @result = {:grantees=>[], :owner=>{}}
1079
- @current_grantee = {}
1080
- end
1081
- def tagstart(name, attributes)
1082
- @current_grantee = { :attributes => attributes } if name=='Grantee'
1083
- end
1084
- def tagend(name)
1085
- case name
1086
- # service info
1087
- when 'ID'
1088
- if @xmlpath == 'AccessControlPolicy/Owner'
1089
- @result[:owner][:id] = @text
343
+ private
344
+ def decide_marker(response)
345
+ return response[:next_marker].dup if response[:next_marker]
346
+ last_key = response[:contents].last[:key]
347
+ last_prefix = response[:common_prefixes].last
348
+ if (!last_key)
349
+ return nil if (!last_prefix)
350
+ last_prefix.dup
351
+ elsif (!last_prefix)
352
+ last_key.dup
1090
353
  else
1091
- @current_grantee[:id] = @text
354
+ last_key > last_prefix ? last_key.dup : last_prefix.dup
1092
355
  end
1093
- when 'DisplayName'
1094
- if @xmlpath == 'AccessControlPolicy/Owner'
1095
- @result[:owner][:display_name] = @text
356
+ end
357
+
358
+ def under_max_keys(internal_options)
359
+ internal_options[:'max-keys'] ? internal_options[:'max-keys'] > 0 : true
360
+ end
361
+
362
+ public
363
+ # Saves object to Amazon. Returns +true+ or an exception.
364
+ # Any header starting with AMAZON_METADATA_PREFIX is considered
365
+ # user metadata. It will be stored with the object and returned
366
+ # when you retrieve the object. The total size of the HTTP
367
+ # request, not including the body, must be less than 4 KB.
368
+ #
369
+ # s3.put('my_awesome_bucket', 'log/current/1.log', 'Ola-la!', 'x-amz-meta-family'=>'Woho556!') #=> true
370
+ #
371
+ # This method is capable of 'streaming' uploads; that is, it can upload
372
+ # data from a file or other IO object without first reading all the data
373
+ # into memory. This is most useful for large PUTs - it is difficult to read
374
+ # a 2 GB file entirely into memory before sending it to S3.
375
+ # To stream an upload, pass an object that responds to 'read' (like the read
376
+ # method of IO) and to either 'lstat' or 'size'. For files, this means
377
+ # streaming is enabled by simply making the call:
378
+ #
379
+ # s3.put(bucket_name, 'S3keyname.forthisfile', File.open('localfilename.dat'))
380
+ #
381
+ # If the IO object you wish to stream from responds to the read method but
382
+ # doesn't implement lstat or size, you can extend the object dynamically
383
+ # to implement these methods, or define your own class which defines these
384
+ # methods. Be sure that your class returns 'nil' from read() after having
385
+ # read 'size' bytes. Otherwise S3 will drop the socket after
386
+ # 'Content-Length' bytes have been uploaded, and HttpConnection will
387
+ # interpret this as an error.
388
+ #
389
+ # This method now supports very large PUTs, where very large
390
+ # is > 2 GB.
391
+ #
392
+ # For Win32 users: Files and IO objects should be opened in binary mode. If
393
+ # a text mode IO object is passed to PUT, it will be converted to binary
394
+ # mode.
395
+ #
396
+
397
+ def put(bucket, key, data=nil, headers={})
398
+ # On Windows, if someone opens a file in text mode, we must reset it so
399
+ # to binary mode for streaming to work properly
400
+ if (data.respond_to?(:binmode))
401
+ data.binmode
402
+ end
403
+ data_size = data.respond_to?(:lstat) ? data.lstat.size :
404
+ (data.respond_to?(:size) ? data.size : 0)
405
+ if (data_size >= USE_100_CONTINUE_PUT_SIZE)
406
+ headers['expect'] = '100-continue'
407
+ end
408
+ req_hash = generate_rest_request('PUT', headers.merge(:url=>"#{bucket}/#{CGI::escape key}", :data=>data,
409
+ 'Content-Length' => data_size.to_s))
410
+ request_info(req_hash, RightHttp2xxParser.new)
411
+ rescue
412
+ on_exception
413
+ end
414
+
415
+
416
+ # New experimental API for uploading objects, introduced in Aws 1.8.1.
417
+ # store_object is similar in function to the older function put, but returns the full response metadata. It also allows for optional verification
418
+ # of object md5 checksums on upload. Parameters are passed as hash entries and are checked for completeness as well as for spurious arguments.
419
+ # The hash of the response headers contains useful information like the Amazon request ID and the object ETag (MD5 checksum).
420
+ #
421
+ # If the optional :md5 argument is provided, store_object verifies that the given md5 matches the md5 returned by S3. The :verified_md5 field in the response hash is
422
+ # set true or false depending on the outcome of this check. If no :md5 argument is given, :verified_md5 will be false in the response.
423
+ #
424
+ # The optional argument of :headers allows the caller to specify arbitrary request header values.
425
+ #
426
+ # s3.store_object(:bucket => "foobucket", :key => "foo", :md5 => "a507841b1bc8115094b00bbe8c1b2954", :data => "polemonium" )
427
+ # => {"x-amz-id-2"=>"SVsnS2nfDaR+ixyJUlRKM8GndRyEMS16+oZRieamuL61pPxPaTuWrWtlYaEhYrI/",
428
+ # "etag"=>"\"a507841b1bc8115094b00bbe8c1b2954\"",
429
+ # "date"=>"Mon, 29 Sep 2008 18:57:46 GMT",
430
+ # :verified_md5=>true,
431
+ # "x-amz-request-id"=>"63916465939995BA",
432
+ # "server"=>"AmazonS3",
433
+ # "content-length"=>"0"}
434
+ #
435
+ # s3.store_object(:bucket => "foobucket", :key => "foo", :data => "polemonium" )
436
+ # => {"x-amz-id-2"=>"MAt9PLjgLX9UYJ5tV2fI/5dBZdpFjlzRVpWgBDpvZpl+V+gJFcBMW2L+LBstYpbR",
437
+ # "etag"=>"\"a507841b1bc8115094b00bbe8c1b2954\"",
438
+ # "date"=>"Mon, 29 Sep 2008 18:58:56 GMT",
439
+ # :verified_md5=>false,
440
+ # "x-amz-request-id"=>"3B25A996BC2CDD3B",
441
+ # "server"=>"AmazonS3",
442
+ # "content-length"=>"0"}
443
+
444
+ def store_object(params)
445
+ AwsUtils.allow_only([:bucket, :key, :data, :headers, :md5], params)
446
+ AwsUtils.mandatory_arguments([:bucket, :key, :data], params)
447
+ params[:headers] = {} unless params[:headers]
448
+
449
+ params[:data].binmode if (params[:data].respond_to?(:binmode)) # On Windows, if someone opens a file in text mode, we must reset it to binary mode for streaming to work properly
450
+ if (params[:data].respond_to?(:lstat) && params[:data].lstat.size >= USE_100_CONTINUE_PUT_SIZE) ||
451
+ (params[:data].respond_to?(:size) && params[:data].size >= USE_100_CONTINUE_PUT_SIZE)
452
+ params[:headers]['expect'] = '100-continue'
453
+ end
454
+
455
+ req_hash = generate_rest_request('PUT', params[:headers].merge(:url=>"#{params[:bucket]}/#{CGI::escape params[:key]}", :data=>params[:data]))
456
+ resp = request_info(req_hash, S3HttpResponseHeadParser.new)
457
+ if (params[:md5])
458
+ resp[:verified_md5] = (resp['etag'].gsub(/\"/, '') == params[:md5]) ? true : false
1096
459
  else
1097
- @current_grantee[:display_name] = @text
460
+ resp[:verified_md5] = false
1098
461
  end
1099
- when 'URI'
1100
- @current_grantee[:uri] = @text
1101
- when 'Permission'
1102
- @current_grantee[:permissions] = @text
1103
- when 'Grant'
1104
- @result[:grantees] << @current_grantee
462
+ resp
463
+ rescue
464
+ on_exception
1105
465
  end
1106
- end
1107
- end
1108
-
1109
- class S3LoggingParser < AwsParser # :nodoc:
1110
- def reset
1111
- @result = {:enabled => false, :targetbucket => '', :targetprefix => ''}
1112
- @current_grantee = {}
1113
- end
1114
- def tagend(name)
1115
- case name
1116
- # service info
1117
- when 'TargetBucket'
1118
- if @xmlpath == 'BucketLoggingStatus/LoggingEnabled'
1119
- @result[:targetbucket] = @text
1120
- @result[:enabled] = true
466
+
467
+ # Identical in function to store_object, but requires verification that the returned ETag is identical to the checksum passed in by the user as the 'md5' argument.
468
+ # If the check passes, returns the response metadata with the "verified_md5" field set true. Raises an exception if the checksums conflict.
469
+ # This call is implemented as a wrapper around store_object and the user may gain different semantics by creating a custom wrapper.
470
+ #
471
+ # s3.store_object_and_verify(:bucket => "foobucket", :key => "foo", :md5 => "a507841b1bc8115094b00bbe8c1b2954", :data => "polemonium" )
472
+ # => {"x-amz-id-2"=>"IZN3XsH4FlBU0+XYkFTfHwaiF1tNzrm6dIW2EM/cthKvl71nldfVC0oVQyydzWpb",
473
+ # "etag"=>"\"a507841b1bc8115094b00bbe8c1b2954\"",
474
+ # "date"=>"Mon, 29 Sep 2008 18:38:32 GMT",
475
+ # :verified_md5=>true,
476
+ # "x-amz-request-id"=>"E8D7EA4FE00F5DF7",
477
+ # "server"=>"AmazonS3",
478
+ # "content-length"=>"0"}
479
+ #
480
+ # s3.store_object_and_verify(:bucket => "foobucket", :key => "foo", :md5 => "a507841b1bc8115094b00bbe8c1b2953", :data => "polemonium" )
481
+ # Aws::AwsError: Uploaded object failed MD5 checksum verification: {"x-amz-id-2"=>"HTxVtd2bf7UHHDn+WzEH43MkEjFZ26xuYvUzbstkV6nrWvECRWQWFSx91z/bl03n",
482
+ # "etag"=>"\"a507841b1bc8115094b00bbe8c1b2954\"",
483
+ # "date"=>"Mon, 29 Sep 2008 18:38:41 GMT",
484
+ # :verified_md5=>false,
485
+ # "x-amz-request-id"=>"0D7ADE09F42606F2",
486
+ # "server"=>"AmazonS3",
487
+ # "content-length"=>"0"}
488
+ def store_object_and_verify(params)
489
+ AwsUtils.mandatory_arguments([:md5], params)
490
+ r = store_object(params)
491
+ r[:verified_md5] ? (return r) : (raise AwsError.new("Uploaded object failed MD5 checksum verification: #{r.inspect}"))
492
+ end
493
+
494
+ # Retrieves object data from Amazon. Returns a +hash+ or an exception.
495
+ #
496
+ # s3.get('my_awesome_bucket', 'log/curent/1.log') #=>
497
+ #
498
+ # {:object => "Ola-la!",
499
+ # :headers => {"last-modified" => "Wed, 23 May 2007 09:08:04 GMT",
500
+ # "content-type" => "",
501
+ # "etag" => "\"000000000096f4ee74bc4596443ef2a4\"",
502
+ # "date" => "Wed, 23 May 2007 09:08:03 GMT",
503
+ # "x-amz-id-2" => "ZZZZZZZZZZZZZZZZZZZZ1HJXZoehfrS4QxcxTdNGldR7w/FVqblP50fU8cuIMLiu",
504
+ # "x-amz-meta-family" => "Woho556!",
505
+ # "x-amz-request-id" => "0000000C246D770C",
506
+ # "server" => "AmazonS3",
507
+ # "content-length" => "7"}}
508
+ #
509
+ # If a block is provided, yields incrementally to the block as
510
+ # the response is read. For large responses, this function is ideal as
511
+ # the response can be 'streamed'. The hash containing header fields is
512
+ # still returned.
513
+ # Example:
514
+ # foo = File.new('./chunder.txt', File::CREAT|File::RDWR)
515
+ # rhdr = s3.get('aws-test', 'Cent5V1_7_1.img.part.00') do |chunk|
516
+ # foo.write(chunk)
517
+ # end
518
+ # foo.close
519
+ #
520
+
521
+ def get(bucket, key, headers={}, &block)
522
+ req_hash = generate_rest_request('GET', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"))
523
+ request_info(req_hash, S3HttpResponseBodyParser.new, &block)
524
+ rescue
525
+ on_exception
526
+ end
527
+
528
+ # New experimental API for retrieving objects, introduced in Aws 1.8.1.
529
+ # retrieve_object is similar in function to the older function get. It allows for optional verification
530
+ # of object md5 checksums on retrieval. Parameters are passed as hash entries and are checked for completeness as well as for spurious arguments.
531
+ #
532
+ # If the optional :md5 argument is provided, retrieve_object verifies that the given md5 matches the md5 returned by S3. The :verified_md5 field in the response hash is
533
+ # set true or false depending on the outcome of this check. If no :md5 argument is given, :verified_md5 will be false in the response.
534
+ #
535
+ # The optional argument of :headers allows the caller to specify arbitrary request header values.
536
+ # Mandatory arguments:
537
+ # :bucket - the bucket in which the object is stored
538
+ # :key - the object address (or path) within the bucket
539
+ # Optional arguments:
540
+ # :headers - hash of additional HTTP headers to include with the request
541
+ # :md5 - MD5 checksum against which to verify the retrieved object
542
+ #
543
+ # s3.retrieve_object(:bucket => "foobucket", :key => "foo")
544
+ # => {:verified_md5=>false,
545
+ # :headers=>{"last-modified"=>"Mon, 29 Sep 2008 18:58:56 GMT",
546
+ # "x-amz-id-2"=>"2Aj3TDz6HP5109qly//18uHZ2a1TNHGLns9hyAtq2ved7wmzEXDOPGRHOYEa3Qnp",
547
+ # "content-type"=>"",
548
+ # "etag"=>"\"a507841b1bc8115094b00bbe8c1b2954\"",
549
+ # "date"=>"Tue, 30 Sep 2008 00:52:44 GMT",
550
+ # "x-amz-request-id"=>"EE4855DE27A2688C",
551
+ # "server"=>"AmazonS3",
552
+ # "content-length"=>"10"},
553
+ # :object=>"polemonium"}
554
+ #
555
+ # s3.retrieve_object(:bucket => "foobucket", :key => "foo", :md5=>'a507841b1bc8115094b00bbe8c1b2954')
556
+ # => {:verified_md5=>true,
557
+ # :headers=>{"last-modified"=>"Mon, 29 Sep 2008 18:58:56 GMT",
558
+ # "x-amz-id-2"=>"mLWQcI+VuKVIdpTaPXEo84g0cz+vzmRLbj79TS8eFPfw19cGFOPxuLy4uGYVCvdH",
559
+ # "content-type"=>"", "etag"=>"\"a507841b1bc8115094b00bbe8c1b2954\"",
560
+ # "date"=>"Tue, 30 Sep 2008 00:53:08 GMT",
561
+ # "x-amz-request-id"=>"6E7F317356580599",
562
+ # "server"=>"AmazonS3",
563
+ # "content-length"=>"10"},
564
+ # :object=>"polemonium"}
565
+ # If a block is provided, yields incrementally to the block as
566
+ # the response is read. For large responses, this function is ideal as
567
+ # the response can be 'streamed'. The hash containing header fields is
568
+ # still returned.
569
+ def retrieve_object(params, &block)
570
+ AwsUtils.mandatory_arguments([:bucket, :key], params)
571
+ AwsUtils.allow_only([:bucket, :key, :headers, :md5], params)
572
+ params[:headers] = {} unless params[:headers]
573
+ req_hash = generate_rest_request('GET', params[:headers].merge(:url=>"#{params[:bucket]}/#{CGI::escape params[:key]}"))
574
+ resp = request_info(req_hash, S3HttpResponseBodyParser.new, &block)
575
+ resp[:verified_md5] = false
576
+ if (params[:md5] && (resp[:headers]['etag'].gsub(/\"/, '') == params[:md5]))
577
+ resp[:verified_md5] = true
578
+ end
579
+ resp
580
+ rescue
581
+ on_exception
582
+ end
583
+
584
+ # Identical in function to retrieve_object, but requires verification that the returned ETag is identical to the checksum passed in by the user as the 'md5' argument.
585
+ # If the check passes, returns the response metadata with the "verified_md5" field set true. Raises an exception if the checksums conflict.
586
+ # This call is implemented as a wrapper around retrieve_object and the user may gain different semantics by creating a custom wrapper.
587
+ def retrieve_object_and_verify(params, &block)
588
+ AwsUtils.mandatory_arguments([:md5], params)
589
+ resp = retrieve_object(params, &block)
590
+ return resp if resp[:verified_md5]
591
+ raise AwsError.new("Retrieved object failed MD5 checksum verification: #{resp.inspect}")
592
+ end
593
+
594
+ # Retrieves object metadata. Returns a +hash+ of http_response_headers.
595
+ #
596
+ # s3.head('my_awesome_bucket', 'log/curent/1.log') #=>
597
+ # {"last-modified" => "Wed, 23 May 2007 09:08:04 GMT",
598
+ # "content-type" => "",
599
+ # "etag" => "\"000000000096f4ee74bc4596443ef2a4\"",
600
+ # "date" => "Wed, 23 May 2007 09:08:03 GMT",
601
+ # "x-amz-id-2" => "ZZZZZZZZZZZZZZZZZZZZ1HJXZoehfrS4QxcxTdNGldR7w/FVqblP50fU8cuIMLiu",
602
+ # "x-amz-meta-family" => "Woho556!",
603
+ # "x-amz-request-id" => "0000000C246D770C",
604
+ # "server" => "AmazonS3",
605
+ # "content-length" => "7"}
606
+ #
607
+ def head(bucket, key, headers={})
608
+ req_hash = generate_rest_request('HEAD', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"))
609
+ request_info(req_hash, S3HttpResponseHeadParser.new)
610
+ rescue
611
+ on_exception
612
+ end
613
+
614
+ # Deletes key. Returns +true+ or an exception.
615
+ #
616
+ # s3.delete('my_awesome_bucket', 'log/curent/1.log') #=> true
617
+ #
618
+ def delete(bucket, key='', headers={})
619
+ req_hash = generate_rest_request('DELETE', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"))
620
+ request_info(req_hash, RightHttp2xxParser.new)
621
+ rescue
622
+ on_exception
623
+ end
624
+
625
+ # Copy an object.
626
+ # directive: :copy - copy meta-headers from source (default value)
627
+ # :replace - replace meta-headers by passed ones
628
+ #
629
+ # # copy a key with meta-headers
630
+ # s3.copy('b1', 'key1', 'b1', 'key1_copy') #=> {:e_tag=>"\"e8b...8d\"", :last_modified=>"2008-05-11T10:25:22.000Z"}
631
+ #
632
+ # # copy a key, overwrite meta-headers
633
+ # 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"}
634
+ #
635
+ # see: http://docs.amazonwebservices.com/AmazonS3/2006-03-01/UsingCopyingObjects.html
636
+ # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTObjectCOPY.html
637
+ #
638
+ def copy(src_bucket, src_key, dest_bucket, dest_key=nil, directive=:copy, headers={})
639
+ dest_key ||= src_key
640
+ headers['x-amz-metadata-directive'] = directive.to_s.upcase
641
+ headers['x-amz-copy-source'] = "#{src_bucket}/#{CGI::escape src_key}"
642
+ req_hash = generate_rest_request('PUT', headers.merge(:url=>"#{dest_bucket}/#{CGI::escape dest_key}"))
643
+ request_info(req_hash, S3CopyParser.new)
644
+ rescue
645
+ on_exception
646
+ end
647
+
648
+ # Move an object.
649
+ # directive: :copy - copy meta-headers from source (default value)
650
+ # :replace - replace meta-headers by passed ones
651
+ #
652
+ # # move bucket1/key1 to bucket1/key2
653
+ # s3.move('bucket1', 'key1', 'bucket1', 'key2') #=> {:e_tag=>"\"e8b...8d\"", :last_modified=>"2008-05-11T10:27:22.000Z"}
654
+ #
655
+ # # move bucket1/key1 to bucket2/key2 with new meta-headers assignment
656
+ # s3.copy('bucket1', 'key1', 'bucket2', 'key2', :replace, 'x-amz-meta-family'=>'Woho555!') #=> {:e_tag=>"\"e8b...8d\"", :last_modified=>"2008-05-11T10:28:22.000Z"}
657
+ #
658
+ def move(src_bucket, src_key, dest_bucket, dest_key=nil, directive=:copy, headers={})
659
+ copy_result = copy(src_bucket, src_key, dest_bucket, dest_key, directive, headers)
660
+ # delete an original key if it differs from a destination one
661
+ delete(src_bucket, src_key) unless src_bucket == dest_bucket && src_key == dest_key
662
+ copy_result
663
+ end
664
+
665
+ # Rename an object.
666
+ #
667
+ # # rename bucket1/key1 to bucket1/key2
668
+ # s3.rename('bucket1', 'key1', 'key2') #=> {:e_tag=>"\"e8b...8d\"", :last_modified=>"2008-05-11T10:29:22.000Z"}
669
+ #
670
+ def rename(src_bucket, src_key, dest_key, headers={})
671
+ move(src_bucket, src_key, src_bucket, dest_key, :copy, headers)
672
+ end
673
+
674
+ # 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.
675
+ #
676
+ # s3.get_acl('my_awesome_bucket', 'log/curent/1.log') #=>
677
+ # {:headers => {"x-amz-id-2"=>"B3BdDMDUz+phFF2mGBH04E46ZD4Qb9HF5PoPHqDRWBv+NVGeA3TOQ3BkVvPBjgxX",
678
+ # "content-type"=>"application/xml;charset=ISO-8859-1",
679
+ # "date"=>"Wed, 23 May 2007 09:40:16 GMT",
680
+ # "x-amz-request-id"=>"B183FA7AB5FBB4DD",
681
+ # "server"=>"AmazonS3",
682
+ # "transfer-encoding"=>"chunked"},
683
+ # :object => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<AccessControlPolicy xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Owner>
684
+ # <ID>16144ab2929314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a</ID><DisplayName>root</DisplayName></Owner>
685
+ # <AccessControlList><Grant><Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\"><ID>
686
+ # 16144ab2929314cc309ffe736daa2b264357476c7fea6efb2c3347ac3ab2792a</ID><DisplayName>root</DisplayName></Grantee>
687
+ # <Permission>FULL_CONTROL</Permission></Grant></AccessControlList></AccessControlPolicy>" }
688
+ #
689
+ def get_acl(bucket, key='', headers={})
690
+ key = key.blank? ? '' : "/#{CGI::escape key}"
691
+ req_hash = generate_rest_request('GET', headers.merge(:url=>"#{bucket}#{key}?acl"))
692
+ request_info(req_hash, S3HttpResponseBodyParser.new)
693
+ rescue
694
+ on_exception
695
+ end
696
+
697
+ # Retieves the ACL (access control policy) for a bucket or object.
698
+ # Returns a hash of {:owner, :grantees}
699
+ #
700
+ # s3.get_acl_parse('my_awesome_bucket', 'log/curent/1.log') #=>
701
+ #
702
+ # { :grantees=>
703
+ # { "16...2a"=>
704
+ # { :display_name=>"root",
705
+ # :permissions=>["FULL_CONTROL"],
706
+ # :attributes=>
707
+ # { "xsi:type"=>"CanonicalUser",
708
+ # "xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance"}},
709
+ # "http://acs.amazonaws.com/groups/global/AllUsers"=>
710
+ # { :display_name=>"AllUsers",
711
+ # :permissions=>["READ"],
712
+ # :attributes=>
713
+ # { "xsi:type"=>"Group",
714
+ # "xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance"}}},
715
+ # :owner=>
716
+ # { :id=>"16..2a",
717
+ # :display_name=>"root"}}
718
+ #
719
+ def get_acl_parse(bucket, key='', headers={})
720
+ key = key.blank? ? '' : "/#{CGI::escape key}"
721
+ req_hash = generate_rest_request('GET', headers.merge(:url=>"#{bucket}#{key}?acl"))
722
+ acl = request_info(req_hash, S3AclParser.new(:logger => @logger))
723
+ result = {}
724
+ result[:owner] = acl[:owner]
725
+ result[:grantees] = {}
726
+ acl[:grantees].each do |grantee|
727
+ key = grantee[:id] || grantee[:uri]
728
+ if result[:grantees].key?(key)
729
+ result[:grantees][key][:permissions] << grantee[:permissions]
730
+ else
731
+ result[:grantees][key] =
732
+ {:display_name => grantee[:display_name] || grantee[:uri].to_s[/[^\/]*$/],
733
+ :permissions => grantee[:permissions].lines.to_a,
734
+ :attributes => grantee[:attributes]}
735
+ end
1121
736
  end
1122
- when 'TargetPrefix'
1123
- if @xmlpath == 'BucketLoggingStatus/LoggingEnabled'
1124
- @result[:targetprefix] = @text
1125
- @result[:enabled] = true
737
+ result
738
+ rescue
739
+ on_exception
740
+ end
741
+
742
+ # Sets the ACL on a bucket or object.
743
+ def put_acl(bucket, key, acl_xml_doc, headers={})
744
+ key = key.blank? ? '' : "/#{CGI::escape key}"
745
+ req_hash = generate_rest_request('PUT', headers.merge(:url=>"#{bucket}#{key}?acl", :data=>acl_xml_doc))
746
+ request_info(req_hash, S3HttpResponseBodyParser.new)
747
+ rescue
748
+ on_exception
749
+ end
750
+
751
+ # Retieves the ACL (access control policy) for a bucket. Returns a hash of headers and xml doc with ACL data.
752
+ def get_bucket_acl(bucket, headers={})
753
+ return get_acl(bucket, '', headers)
754
+ rescue
755
+ on_exception
756
+ end
757
+
758
+ # Sets the ACL on a bucket only.
759
+ def put_bucket_acl(bucket, acl_xml_doc, headers={})
760
+ return put_acl(bucket, '', acl_xml_doc, headers)
761
+ rescue
762
+ on_exception
763
+ end
764
+
765
+
766
+ # Removes all keys from bucket. Returns +true+ or an exception.
767
+ #
768
+ # s3.clear_bucket('my_awesome_bucket') #=> true
769
+ #
770
+ def clear_bucket(bucket)
771
+ incrementally_list_bucket(bucket) do |results|
772
+ results[:contents].each { |key| delete(bucket, key[:key]) }
1126
773
  end
774
+ true
775
+ rescue
776
+ on_exception
1127
777
  end
1128
- end
1129
- end
1130
778
 
1131
- class S3CopyParser < AwsParser # :nodoc:
1132
- def reset
1133
- @result = {}
1134
- end
1135
- def tagend(name)
1136
- case name
1137
- when 'LastModified' then @result[:last_modified] = @text
1138
- when 'ETag' then @result[:e_tag] = @text
779
+ # Deletes all keys in bucket then deletes bucket. Returns +true+ or an exception.
780
+ #
781
+ # s3.force_delete_bucket('my_awesome_bucket')
782
+ #
783
+ def force_delete_bucket(bucket)
784
+ clear_bucket(bucket)
785
+ delete_bucket(bucket)
786
+ rescue
787
+ on_exception
1139
788
  end
1140
- end
1141
- end
1142
-
1143
- #-----------------------------------------------------------------
1144
- # PARSERS: Non XML
1145
- #-----------------------------------------------------------------
1146
-
1147
- class S3HttpResponseParser # :nodoc:
1148
- attr_reader :result
1149
- def parse(response)
1150
- @result = response
1151
- end
1152
- def headers_to_string(headers)
1153
- result = {}
1154
- headers.each do |key, value|
1155
- value = value[0] if value.is_a?(Array) && value.size<2
1156
- result[key] = value
789
+
790
+ # Deletes all keys where the 'folder_key' may be assumed as 'folder' name. Returns an array of string keys that have been deleted.
791
+ #
792
+ # s3.list_bucket('my_awesome_bucket').map{|key_data| key_data[:key]} #=> ['test','test/2/34','test/3','test1','test1/logs']
793
+ # s3.delete_folder('my_awesome_bucket','test') #=> ['test','test/2/34','test/3']
794
+ #
795
+ def delete_folder(bucket, folder_key, separator='/')
796
+ folder_key.chomp!(separator)
797
+ allkeys = []
798
+ incrementally_list_bucket(bucket, {'prefix' => folder_key}) do |results|
799
+ keys = results[:contents].map { |s3_key| s3_key[:key][/^#{folder_key}($|#{separator}.*)/] ? s3_key[:key] : nil }.compact
800
+ keys.each { |key| delete(bucket, key) }
801
+ allkeys << keys
802
+ end
803
+ allkeys
804
+ rescue
805
+ on_exception
1157
806
  end
1158
- result
1159
- end
1160
- end
1161
807
 
1162
- class S3HttpResponseBodyParser < S3HttpResponseParser # :nodoc:
1163
- def parse(response)
1164
- @result = {
1165
- :object => response.body,
1166
- :headers => headers_to_string(response.to_hash)
1167
- }
1168
- end
1169
- end
808
+ # Retrieves object data only (headers are omitted). Returns +string+ or an exception.
809
+ #
810
+ # s3.get('my_awesome_bucket', 'log/curent/1.log') #=> 'Ola-la!'
811
+ #
812
+ def get_object(bucket, key, headers={})
813
+ get(bucket, key, headers)[:object]
814
+ rescue
815
+ on_exception
816
+ end
817
+
818
+ #-----------------------------------------------------------------
819
+ # Query API: Links
820
+ #-----------------------------------------------------------------
821
+
822
+ # Generates link for QUERY API
823
+ def generate_link(method, headers={}, expires=nil) #:nodoc:
824
+ # calculate request data
825
+ server, path, path_to_sign = fetch_request_params(headers)
826
+ # expiration time
827
+ expires ||= DEFAULT_EXPIRES_AFTER
828
+ expires = Time.now.utc + expires if expires.is_a?(Fixnum) && (expires < ONE_YEAR_IN_SECONDS)
829
+ expires = expires.to_i
830
+ # remove unset(==optional) and symbolyc keys
831
+ headers.each { |key, value| headers.delete(key) if (value.nil? || key.is_a?(Symbol)) }
832
+ #generate auth strings
833
+ auth_string = canonical_string(method, path_to_sign, headers, expires)
834
+ signature = CGI::escape(Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new("sha1"), @aws_secret_access_key, auth_string)).strip)
835
+ # path building
836
+ addon = "Signature=#{signature}&Expires=#{expires}&AWSAccessKeyId=#{@aws_access_key_id}"
837
+ path += path[/\?/] ? "&#{addon}" : "?#{addon}"
838
+ "#{@params[:protocol]}://#{server}:#{@params[:port]}#{path}"
839
+ rescue
840
+ on_exception
841
+ end
842
+
843
+ # Generates link for 'ListAllMyBuckets'.
844
+ #
845
+ # s3.list_all_my_buckets_link #=> url string
846
+ #
847
+ def list_all_my_buckets_link(expires=nil, headers={})
848
+ generate_link('GET', headers.merge(:url=>''), expires)
849
+ rescue
850
+ on_exception
851
+ end
852
+
853
+ # Generates link for 'CreateBucket'.
854
+ #
855
+ # s3.create_bucket_link('my_awesome_bucket') #=> url string
856
+ #
857
+ def create_bucket_link(bucket, expires=nil, headers={})
858
+ generate_link('PUT', headers.merge(:url=>bucket), expires)
859
+ rescue
860
+ on_exception
861
+ end
862
+
863
+ # Generates link for 'DeleteBucket'.
864
+ #
865
+ # s3.delete_bucket_link('my_awesome_bucket') #=> url string
866
+ #
867
+ def delete_bucket_link(bucket, expires=nil, headers={})
868
+ generate_link('DELETE', headers.merge(:url=>bucket), expires)
869
+ rescue
870
+ on_exception
871
+ end
872
+
873
+ # Generates link for 'ListBucket'.
874
+ #
875
+ # s3.list_bucket_link('my_awesome_bucket') #=> url string
876
+ #
877
+ def list_bucket_link(bucket, options=nil, expires=nil, headers={})
878
+ bucket += '?' + options.map { |k, v| "#{k.to_s}=#{CGI::escape v.to_s}" }.join('&') unless options.blank?
879
+ generate_link('GET', headers.merge(:url=>bucket), expires)
880
+ rescue
881
+ on_exception
882
+ end
883
+
884
+ # Generates link for 'PutObject'.
885
+ #
886
+ # s3.put_link('my_awesome_bucket',key, object) #=> url string
887
+ #
888
+ def put_link(bucket, key, data=nil, expires=nil, headers={})
889
+ generate_link('PUT', headers.merge(:url=>"#{bucket}/#{AwsUtils::URLencode key}", :data=>data), expires)
890
+ rescue
891
+ on_exception
892
+ end
893
+
894
+ # Generates link for 'GetObject'.
895
+ #
896
+ # if a bucket comply with virtual hosting naming then retuns a link with the
897
+ # bucket as a part of host name:
898
+ #
899
+ # s3.get_link('my-awesome-bucket',key) #=> https://my-awesome-bucket.s3.amazonaws.com:443/asia%2Fcustomers?Signature=nh7...
900
+ #
901
+ # otherwise returns an old style link (the bucket is a part of path):
902
+ #
903
+ # s3.get_link('my_awesome_bucket',key) #=> https://s3.amazonaws.com:443/my_awesome_bucket/asia%2Fcustomers?Signature=QAO...
904
+ #
905
+ # see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/VirtualHosting.html
906
+ def get_link(bucket, key, expires=nil, headers={})
907
+ generate_link('GET', headers.merge(:url=>"#{bucket}/#{AwsUtils::URLencode key}"), expires)
908
+ rescue
909
+ on_exception
910
+ end
911
+
912
+ # Generates link for 'HeadObject'.
913
+ #
914
+ # s3.head_link('my_awesome_bucket',key) #=> url string
915
+ #
916
+ def head_link(bucket, key, expires=nil, headers={})
917
+ generate_link('HEAD', headers.merge(:url=>"#{bucket}/#{AwsUtils::URLencode key}"), expires)
918
+ rescue
919
+ on_exception
920
+ end
921
+
922
+ # Generates link for 'DeleteObject'.
923
+ #
924
+ # s3.delete_link('my_awesome_bucket',key) #=> url string
925
+ #
926
+ def delete_link(bucket, key, expires=nil, headers={})
927
+ generate_link('DELETE', headers.merge(:url=>"#{bucket}/#{AwsUtils::URLencode key}"), expires)
928
+ rescue
929
+ on_exception
930
+ end
931
+
932
+
933
+ # Generates link for 'GetACL'.
934
+ #
935
+ # s3.get_acl_link('my_awesome_bucket',key) #=> url string
936
+ #
937
+ def get_acl_link(bucket, key='', headers={})
938
+ return generate_link('GET', headers.merge(:url=>"#{bucket}/#{AwsUtils::URLencode key}?acl"))
939
+ rescue
940
+ on_exception
941
+ end
942
+
943
+ # Generates link for 'PutACL'.
944
+ #
945
+ # s3.put_acl_link('my_awesome_bucket',key) #=> url string
946
+ #
947
+ def put_acl_link(bucket, key='', headers={})
948
+ return generate_link('PUT', headers.merge(:url=>"#{bucket}/#{AwsUtils::URLencode key}?acl"))
949
+ rescue
950
+ on_exception
951
+ end
952
+
953
+ # Generates link for 'GetBucketACL'.
954
+ #
955
+ # s3.get_acl_link('my_awesome_bucket',key) #=> url string
956
+ #
957
+ def get_bucket_acl_link(bucket, headers={})
958
+ return get_acl_link(bucket, '', headers)
959
+ rescue
960
+ on_exception
961
+ end
962
+
963
+ # Generates link for 'PutBucketACL'.
964
+ #
965
+ # s3.put_acl_link('my_awesome_bucket',key) #=> url string
966
+ #
967
+ def put_bucket_acl_link(bucket, acl_xml_doc, headers={})
968
+ return put_acl_link(bucket, '', acl_xml_doc, headers)
969
+ rescue
970
+ on_exception
971
+ end
972
+
973
+ #-----------------------------------------------------------------
974
+ # PARSERS:
975
+ #-----------------------------------------------------------------
976
+
977
+ class S3ListAllMyBucketsParser < AwsParser # :nodoc:
978
+ def reset
979
+ @result = []
980
+ @owner = {}
981
+ end
982
+
983
+ def tagstart(name, attributes)
984
+ @current_bucket = {} if name == 'Bucket'
985
+ end
986
+
987
+ def tagend(name)
988
+ case name
989
+ when 'ID';
990
+ @owner[:owner_id] = @text
991
+ when 'DisplayName';
992
+ @owner[:owner_display_name] = @text
993
+ when 'Name';
994
+ @current_bucket[:name] = @text
995
+ when 'CreationDate';
996
+ @current_bucket[:creation_date] = @text
997
+ when 'Bucket';
998
+ @result << @current_bucket.merge(@owner)
999
+ end
1000
+ end
1001
+ end
1002
+
1003
+ class S3ListBucketParser < AwsParser # :nodoc:
1004
+ def reset
1005
+ @result = []
1006
+ @service = {}
1007
+ @current_key = {}
1008
+ end
1009
+
1010
+ def tagstart(name, attributes)
1011
+ @current_key = {} if name == 'Contents'
1012
+ end
1013
+
1014
+ def tagend(name)
1015
+ case name
1016
+ # service info
1017
+ when 'Name';
1018
+ @service['name'] = @text
1019
+ when 'Prefix';
1020
+ @service['prefix'] = @text
1021
+ when 'Marker';
1022
+ @service['marker'] = @text
1023
+ when 'MaxKeys';
1024
+ @service['max-keys'] = @text
1025
+ when 'Delimiter';
1026
+ @service['delimiter'] = @text
1027
+ when 'IsTruncated';
1028
+ @service['is_truncated'] = (@text =~ /false/ ? false : true)
1029
+ # key data
1030
+ when 'Key';
1031
+ @current_key[:key] = @text
1032
+ when 'LastModified';
1033
+ @current_key[:last_modified] = @text
1034
+ when 'ETag';
1035
+ @current_key[:e_tag] = @text
1036
+ when 'Size';
1037
+ @current_key[:size] = @text.to_i
1038
+ when 'StorageClass';
1039
+ @current_key[:storage_class] = @text
1040
+ when 'ID';
1041
+ @current_key[:owner_id] = @text
1042
+ when 'DisplayName';
1043
+ @current_key[:owner_display_name] = @text
1044
+ when 'Contents';
1045
+ @current_key[:service] = @service; @result << @current_key
1046
+ end
1047
+ end
1048
+ end
1049
+
1050
+ class S3ImprovedListBucketParser < AwsParser # :nodoc:
1051
+ def reset
1052
+ @result = {}
1053
+ @result[:contents] = []
1054
+ @result[:common_prefixes] = []
1055
+ @contents = []
1056
+ @current_key = {}
1057
+ @common_prefixes = []
1058
+ @in_common_prefixes = false
1059
+ end
1060
+
1061
+ def tagstart(name, attributes)
1062
+ @current_key = {} if name == 'Contents'
1063
+ @in_common_prefixes = true if name == 'CommonPrefixes'
1064
+ end
1065
+
1066
+ def tagend(name)
1067
+ case name
1068
+ # service info
1069
+ when 'Name';
1070
+ @result[:name] = @text
1071
+ # Amazon uses the same tag for the search prefix and for the entries
1072
+ # in common prefix...so use our simple flag to see which element
1073
+ # we are parsing
1074
+ when 'Prefix';
1075
+ @in_common_prefixes ? @common_prefixes << @text : @result[:prefix] = @text
1076
+ when 'Marker';
1077
+ @result[:marker] = @text
1078
+ when 'MaxKeys';
1079
+ @result[:max_keys] = @text
1080
+ when 'Delimiter';
1081
+ @result[:delimiter] = @text
1082
+ when 'IsTruncated';
1083
+ @result[:is_truncated] = (@text =~ /false/ ? false : true)
1084
+ when 'NextMarker';
1085
+ @result[:next_marker] = @text
1086
+ # key data
1087
+ when 'Key';
1088
+ @current_key[:key] = @text
1089
+ when 'LastModified';
1090
+ @current_key[:last_modified] = @text
1091
+ when 'ETag';
1092
+ @current_key[:e_tag] = @text
1093
+ when 'Size';
1094
+ @current_key[:size] = @text.to_i
1095
+ when 'StorageClass';
1096
+ @current_key[:storage_class] = @text
1097
+ when 'ID';
1098
+ @current_key[:owner_id] = @text
1099
+ when 'DisplayName';
1100
+ @current_key[:owner_display_name] = @text
1101
+ when 'Contents';
1102
+ @result[:contents] << @current_key
1103
+ # Common Prefix stuff
1104
+ when 'CommonPrefixes';
1105
+ @result[:common_prefixes] = @common_prefixes; @in_common_prefixes = false
1106
+ end
1107
+ end
1108
+ end
1109
+
1110
+ class S3BucketLocationParser < AwsParser # :nodoc:
1111
+ def reset
1112
+ @result = ''
1113
+ end
1114
+
1115
+ def tagend(name)
1116
+ @result = @text if name == 'LocationConstraint'
1117
+ end
1118
+ end
1119
+
1120
+ class S3AclParser < AwsParser # :nodoc:
1121
+ def reset
1122
+ @result = {:grantees=>[], :owner=>{}}
1123
+ @current_grantee = {}
1124
+ end
1125
+
1126
+ def tagstart(name, attributes)
1127
+ @current_grantee = {:attributes => attributes} if name=='Grantee'
1128
+ end
1129
+
1130
+ def tagend(name)
1131
+ case name
1132
+ # service info
1133
+ when 'ID'
1134
+ if @xmlpath == 'AccessControlPolicy/Owner'
1135
+ @result[:owner][:id] = @text
1136
+ else
1137
+ @current_grantee[:id] = @text
1138
+ end
1139
+ when 'DisplayName'
1140
+ if @xmlpath == 'AccessControlPolicy/Owner'
1141
+ @result[:owner][:display_name] = @text
1142
+ else
1143
+ @current_grantee[:display_name] = @text
1144
+ end
1145
+ when 'URI'
1146
+ @current_grantee[:uri] = @text
1147
+ when 'Permission'
1148
+ @current_grantee[:permissions] = @text
1149
+ when 'Grant'
1150
+ @result[:grantees] << @current_grantee
1151
+ end
1152
+ end
1153
+ end
1154
+
1155
+ class S3LoggingParser < AwsParser # :nodoc:
1156
+ def reset
1157
+ @result = {:enabled => false, :targetbucket => '', :targetprefix => ''}
1158
+ @current_grantee = {}
1159
+ end
1160
+
1161
+ def tagend(name)
1162
+ case name
1163
+ # service info
1164
+ when 'TargetBucket'
1165
+ if @xmlpath == 'BucketLoggingStatus/LoggingEnabled'
1166
+ @result[:targetbucket] = @text
1167
+ @result[:enabled] = true
1168
+ end
1169
+ when 'TargetPrefix'
1170
+ if @xmlpath == 'BucketLoggingStatus/LoggingEnabled'
1171
+ @result[:targetprefix] = @text
1172
+ @result[:enabled] = true
1173
+ end
1174
+ end
1175
+ end
1176
+ end
1177
+
1178
+ class S3CopyParser < AwsParser # :nodoc:
1179
+ def reset
1180
+ @result = {}
1181
+ end
1182
+
1183
+ def tagend(name)
1184
+ case name
1185
+ when 'LastModified' then
1186
+ @result[:last_modified] = @text
1187
+ when 'ETag' then
1188
+ @result[:e_tag] = @text
1189
+ end
1190
+ end
1191
+ end
1192
+
1193
+ #-----------------------------------------------------------------
1194
+ # PARSERS: Non XML
1195
+ #-----------------------------------------------------------------
1196
+
1197
+ class S3HttpResponseParser # :nodoc:
1198
+ attr_reader :result
1199
+
1200
+ def parse(response)
1201
+ @result = response
1202
+ end
1203
+
1204
+ def headers_to_string(headers)
1205
+ result = {}
1206
+ headers.each do |key, value|
1207
+ value = value[0] if value.is_a?(Array) && value.size<2
1208
+ result[key] = value
1209
+ end
1210
+ result
1211
+ end
1212
+ end
1213
+
1214
+ class S3HttpResponseBodyParser < S3HttpResponseParser # :nodoc:
1215
+ def parse(response)
1216
+ @result = {
1217
+ :object => response.body,
1218
+ :headers => headers_to_string(response.to_hash)
1219
+ }
1220
+ end
1221
+ end
1222
+
1223
+ class S3HttpResponseHeadParser < S3HttpResponseParser # :nodoc:
1224
+ def parse(response)
1225
+ @result = headers_to_string(response.to_hash)
1226
+ end
1227
+ end
1170
1228
 
1171
- class S3HttpResponseHeadParser < S3HttpResponseParser # :nodoc:
1172
- def parse(response)
1173
- @result = headers_to_string(response.to_hash)
1174
- end
1175
1229
  end
1176
-
1177
- end
1178
1230
 
1179
1231
  end