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