aws 2.3.8 → 2.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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