radosgw-s3 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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