radosgw-s3 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ module S3
2
+ module BucketsExtension
3
+ # Builds new bucket with given name
4
+ def build(name)
5
+ Bucket.send(:new, proxy_owner, name)
6
+ end
7
+
8
+ # Finds the bucket with given name (only those which exist and You have access to it)
9
+ # return nil in case Error::NoSuchBucket or Error::ForbiddenBucket
10
+ def find_first(name)
11
+ bucket = build(name)
12
+ bucket.retrieve
13
+ rescue Error::ForbiddenBucket, Error::NoSuchBucket
14
+ nil
15
+ end
16
+ alias :find :find_first
17
+
18
+ # Finds all buckets in the service
19
+ def find_all
20
+ proxy_target
21
+ end
22
+
23
+ # Destroys all buckets in the service. Doesn't destroy non-empty
24
+ # buckets by default, pass true to force destroy (USE WITH CARE!).
25
+ def destroy_all(force = false)
26
+ proxy_target.each { |bucket| bucket.destroy(force) }
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,225 @@
1
+ module S3
2
+
3
+ # Class responsible for handling connections to amazon hosts
4
+ class Connection
5
+ include Parser
6
+
7
+ attr_accessor :access_key_id, :secret_access_key, :use_ssl, :timeout, :debug, :proxy, :host
8
+ alias :use_ssl? :use_ssl
9
+
10
+ # Creates new connection object.
11
+ #
12
+ # ==== Options
13
+ # * <tt>:access_key_id</tt> - Access key id (REQUIRED)
14
+ # * <tt>:secret_access_key</tt> - Secret access key (REQUIRED)
15
+ # * <tt>:use_ssl</tt> - Use https or http protocol (false by
16
+ # default)
17
+ # * <tt>:debug</tt> - Display debug information on the STDOUT
18
+ # (false by default)
19
+ # * <tt>:timeout</tt> - Timeout to use by the Net::HTTP object
20
+ # (60 by default)
21
+ # * <tt>:proxy</tt> - Hash for Net::HTTP Proxy settings
22
+ # { :host => "proxy.mydomain.com", :port => "80, :user => "user_a", :password => "secret" }
23
+ # * <tt>:chunk_size</tt> - Size of a chunk when streaming
24
+ # (1048576 (1 MiB) by default)
25
+ def initialize(options = {})
26
+ @access_key_id = options.fetch(:access_key_id)
27
+ @secret_access_key = options.fetch(:secret_access_key)
28
+ @host = options.fetch(:host)
29
+ @use_ssl = options.fetch(:use_ssl, false)
30
+ @debug = options.fetch(:debug, false)
31
+ @timeout = options.fetch(:timeout, 60)
32
+ @proxy = options.fetch(:proxy, nil)
33
+ @chunk_size = options.fetch(:chunk_size, 1048576)
34
+ end
35
+
36
+ # Makes request with given HTTP method, sets missing parameters,
37
+ # adds signature to request header and returns response object
38
+ # (Net::HTTPResponse)
39
+ #
40
+ # ==== Parameters
41
+ # * <tt>method</tt> - HTTP Method symbol, can be <tt>:get</tt>,
42
+ # <tt>:put</tt>, <tt>:delete</tt>
43
+ #
44
+ # ==== Options:
45
+ # * <tt>:host</tt> - Hostname to connecto to, defaults
46
+ # to <tt>s3.amazonaws.com</tt>
47
+ # * <tt>:path</tt> - path to send request to (REQUIRED)
48
+ # * <tt>:body</tt> - Request body, only meaningful for
49
+ # <tt>:put</tt> request
50
+ # * <tt>:params</tt> - Parameters to add to query string for
51
+ # request, can be String or Hash
52
+ # * <tt>:headers</tt> - Hash of headers fields to add to request
53
+ # header
54
+ #
55
+ # ==== Returns
56
+ # Net::HTTPResponse object -- response from the server
57
+ def request(method, options)
58
+ host = @host
59
+ path = options.fetch(:path)
60
+ body = options.fetch(:body, nil)
61
+ params = options.fetch(:params, {})
62
+ headers = options.fetch(:headers, {})
63
+
64
+ # Must be done before adding params
65
+ # Encodes all characters except forward-slash (/) and explicitly legal URL characters
66
+ path = URI.escape(path, /[^#{URI::REGEXP::PATTERN::UNRESERVED}\/]/)
67
+
68
+ if params
69
+ params = params.is_a?(String) ? params : self.class.parse_params(params)
70
+ path << "?#{params}"
71
+ end
72
+
73
+ request = Request.new(@chunk_size, method.to_s.upcase, !!body, method.to_s.upcase != "HEAD", path)
74
+
75
+ headers = self.class.parse_headers(headers)
76
+ headers.each do |key, value|
77
+ request[key] = value
78
+ end
79
+
80
+ if body
81
+ if body.respond_to?(:read)
82
+ request.body_stream = body
83
+ else
84
+ request.body = body
85
+ end
86
+ request.content_length = body.respond_to?(:lstat) ? body.stat.size : body.size
87
+ end
88
+
89
+ send_request(host, request)
90
+ end
91
+
92
+ # Helper function to parser parameters and create single string of
93
+ # params added to questy string
94
+ #
95
+ # ==== Parameters
96
+ # * <tt>params</tt> - Hash of parameters
97
+ #
98
+ # ==== Returns
99
+ # String -- containing all parameters joined in one params string,
100
+ # i.e. <tt>param1=val&param2&param3=0</tt>
101
+ def self.parse_params(params)
102
+ interesting_keys = [:max_keys, :prefix, :marker, :delimiter, :location]
103
+
104
+ result = []
105
+ params.each do |key, value|
106
+ if interesting_keys.include?(key)
107
+ parsed_key = key.to_s.gsub("_", "-")
108
+ case value
109
+ when nil
110
+ result << parsed_key
111
+ else
112
+ result << "#{parsed_key}=#{value}"
113
+ end
114
+ end
115
+ end
116
+ result.join("&")
117
+ end
118
+
119
+ # Helper function to change headers from symbols, to in correct
120
+ # form (i.e. with '-' instead of '_')
121
+ #
122
+ # ==== Parameters
123
+ # * <tt>headers</tt> - Hash of pairs <tt>headername => value</tt>,
124
+ # where value can be Range (for Range header) or any other value
125
+ # which can be translated to string
126
+ #
127
+ # ==== Returns
128
+ # Hash of headers translated from symbol to string, containing
129
+ # only interesting headers
130
+ def self.parse_headers(headers)
131
+ interesting_keys = [:content_type, :content_length, :cache_control, :x_amz_acl, :x_amz_storage_class, :range,
132
+ :if_modified_since, :if_unmodified_since,
133
+ :if_match, :if_none_match,
134
+ :content_disposition, :content_encoding,
135
+ :x_amz_copy_source, :x_amz_metadata_directive,
136
+ :x_amz_copy_source_if_match,
137
+ :x_amz_copy_source_if_none_match,
138
+ :x_amz_copy_source_if_unmodified_since,
139
+ :x_amz_copy_source_if_modified_since]
140
+
141
+ parsed_headers = {}
142
+ if headers
143
+ headers.each do |key, value|
144
+ if interesting_keys.include?(key)
145
+ parsed_key = key.to_s.gsub("_", "-")
146
+ parsed_value = value
147
+ case value
148
+ when Range
149
+ parsed_value = "bytes=#{value.first}-#{value.last}"
150
+ end
151
+ parsed_headers[parsed_key] = parsed_value
152
+ end
153
+ end
154
+ end
155
+ parsed_headers
156
+ end
157
+
158
+ private
159
+
160
+ def port
161
+ use_ssl ? 443 : 80
162
+ end
163
+
164
+ def proxy_settings
165
+ @proxy.values_at(:host, :port, :user, :password) unless @proxy.nil? || @proxy.empty?
166
+ end
167
+
168
+ def http(host)
169
+ http = Net::HTTP.new(host, port, *proxy_settings)
170
+ http.set_debug_output(STDOUT) if @debug
171
+ http.use_ssl = @use_ssl
172
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @use_ssl
173
+ http.read_timeout = @timeout if @timeout
174
+ http
175
+ end
176
+
177
+ def send_request(host, request, skip_authorization = false)
178
+ response = http(host).start do |http|
179
+ host = http.address
180
+
181
+ request["Date"] ||= Time.now.httpdate
182
+
183
+ if request.body
184
+ request["Content-Type"] ||= "application/octet-stream"
185
+ request["Content-MD5"] = Base64.encode64(Digest::MD5.digest(request.body)).chomp unless request.body.empty?
186
+ end
187
+
188
+ unless skip_authorization
189
+ request["Authorization"] = Signature.generate(:host => host,
190
+ :request => request,
191
+ :access_key_id => access_key_id,
192
+ :secret_access_key => secret_access_key)
193
+ end
194
+
195
+ http.request(request)
196
+ end
197
+
198
+ if response.code.to_i == 307
199
+ if response.body
200
+ doc = Document.new response.body
201
+ send_request(doc.elements["Error"].elements["Endpoint"].text, request, true)
202
+ end
203
+ else
204
+ handle_response(response)
205
+ end
206
+ end
207
+
208
+ def handle_response(response)
209
+ case response.code.to_i
210
+ when 200...300
211
+ response
212
+ when 300...600
213
+ if response.body.nil? || response.body.empty?
214
+ raise Error::ResponseError.new(nil, response)
215
+ else
216
+ code, message = parse_error(response.body)
217
+ raise Error::ResponseError.exception(code).new(message, response)
218
+ end
219
+ else
220
+ raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
221
+ end
222
+ response
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,111 @@
1
+ module S3
2
+ module Error
3
+
4
+ # All responses with a code between 300 and 599 that contain an
5
+ # <Error></Error> body are wrapped in an ErrorResponse which
6
+ # contains an Error object. This Error class generates a custom
7
+ # exception with the name of the xml Error and its message. All
8
+ # such runtime generated exception classes descend from
9
+ # ResponseError and contain the ErrorResponse object so that all
10
+ # code that makes a request can rescue ResponseError and get
11
+ # access to the ErrorResponse.
12
+ class ResponseError < StandardError
13
+ attr_reader :response
14
+
15
+ # Creates new S3::ResponseError.
16
+ #
17
+ # ==== Parameters
18
+ # * <tt>message</tt> - what went wrong
19
+ # * <tt>response</tt> - Net::HTTPResponse object or nil
20
+ def initialize(message, response)
21
+ @response = response
22
+ super(message)
23
+ end
24
+
25
+ # Factory for all other Exception classes in module, each for
26
+ # every error response available from AmazonAWS
27
+ #
28
+ # ==== Parameters
29
+ # * <tt>code</tt> - Code name of exception
30
+ #
31
+ # ==== Returns
32
+ # Descendant of ResponseError suitable for that exception code
33
+ # or ResponseError class if no class found
34
+ def self.exception(code)
35
+ S3::Error.const_get(code)
36
+ rescue NameError
37
+ ResponseError
38
+ end
39
+ end
40
+
41
+ #:stopdoc:
42
+
43
+ class AccessDenied < ResponseError; end
44
+ class AccountProblem < ResponseError; end
45
+ class AmbiguousGrantByEmailAddress < ResponseError; end
46
+ class BadDigest < ResponseError; end
47
+ class BucketAlreadyExists < ResponseError; end
48
+ class BucketAlreadyOwnedByYou < ResponseError; end
49
+ class BucketNotEmpty < ResponseError; end
50
+ class CredentialsNotSupported < ResponseError; end
51
+ class CrossLocationLoggingProhibited < ResponseError; end
52
+ class EntityTooSmall < ResponseError; end
53
+ class EntityTooLarge < ResponseError; end
54
+ class ExpiredToken < ResponseError; end
55
+ class ForbiddenBucket < ResponseError; end
56
+ class IncompleteBody < ResponseError; end
57
+ class IncorrectNumberOfFilesInPostRequestPOST < ResponseError; end
58
+ class InlineDataTooLarge < ResponseError; end
59
+ class InternalError < ResponseError; end
60
+ class InvalidAccessKeyId < ResponseError; end
61
+ class InvalidAddressingHeader < ResponseError; end
62
+ class InvalidArgument < ResponseError; end
63
+ class InvalidBucketName < ResponseError; end
64
+ class InvalidDigest < ResponseError; end
65
+ class InvalidLocationConstraint < ResponseError; end
66
+ class InvalidPayer < ResponseError; end
67
+ class InvalidPolicyDocument < ResponseError; end
68
+ class InvalidRange < ResponseError; end
69
+ class InvalidSecurity < ResponseError; end
70
+ class InvalidSOAPRequest < ResponseError; end
71
+ class InvalidStorageClass < ResponseError; end
72
+ class InvalidTargetBucketForLogging < ResponseError; end
73
+ class InvalidToken < ResponseError; end
74
+ class InvalidURI < ResponseError; end
75
+ class KeyTooLong < ResponseError; end
76
+ class MalformedACLError < ResponseError; end
77
+ class MalformedACLError < ResponseError; end
78
+ class MalformedPOSTRequest < ResponseError; end
79
+ class MalformedXML < ResponseError; end
80
+ class MaxMessageLengthExceeded < ResponseError; end
81
+ class MaxPostPreDataLengthExceededErrorYour < ResponseError; end
82
+ class MetadataTooLarge < ResponseError; end
83
+ class MethodNotAllowed < ResponseError; end
84
+ class MissingAttachment < ResponseError; end
85
+ class MissingContentLength < ResponseError; end
86
+ class MissingRequestBodyError < ResponseError; end
87
+ class MissingSecurityElement < ResponseError; end
88
+ class MissingSecurityHeader < ResponseError; end
89
+ class NoLoggingStatusForKey < ResponseError; end
90
+ class NoSuchBucket < ResponseError; end
91
+ class NoSuchKey < ResponseError; end
92
+ class NotImplemented < ResponseError; end
93
+ class NotSignedUp < ResponseError; end
94
+ class OperationAborted < ResponseError; end
95
+ class PermanentRedirect < ResponseError; end
96
+ class PreconditionFailed < ResponseError; end
97
+ class Redirect < ResponseError; end
98
+ class RequestIsNotMultiPartContent < ResponseError; end
99
+ class RequestTimeout < ResponseError; end
100
+ class RequestTimeTooSkewed < ResponseError; end
101
+ class RequestTorrentOfBucketError < ResponseError; end
102
+ class SignatureDoesNotMatch < ResponseError; end
103
+ class SlowDown < ResponseError; end
104
+ class TemporaryRedirect < ResponseError; end
105
+ class TokenRefreshRequired < ResponseError; end
106
+ class TooManyBuckets < ResponseError; end
107
+ class UnexpectedContent < ResponseError; end
108
+ class UnresolvableGrantByEmailAddress < ResponseError; end
109
+ class UserKeyMustBeSpecified < ResponseError; end
110
+ end
111
+ end
data/lib/s3/object.rb ADDED
@@ -0,0 +1,262 @@
1
+ module S3
2
+
3
+ # Class responsible for handling objects stored in S3 buckets
4
+ class Object
5
+ include Parser
6
+ extend Forwardable
7
+
8
+ attr_accessor :content_type, :content_disposition, :content_encoding, :cache_control
9
+ attr_reader :last_modified, :etag, :size, :bucket, :key, :acl, :storage_class, :metadata
10
+ attr_writer :content
11
+
12
+ def_instance_delegators :bucket, :name, :service, :bucket_request, :vhost?, :host, :path_prefix
13
+ def_instance_delegators :service, :protocol, :port, :secret_access_key
14
+ private_class_method :new
15
+
16
+ # Compares the object with other object. Returns true if the key
17
+ # of the objects are the same, and both have the same buckets (see
18
+ # Bucket equality)
19
+ def ==(other)
20
+ other.equal?(self) || (other.instance_of?(self.class) && self.key == other.key && self.bucket == other.bucket)
21
+ end
22
+
23
+ # Returns full key of the object: e.g. <tt>bucket-name/object/key.ext</tt>
24
+ def full_key
25
+ [name, key].join("/")
26
+ end
27
+
28
+ # Assigns a new +key+ to the object, raises ArgumentError if given
29
+ # key is not valid key name
30
+ def key=(key)
31
+ raise ArgumentError.new("Invalid key name: #{key}") unless key_valid?(key)
32
+ @key ||= key
33
+ end
34
+
35
+ # Assigns a new ACL to the object. Please note that ACL is not
36
+ # retrieved from the server and set to "private" by default.
37
+ #
38
+ # ==== Example
39
+ # object.acl = :public_read
40
+ def acl=(acl)
41
+ @acl = acl.to_s.gsub("_", "-") if acl
42
+ end
43
+
44
+ # Assigns a new storage class (RRS) to the object. Please note
45
+ # that the storage class is not retrieved from the server and set
46
+ # to "STANDARD" by default.
47
+ #
48
+ # ==== Example
49
+ # object.storage_class = :reduced_redundancy
50
+ def storage_class=(storage_class)
51
+ @storage_class = storage_class.to_s.upcase if storage_class
52
+ end
53
+
54
+ # Retrieves the object from the server. Method is used to download
55
+ # object information only (content type, size).
56
+ # Notice: It does NOT download the content of the object
57
+ # (use the #content method to do it).
58
+ # Notice: this do not fetch acl information, use #request_acl
59
+ # method for that.
60
+ def retrieve
61
+ object_headers
62
+ self
63
+ end
64
+
65
+ # Retrieves the object from the server, returns true if the object
66
+ # exists or false otherwise. Uses #retrieve method, but catches
67
+ # S3::Error::NoSuchKey exception and returns false when it happens
68
+ def exists?
69
+ retrieve
70
+ true
71
+ rescue Error::NoSuchKey
72
+ false
73
+ end
74
+
75
+ # Retrieves acl for object from the server.
76
+ #
77
+ # Return:
78
+ # hash: user|group => permission
79
+ def request_acl
80
+ response = object_request(:get, :params => "acl")
81
+ parse_acl(response.body)
82
+ end
83
+
84
+ # Downloads the content of the object, and caches it. Pass true to
85
+ # clear the cache and download the object again.
86
+ def content(reload = false)
87
+ return @content if defined?(@content) and not reload
88
+ get_object
89
+ @content
90
+ end
91
+
92
+ # Saves the object, returns true if successfull.
93
+ def save
94
+ put_object
95
+ true
96
+ end
97
+
98
+ # Copies the file to another key and/or bucket.
99
+ #
100
+ # ==== Options
101
+ # * <tt>:key</tt> - New key to store object in
102
+ # * <tt>:bucket</tt> - New bucket to store object in (instance of
103
+ # S3::Bucket)
104
+ # * <tt>:acl</tt> - ACL of the copied object (default:
105
+ # "private")
106
+ # * <tt>:content_type</tt> - Content type of the copied object
107
+ # (default: "application/octet-stream")
108
+ def copy(options = {})
109
+ copy_object(options)
110
+ end
111
+
112
+ # Destroys the file on the server
113
+ def destroy
114
+ delete_object
115
+ true
116
+ end
117
+
118
+ # Returns Object's URL using protocol specified in service,
119
+ # e.g. <tt>http://domain.com.s3.amazonaws.com/key/with/path.extension</tt>
120
+ def url
121
+ "#{protocol}#{host}/#{path_prefix}#{URI.escape(key, /[^#{URI::REGEXP::PATTERN::UNRESERVED}\/]/)}"
122
+ end
123
+
124
+ # Returns a temporary url to the object that expires on the
125
+ # timestamp given. Defaults to one hour expire time.
126
+ def temporary_url(expires_at = Time.now + 3600)
127
+ signature = Signature.generate_temporary_url_signature(:bucket => name,
128
+ :resource => key,
129
+ :expires_at => expires_at,
130
+ :secret_access_key => secret_access_key)
131
+
132
+ "#{url}?AWSAccessKeyId=#{self.bucket.service.access_key_id}&Expires=#{expires_at.to_i.to_s}&Signature=#{signature}"
133
+ end
134
+
135
+ # Returns Object's CNAME URL (without <tt>s3.amazonaws.com</tt>
136
+ # suffix) using protocol specified in Service,
137
+ # e.g. <tt>http://domain.com/key/with/path.extension</tt>. (you
138
+ # have to set the CNAME in your DNS before using the CNAME URL
139
+ # schema).
140
+ def cname_url
141
+ URI.escape("#{protocol}#{name}/#{key}") if bucket.vhost?
142
+ end
143
+
144
+ def inspect #:nodoc:
145
+ "#<#{self.class}:/#{name}/#{key}>"
146
+ end
147
+
148
+ private
149
+
150
+ attr_writer :last_modified, :etag, :size, :original_key, :bucket
151
+
152
+ def copy_object(options = {})
153
+ key = options[:key] or raise ArgumentError, "No key given"
154
+ raise ArgumentError.new("Invalid key name: #{key}") unless key_valid?(key)
155
+ bucket = options[:bucket] || self.bucket
156
+
157
+ headers = {}
158
+
159
+ headers[:x_amz_acl] = options[:acl] || acl || "private"
160
+ headers[:content_type] = options[:content_type] || content_type || "application/octet-stream"
161
+ headers[:content_encoding] = options[:content_encoding] if options[:content_encoding]
162
+ headers[:content_disposition] = options[:content_disposition] if options[:content_disposition]
163
+ headers[:cache_control] = options[:cache_control] if options[:cache_control]
164
+ headers[:x_amz_copy_source] = full_key
165
+ headers[:x_amz_metadata_directive] = options[:replace] == false ? "COPY" : "REPLACE"
166
+ headers[:x_amz_copy_source_if_match] = options[:if_match] if options[:if_match]
167
+ headers[:x_amz_copy_source_if_none_match] = options[:if_none_match] if options[:if_none_match]
168
+ headers[:x_amz_copy_source_if_unmodified_since] = options[:if_modified_since] if options[:if_modified_since]
169
+ headers[:x_amz_copy_source_if_modified_since] = options[:if_unmodified_since] if options[:if_unmodified_since]
170
+
171
+ response = bucket.send(:bucket_request, :put, :path => key, :headers => headers)
172
+ object_attributes = parse_copy_object_result(response.body)
173
+
174
+ object = Object.send(:new, bucket, object_attributes.merge(:key => key, :size => size))
175
+ object.acl = response["x-amz-acl"]
176
+ object.content_type = response["content-type"]
177
+ object.content_encoding = response["content-encoding"]
178
+ object.content_disposition = response["content-disposition"]
179
+ object.cache_control = response["cache-control"]
180
+ object
181
+ end
182
+
183
+ def get_object(options = {})
184
+ response = object_request(:get, options)
185
+ parse_headers(response)
186
+ end
187
+
188
+ def object_headers(options = {})
189
+ response = object_request(:head, options)
190
+ parse_headers(response)
191
+ rescue Error::ResponseError => e
192
+ if e.response.code.to_i == 404
193
+ raise Error::ResponseError.exception("NoSuchKey").new("The specified key does not exist.", nil)
194
+ else
195
+ raise e
196
+ end
197
+ end
198
+
199
+ def put_object
200
+ response = object_request(:put, :body => content, :headers => dump_headers)
201
+ parse_headers(response)
202
+ end
203
+
204
+ def delete_object(options = {})
205
+ object_request(:delete)
206
+ end
207
+
208
+ def initialize(bucket, options = {})
209
+ self.bucket = bucket
210
+ self.key = options[:key]
211
+ self.last_modified = options[:last_modified]
212
+ self.etag = options[:etag]
213
+ self.size = options[:size]
214
+ self.cache_control = options[:cache_control]
215
+ end
216
+
217
+ def object_request(method, options = {})
218
+ bucket_request(method, options.merge(:path => key))
219
+ end
220
+
221
+ def last_modified=(last_modified)
222
+ @last_modified = Time.parse(last_modified) if last_modified
223
+ end
224
+
225
+ def etag=(etag)
226
+ @etag = etag[1..-2] if etag
227
+ end
228
+
229
+ def key_valid?(key)
230
+ !(key.nil? or key.empty?)
231
+ end
232
+
233
+ def dump_headers
234
+ headers = {}
235
+ headers[:x_amz_acl] = @acl || "private"
236
+ headers[:x_amz_storage_class] = @storage_class || "STANDARD"
237
+ headers[:content_type] = @content_type || "application/octet-stream"
238
+ headers[:content_encoding] = @content_encoding if @content_encoding
239
+ headers[:content_disposition] = @content_disposition if @content_disposition
240
+ headers[:cache_control] = @cache_control if @cache_control
241
+ headers
242
+ end
243
+
244
+ def parse_headers(response)
245
+ @metadata = response.to_hash.select { |k, v| k.to_s.start_with?("x-amz-meta") }
246
+ self.etag = response["etag"] if response.key?("etag")
247
+ self.content_type = response["content-type"] if response.key?("content-type")
248
+ self.content_disposition = response["content-disposition"] if response.key?("content-disposition")
249
+ self.cache_control = response["cache-control"] if response.key?("cache-control")
250
+ self.content_encoding = response["content-encoding"] if response.key?("content-encoding")
251
+ self.last_modified = response["last-modified"] if response.key?("last-modified")
252
+ if response.key?("content-range")
253
+ self.size = response["content-range"].sub(/[^\/]+\//, "").to_i
254
+ else
255
+ self.size = response["content-length"]
256
+ if body = response.body
257
+ self.content = body
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end