1# Licensed under the Apache License, Version 2.0 (the "License"); you may 2 3# not use this file except in compliance with the License. You may obtain 4# a copy of the License at 5# 6# http://www.apache.org/licenses/LICENSE-2.0 7# 8# Unless required by applicable law or agreed to in writing, software 9# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11# License for the specific language governing permissions and limitations 12# under the License. 13 14import copy 15 16from openstack import exceptions 17from openstack.object_store.v1 import _base 18from openstack import resource 19 20 21class Object(_base.BaseResource): 22 _custom_metadata_prefix = "X-Object-Meta-" 23 _system_metadata = { 24 "content_disposition": "content-disposition", 25 "content_encoding": "content-encoding", 26 "content_type": "content-type", 27 "delete_after": "x-delete-after", 28 "delete_at": "x-delete-at", 29 "is_content_type_detected": "x-detect-content-type", 30 } 31 32 base_path = "/%(container)s" 33 pagination_key = 'X-Container-Object-Count' 34 35 allow_create = True 36 allow_fetch = True 37 allow_commit = True 38 allow_delete = True 39 allow_list = True 40 allow_head = True 41 42 _query_mapping = resource.QueryParameters( 43 'prefix', 'format' 44 ) 45 46 # Data to be passed during a POST call to create an object on the server. 47 # TODO(mordred) Make a base class BaseDataResource that can be used here 48 # and with glance images that has standard overrides for dealing with 49 # binary data. 50 data = None 51 52 # URL parameters 53 #: The unique name for the container. 54 container = resource.URI("container") 55 #: The unique name for the object. 56 name = resource.Body("name", alternate_id=True) 57 58 # Object details 59 # Make these private because they should only matter in the case where 60 # we have a Body with no headers (like if someone programmatically is 61 # creating an Object) 62 _hash = resource.Body("hash") 63 _bytes = resource.Body("bytes", type=int) 64 _last_modified = resource.Body("last_modified") 65 _content_type = resource.Body("content_type") 66 67 # Headers for HEAD and GET requests 68 #: If set to True, Object Storage queries all replicas to return 69 #: the most recent one. If you omit this header, Object Storage 70 #: responds faster after it finds one valid replica. Because 71 #: setting this header to True is more expensive for the back end, 72 #: use it only when it is absolutely needed. *Type: bool* 73 is_newest = resource.Header("x-newest", type=bool) 74 #: TODO(briancurtin) there's a lot of content here... 75 range = resource.Header("range", type=dict) 76 #: See http://www.ietf.org/rfc/rfc2616.txt. 77 if_match = resource.Header("if-match", type=list) 78 #: In combination with Expect: 100-Continue, specify an 79 #: "If-None-Match: \*" header to query whether the server already 80 #: has a copy of the object before any data is sent. 81 if_none_match = resource.Header("if-none-match", type=list) 82 #: See http://www.ietf.org/rfc/rfc2616.txt. 83 if_modified_since = resource.Header("if-modified-since", type=str) 84 #: See http://www.ietf.org/rfc/rfc2616.txt. 85 if_unmodified_since = resource.Header("if-unmodified-since", type=str) 86 87 # Query parameters 88 #: Used with temporary URLs to sign the request. For more 89 #: information about temporary URLs, see OpenStack Object Storage 90 #: API v1 Reference. 91 signature = resource.Header("signature") 92 #: Used with temporary URLs to specify the expiry time of the 93 #: signature. For more information about temporary URLs, see 94 #: OpenStack Object Storage API v1 Reference. 95 expires_at = resource.Header("expires") 96 #: If you include the multipart-manifest=get query parameter and 97 #: the object is a large object, the object contents are not 98 #: returned. Instead, the manifest is returned in the 99 #: X-Object-Manifest response header for dynamic large objects 100 #: or in the response body for static large objects. 101 multipart_manifest = resource.Header("multipart-manifest") 102 103 # Response headers from HEAD and GET 104 #: HEAD operations do not return content. However, in this 105 #: operation the value in the Content-Length header is not the 106 #: size of the response body. Instead it contains the size of 107 #: the object, in bytes. 108 content_length = resource.Header( 109 "content-length", type=int, alias='_bytes') 110 #: The MIME type of the object. 111 content_type = resource.Header("content-type", alias="_content_type") 112 #: The type of ranges that the object accepts. 113 accept_ranges = resource.Header("accept-ranges") 114 #: For objects smaller than 5 GB, this value is the MD5 checksum 115 #: of the object content. The value is not quoted. 116 #: For manifest objects, this value is the MD5 checksum of the 117 #: concatenated string of MD5 checksums and ETags for each of 118 #: the segments in the manifest, and not the MD5 checksum of 119 #: the content that was downloaded. Also the value is enclosed 120 #: in double-quote characters. 121 #: You are strongly recommended to compute the MD5 checksum of 122 #: the response body as it is received and compare this value 123 #: with the one in the ETag header. If they differ, the content 124 #: was corrupted, so retry the operation. 125 etag = resource.Header("etag", alias='_hash') 126 #: Set to True if this object is a static large object manifest object. 127 #: *Type: bool* 128 is_static_large_object = resource.Header("x-static-large-object", 129 type=bool) 130 #: If set, the value of the Content-Encoding metadata. 131 #: If not set, this header is not returned by this operation. 132 content_encoding = resource.Header("content-encoding") 133 #: If set, specifies the override behavior for the browser. 134 #: For example, this header might specify that the browser use 135 #: a download program to save this file rather than show the file, 136 #: which is the default. 137 #: If not set, this header is not returned by this operation. 138 content_disposition = resource.Header("content-disposition") 139 #: Specifies the number of seconds after which the object is 140 #: removed. Internally, the Object Storage system stores this 141 #: value in the X-Delete-At metadata item. 142 delete_after = resource.Header("x-delete-after", type=int) 143 #: If set, the time when the object will be deleted by the system 144 #: in the format of a UNIX Epoch timestamp. 145 #: If not set, this header is not returned by this operation. 146 delete_at = resource.Header("x-delete-at") 147 #: If set, to this is a dynamic large object manifest object. 148 #: The value is the container and object name prefix of the 149 #: segment objects in the form container/prefix. 150 object_manifest = resource.Header("x-object-manifest") 151 #: The timestamp of the transaction. 152 timestamp = resource.Header("x-timestamp") 153 #: The date and time that the object was created or the last 154 #: time that the metadata was changed. 155 last_modified_at = resource.Header("last-modified", alias='_last_modified') 156 157 # Headers for PUT and POST requests 158 #: Set to chunked to enable chunked transfer encoding. If used, 159 #: do not set the Content-Length header to a non-zero value. 160 transfer_encoding = resource.Header("transfer-encoding") 161 #: If set to true, Object Storage guesses the content type based 162 #: on the file extension and ignores the value sent in the 163 #: Content-Type header, if present. *Type: bool* 164 is_content_type_detected = resource.Header("x-detect-content-type", 165 type=bool) 166 #: If set, this is the name of an object used to create the new 167 #: object by copying the X-Copy-From object. The value is in form 168 #: {container}/{object}. You must UTF-8-encode and then URL-encode 169 #: the names of the container and object before you include them 170 #: in the header. 171 #: Using PUT with X-Copy-From has the same effect as using the 172 #: COPY operation to copy an object. 173 copy_from = resource.Header("x-copy-from") 174 175 has_body = False 176 177 def __init__(self, data=None, **attrs): 178 super(_base.BaseResource, self).__init__(**attrs) 179 self.data = data 180 181 # The Object Store treats the metadata for its resources inconsistently so 182 # Object.set_metadata must override the BaseResource.set_metadata to 183 # account for it. 184 def set_metadata(self, session, metadata): 185 # Filter out items with empty values so the create metadata behaviour 186 # is the same as account and container 187 filtered_metadata = \ 188 {key: value for key, value in metadata.items() if value} 189 190 # Update from remote if we only have locally created information 191 if not self.last_modified_at: 192 self.head(session) 193 194 # Get a copy of the original metadata so it doesn't get erased on POST 195 # and update it with the new metadata values. 196 metadata = copy.deepcopy(self.metadata) 197 metadata.update(filtered_metadata) 198 199 # Include any original system metadata so it doesn't get erased on POST 200 for key in self._system_metadata: 201 value = getattr(self, key) 202 if value and key not in metadata: 203 metadata[key] = value 204 205 request = self._prepare_request() 206 headers = self._calculate_headers(metadata) 207 response = session.post(request.url, headers=headers) 208 self._translate_response(response, has_body=False) 209 self.metadata.update(metadata) 210 211 return self 212 213 # The Object Store treats the metadata for its resources inconsistently so 214 # Object.delete_metadata must override the BaseResource.delete_metadata to 215 # account for it. 216 def delete_metadata(self, session, keys): 217 if not keys: 218 return 219 # If we have an empty object, update it from the remote side so that 220 # we have a copy of the original metadata. Deleting metadata requires 221 # POSTing and overwriting all of the metadata. If we already have 222 # metadata locally, assume this is an existing object. 223 if not self.metadata: 224 self.head(session) 225 226 metadata = copy.deepcopy(self.metadata) 227 228 # Include any original system metadata so it doesn't get erased on POST 229 for key in self._system_metadata: 230 value = getattr(self, key) 231 if value: 232 metadata[key] = value 233 234 # Remove the requested metadata keys 235 # TODO(mordred) Why don't we just look at self._header_mapping() 236 # instead of having system_metadata? 237 deleted = False 238 attr_keys_to_delete = set() 239 for key in keys: 240 if key == 'delete_after': 241 del(metadata['delete_at']) 242 else: 243 if key in metadata: 244 del(metadata[key]) 245 # Delete the attribute from the local copy of the object. 246 # Metadata that doesn't have Component attributes is 247 # handled by self.metadata being reset when we run 248 # self.head 249 if hasattr(self, key): 250 attr_keys_to_delete.add(key) 251 deleted = True 252 253 # Nothing to delete, skip the POST 254 if not deleted: 255 return self 256 257 request = self._prepare_request() 258 response = session.post( 259 request.url, headers=self._calculate_headers(metadata)) 260 exceptions.raise_from_response( 261 response, error_message="Error deleting metadata keys") 262 263 # Only delete from local object if the remote delete was successful 264 for key in attr_keys_to_delete: 265 delattr(self, key) 266 267 # Just update ourselves from remote again. 268 return self.head(session) 269 270 def _download(self, session, error_message=None, stream=False): 271 request = self._prepare_request() 272 273 response = session.get( 274 request.url, headers=request.headers, stream=stream) 275 exceptions.raise_from_response(response, error_message=error_message) 276 return response 277 278 def download(self, session, error_message=None): 279 response = self._download(session, error_message=error_message) 280 return response.content 281 282 def stream(self, session, error_message=None, chunk_size=1024): 283 response = self._download( 284 session, error_message=error_message, stream=True) 285 return response.iter_content(chunk_size, decode_unicode=False) 286 287 def create(self, session, base_path=None): 288 request = self._prepare_request(base_path=base_path) 289 290 response = session.put( 291 request.url, 292 data=self.data, 293 headers=request.headers) 294 self._translate_response(response, has_body=False) 295 return self 296 297 def _raw_delete(self, session): 298 if not self.allow_delete: 299 raise exceptions.MethodNotSupported(self, "delete") 300 301 request = self._prepare_request() 302 session = self._get_session(session) 303 microversion = self._get_microversion_for(session, 'delete') 304 305 if self.is_static_large_object is None: 306 # Fetch metadata to determine SLO flag 307 self.head(session) 308 309 headers = {} 310 311 if self.is_static_large_object: 312 headers['multipart-manifest'] = 'delete' 313 314 return session.delete( 315 request.url, 316 headers=headers, 317 microversion=microversion) 318