swift 2.23.3__py3-none-any.whl → 2.35.0__py3-none-any.whl
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.
- swift/__init__.py +29 -50
- swift/account/auditor.py +21 -118
- swift/account/backend.py +33 -28
- swift/account/reaper.py +37 -28
- swift/account/replicator.py +22 -0
- swift/account/server.py +60 -26
- swift/account/utils.py +28 -11
- swift-2.23.3.data/scripts/swift-account-audit → swift/cli/account_audit.py +23 -13
- swift-2.23.3.data/scripts/swift-config → swift/cli/config.py +2 -2
- swift/cli/container_deleter.py +5 -11
- swift-2.23.3.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +8 -7
- swift/cli/dispersion_report.py +10 -9
- swift-2.23.3.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +63 -21
- swift/cli/form_signature.py +3 -7
- swift-2.23.3.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +8 -2
- swift/cli/info.py +154 -14
- swift/cli/manage_shard_ranges.py +705 -37
- swift-2.23.3.data/scripts/swift-oldies → swift/cli/oldies.py +25 -14
- swift-2.23.3.data/scripts/swift-orphans → swift/cli/orphans.py +7 -3
- swift/cli/recon.py +196 -67
- swift-2.23.3.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +17 -20
- swift-2.23.3.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
- swift/cli/relinker.py +807 -126
- swift/cli/reload.py +135 -0
- swift/cli/ringbuilder.py +217 -20
- swift/cli/ringcomposer.py +0 -1
- swift/cli/shard-info.py +4 -3
- swift/common/base_storage_server.py +9 -20
- swift/common/bufferedhttp.py +48 -74
- swift/common/constraints.py +20 -15
- swift/common/container_sync_realms.py +9 -11
- swift/common/daemon.py +25 -8
- swift/common/db.py +195 -128
- swift/common/db_auditor.py +168 -0
- swift/common/db_replicator.py +95 -55
- swift/common/digest.py +141 -0
- swift/common/direct_client.py +144 -33
- swift/common/error_limiter.py +93 -0
- swift/common/exceptions.py +25 -1
- swift/common/header_key_dict.py +2 -9
- swift/common/http_protocol.py +373 -0
- swift/common/internal_client.py +129 -59
- swift/common/linkat.py +3 -4
- swift/common/manager.py +284 -67
- swift/common/memcached.py +390 -145
- swift/common/middleware/__init__.py +4 -0
- swift/common/middleware/account_quotas.py +211 -46
- swift/common/middleware/acl.py +3 -8
- swift/common/middleware/backend_ratelimit.py +230 -0
- swift/common/middleware/bulk.py +22 -34
- swift/common/middleware/catch_errors.py +1 -3
- swift/common/middleware/cname_lookup.py +6 -11
- swift/common/middleware/container_quotas.py +1 -1
- swift/common/middleware/container_sync.py +39 -17
- swift/common/middleware/copy.py +12 -0
- swift/common/middleware/crossdomain.py +22 -9
- swift/common/middleware/crypto/__init__.py +2 -1
- swift/common/middleware/crypto/crypto_utils.py +11 -15
- swift/common/middleware/crypto/decrypter.py +28 -11
- swift/common/middleware/crypto/encrypter.py +12 -17
- swift/common/middleware/crypto/keymaster.py +8 -15
- swift/common/middleware/crypto/kms_keymaster.py +2 -1
- swift/common/middleware/dlo.py +15 -11
- swift/common/middleware/domain_remap.py +5 -4
- swift/common/middleware/etag_quoter.py +128 -0
- swift/common/middleware/formpost.py +73 -70
- swift/common/middleware/gatekeeper.py +8 -1
- swift/common/middleware/keystoneauth.py +33 -3
- swift/common/middleware/list_endpoints.py +4 -4
- swift/common/middleware/listing_formats.py +85 -49
- swift/common/middleware/memcache.py +4 -95
- swift/common/middleware/name_check.py +3 -2
- swift/common/middleware/proxy_logging.py +160 -92
- swift/common/middleware/ratelimit.py +17 -10
- swift/common/middleware/read_only.py +6 -4
- swift/common/middleware/recon.py +59 -22
- swift/common/middleware/s3api/acl_handlers.py +25 -3
- swift/common/middleware/s3api/acl_utils.py +6 -1
- swift/common/middleware/s3api/controllers/__init__.py +6 -0
- swift/common/middleware/s3api/controllers/acl.py +3 -2
- swift/common/middleware/s3api/controllers/bucket.py +242 -137
- swift/common/middleware/s3api/controllers/logging.py +2 -2
- swift/common/middleware/s3api/controllers/multi_delete.py +43 -20
- swift/common/middleware/s3api/controllers/multi_upload.py +219 -133
- swift/common/middleware/s3api/controllers/obj.py +112 -8
- swift/common/middleware/s3api/controllers/object_lock.py +44 -0
- swift/common/middleware/s3api/controllers/s3_acl.py +2 -2
- swift/common/middleware/s3api/controllers/tagging.py +57 -0
- swift/common/middleware/s3api/controllers/versioning.py +36 -7
- swift/common/middleware/s3api/etree.py +22 -9
- swift/common/middleware/s3api/exception.py +0 -4
- swift/common/middleware/s3api/s3api.py +113 -41
- swift/common/middleware/s3api/s3request.py +384 -218
- swift/common/middleware/s3api/s3response.py +126 -23
- swift/common/middleware/s3api/s3token.py +16 -17
- swift/common/middleware/s3api/schema/delete.rng +1 -1
- swift/common/middleware/s3api/subresource.py +7 -10
- swift/common/middleware/s3api/utils.py +27 -10
- swift/common/middleware/slo.py +665 -358
- swift/common/middleware/staticweb.py +64 -37
- swift/common/middleware/symlink.py +51 -18
- swift/common/middleware/tempauth.py +76 -58
- swift/common/middleware/tempurl.py +191 -173
- swift/common/middleware/versioned_writes/__init__.py +51 -0
- swift/common/middleware/{versioned_writes.py → versioned_writes/legacy.py} +27 -26
- swift/common/middleware/versioned_writes/object_versioning.py +1482 -0
- swift/common/middleware/x_profile/exceptions.py +1 -4
- swift/common/middleware/x_profile/html_viewer.py +18 -19
- swift/common/middleware/x_profile/profile_model.py +1 -2
- swift/common/middleware/xprofile.py +10 -10
- swift-2.23.3.data/scripts/swift-container-server → swift/common/recon.py +13 -8
- swift/common/registry.py +147 -0
- swift/common/request_helpers.py +324 -57
- swift/common/ring/builder.py +67 -25
- swift/common/ring/composite_builder.py +1 -1
- swift/common/ring/ring.py +177 -51
- swift/common/ring/utils.py +1 -1
- swift/common/splice.py +10 -6
- swift/common/statsd_client.py +205 -0
- swift/common/storage_policy.py +49 -44
- swift/common/swob.py +86 -102
- swift/common/{utils.py → utils/__init__.py} +2163 -2772
- swift/common/utils/base.py +131 -0
- swift/common/utils/config.py +433 -0
- swift/common/utils/ipaddrs.py +256 -0
- swift/common/utils/libc.py +345 -0
- swift/common/utils/logs.py +859 -0
- swift/common/utils/timestamp.py +412 -0
- swift/common/wsgi.py +553 -535
- swift/container/auditor.py +14 -100
- swift/container/backend.py +490 -231
- swift/container/reconciler.py +126 -37
- swift/container/replicator.py +96 -22
- swift/container/server.py +358 -165
- swift/container/sharder.py +1540 -684
- swift/container/sync.py +94 -88
- swift/container/updater.py +53 -32
- swift/obj/auditor.py +153 -35
- swift/obj/diskfile.py +466 -217
- swift/obj/expirer.py +406 -124
- swift/obj/mem_diskfile.py +7 -4
- swift/obj/mem_server.py +1 -0
- swift/obj/reconstructor.py +523 -262
- swift/obj/replicator.py +249 -188
- swift/obj/server.py +207 -122
- swift/obj/ssync_receiver.py +145 -85
- swift/obj/ssync_sender.py +113 -54
- swift/obj/updater.py +652 -139
- swift/obj/watchers/__init__.py +0 -0
- swift/obj/watchers/dark_data.py +213 -0
- swift/proxy/controllers/account.py +11 -11
- swift/proxy/controllers/base.py +848 -604
- swift/proxy/controllers/container.py +433 -92
- swift/proxy/controllers/info.py +3 -2
- swift/proxy/controllers/obj.py +1000 -489
- swift/proxy/server.py +185 -112
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/AUTHORS +58 -11
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/METADATA +51 -56
- swift-2.35.0.dist-info/RECORD +201 -0
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/WHEEL +1 -1
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/entry_points.txt +43 -0
- swift-2.35.0.dist-info/pbr.json +1 -0
- swift/locale/de/LC_MESSAGES/swift.po +0 -1216
- swift/locale/en_GB/LC_MESSAGES/swift.po +0 -1207
- swift/locale/es/LC_MESSAGES/swift.po +0 -1085
- swift/locale/fr/LC_MESSAGES/swift.po +0 -909
- swift/locale/it/LC_MESSAGES/swift.po +0 -894
- swift/locale/ja/LC_MESSAGES/swift.po +0 -965
- swift/locale/ko_KR/LC_MESSAGES/swift.po +0 -964
- swift/locale/pt_BR/LC_MESSAGES/swift.po +0 -881
- swift/locale/ru/LC_MESSAGES/swift.po +0 -891
- swift/locale/tr_TR/LC_MESSAGES/swift.po +0 -832
- swift/locale/zh_CN/LC_MESSAGES/swift.po +0 -833
- swift/locale/zh_TW/LC_MESSAGES/swift.po +0 -838
- swift-2.23.3.data/scripts/swift-account-auditor +0 -23
- swift-2.23.3.data/scripts/swift-account-info +0 -51
- swift-2.23.3.data/scripts/swift-account-reaper +0 -23
- swift-2.23.3.data/scripts/swift-account-replicator +0 -34
- swift-2.23.3.data/scripts/swift-account-server +0 -23
- swift-2.23.3.data/scripts/swift-container-auditor +0 -23
- swift-2.23.3.data/scripts/swift-container-info +0 -55
- swift-2.23.3.data/scripts/swift-container-reconciler +0 -21
- swift-2.23.3.data/scripts/swift-container-replicator +0 -34
- swift-2.23.3.data/scripts/swift-container-sharder +0 -37
- swift-2.23.3.data/scripts/swift-container-sync +0 -23
- swift-2.23.3.data/scripts/swift-container-updater +0 -23
- swift-2.23.3.data/scripts/swift-dispersion-report +0 -24
- swift-2.23.3.data/scripts/swift-form-signature +0 -20
- swift-2.23.3.data/scripts/swift-init +0 -119
- swift-2.23.3.data/scripts/swift-object-auditor +0 -29
- swift-2.23.3.data/scripts/swift-object-expirer +0 -33
- swift-2.23.3.data/scripts/swift-object-info +0 -60
- swift-2.23.3.data/scripts/swift-object-reconstructor +0 -33
- swift-2.23.3.data/scripts/swift-object-relinker +0 -41
- swift-2.23.3.data/scripts/swift-object-replicator +0 -37
- swift-2.23.3.data/scripts/swift-object-server +0 -27
- swift-2.23.3.data/scripts/swift-object-updater +0 -23
- swift-2.23.3.data/scripts/swift-proxy-server +0 -23
- swift-2.23.3.data/scripts/swift-recon +0 -24
- swift-2.23.3.data/scripts/swift-ring-builder +0 -24
- swift-2.23.3.data/scripts/swift-ring-builder-analyzer +0 -22
- swift-2.23.3.data/scripts/swift-ring-composer +0 -22
- swift-2.23.3.dist-info/RECORD +0 -220
- swift-2.23.3.dist-info/pbr.json +0 -1
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/LICENSE +0 -0
- {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1482 @@
|
|
1
|
+
# Copyright (c) 2020 OpenStack Foundation
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
12
|
+
# implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
"""
|
17
|
+
Object versioning in Swift has 3 different modes. There are two
|
18
|
+
:ref:`legacy modes <versioned_writes>` that have similar API with a slight
|
19
|
+
difference in behavior and this middleware introduces a new mode with a
|
20
|
+
completely redesigned API and implementation.
|
21
|
+
|
22
|
+
In terms of the implementation, this middleware relies heavily on the use of
|
23
|
+
static links to reduce the amount of backend data movement that was part of the
|
24
|
+
two legacy modes. It also introduces a new API for enabling the feature and to
|
25
|
+
interact with older versions of an object.
|
26
|
+
|
27
|
+
Compatibility between modes
|
28
|
+
===========================
|
29
|
+
|
30
|
+
This new mode is not backwards compatible or interchangeable with the
|
31
|
+
two legacy modes. This means that existing containers that are being versioned
|
32
|
+
by the two legacy modes cannot enable the new mode. The new mode can only be
|
33
|
+
enabled on a new container or a container without either
|
34
|
+
``X-Versions-Location`` or ``X-History-Location`` header set. Attempting to
|
35
|
+
enable the new mode on a container with either header will result in a
|
36
|
+
``400 Bad Request`` response.
|
37
|
+
|
38
|
+
Enable Object Versioning in a Container
|
39
|
+
=======================================
|
40
|
+
|
41
|
+
After the introduction of this feature containers in a Swift cluster will be
|
42
|
+
in one of either 3 possible states: 1. Object versioning never enabled,
|
43
|
+
2. Object Versioning Enabled or 3. Object Versioning Disabled. Once versioning
|
44
|
+
has been enabled on a container, it will always have a flag stating whether it
|
45
|
+
is either enabled or disabled.
|
46
|
+
|
47
|
+
Clients enable object versioning on a container by performing either a PUT or
|
48
|
+
POST request with the header ``X-Versions-Enabled: true``. Upon enabling the
|
49
|
+
versioning for the first time, the middleware will create a hidden container
|
50
|
+
where object versions are stored. This hidden container will inherit the same
|
51
|
+
Storage Policy as its parent container.
|
52
|
+
|
53
|
+
To disable, clients send a POST request with the header
|
54
|
+
``X-Versions-Enabled: false``. When versioning is disabled, the old versions
|
55
|
+
remain unchanged.
|
56
|
+
|
57
|
+
To delete a versioned container, versioning must be disabled and all versions
|
58
|
+
of all objects must be deleted before the container can be deleted. At such
|
59
|
+
time, the hidden container will also be deleted.
|
60
|
+
|
61
|
+
Object CRUD Operations to a Versioned Container
|
62
|
+
===============================================
|
63
|
+
|
64
|
+
When data is ``PUT`` into a versioned container (a container with the
|
65
|
+
versioning flag enabled), the actual object is written to a hidden container
|
66
|
+
and a symlink object is written to the parent container. Every object is
|
67
|
+
assigned a version id. This id can be retrieved from the
|
68
|
+
``X-Object-Version-Id`` header in the PUT response.
|
69
|
+
|
70
|
+
.. note::
|
71
|
+
|
72
|
+
When object versioning is disabled on a container, new data will no longer
|
73
|
+
be versioned, but older versions remain untouched. Any new data ``PUT``
|
74
|
+
will result in a object with a ``null`` version-id. The versioning API can
|
75
|
+
be used to both list and operate on previous versions even while versioning
|
76
|
+
is disabled.
|
77
|
+
|
78
|
+
If versioning is re-enabled and an overwrite occurs on a `null` id object.
|
79
|
+
The object will be versioned off with a regular version-id.
|
80
|
+
|
81
|
+
A ``GET`` to a versioned object will return the current version of the object.
|
82
|
+
The ``X-Object-Version-Id`` header is also returned in the response.
|
83
|
+
|
84
|
+
A ``POST`` to a versioned object will update the most current object metadata
|
85
|
+
as normal, but will not create a new version of the object. In other words,
|
86
|
+
new versions are only created when the content of the object changes.
|
87
|
+
|
88
|
+
On ``DELETE``, the middleware will write a zero-byte "delete marker" object
|
89
|
+
version that notes **when** the delete took place. The symlink object will also
|
90
|
+
be deleted from the versioned container. The object will no longer appear in
|
91
|
+
container listings for the versioned container and future requests there will
|
92
|
+
return ``404 Not Found``. However, the previous versions content will still be
|
93
|
+
recoverable.
|
94
|
+
|
95
|
+
Object Versioning API
|
96
|
+
=====================
|
97
|
+
|
98
|
+
Clients can now operate on previous versions of an object using this new
|
99
|
+
versioning API.
|
100
|
+
|
101
|
+
First to list previous versions, issue a a ``GET`` request to the versioned
|
102
|
+
container with query parameter::
|
103
|
+
|
104
|
+
?versions
|
105
|
+
|
106
|
+
To list a container with a large number of object versions, clients can
|
107
|
+
also use the ``version_marker`` parameter together with the ``marker``
|
108
|
+
parameter. While the ``marker`` parameter is used to specify an object name
|
109
|
+
the ``version_marker`` will be used specify the version id.
|
110
|
+
|
111
|
+
All other pagination parameters can be used in conjunction with the
|
112
|
+
``versions`` parameter.
|
113
|
+
|
114
|
+
During container listings, delete markers can be identified with the
|
115
|
+
content-type ``application/x-deleted;swift_versions_deleted=1``. The most
|
116
|
+
current version of an object can be identified by the field ``is_latest``.
|
117
|
+
|
118
|
+
To operate on previous versions, clients can use the query parameter::
|
119
|
+
|
120
|
+
?version-id=<id>
|
121
|
+
|
122
|
+
where the ``<id>`` is the value from the ``X-Object-Version-Id`` header.
|
123
|
+
|
124
|
+
Only COPY, HEAD, GET and DELETE operations can be performed on previous
|
125
|
+
versions. Either a PUT or POST request with a ``version-id`` parameter will
|
126
|
+
result in a ``400 Bad Request`` response.
|
127
|
+
|
128
|
+
A HEAD/GET request to a delete-marker will result in a ``404 Not Found``
|
129
|
+
response.
|
130
|
+
|
131
|
+
When issuing DELETE requests with a ``version-id`` parameter, delete markers
|
132
|
+
are not written down. A DELETE request with a ``version-id`` parameter to
|
133
|
+
the current object will result in a both the symlink and the backing data
|
134
|
+
being deleted. A DELETE to any other version will result in that version only
|
135
|
+
be deleted and no changes made to the symlink pointing to the current version.
|
136
|
+
|
137
|
+
How to Enable Object Versioning in a Swift Cluster
|
138
|
+
==================================================
|
139
|
+
|
140
|
+
To enable this new mode in a Swift cluster the ``versioned_writes`` and
|
141
|
+
``symlink`` middlewares must be added to the proxy pipeline, you must also set
|
142
|
+
the option ``allow_object_versioning`` to ``True``.
|
143
|
+
"""
|
144
|
+
|
145
|
+
import calendar
|
146
|
+
import itertools
|
147
|
+
import json
|
148
|
+
import time
|
149
|
+
|
150
|
+
from urllib.parse import unquote
|
151
|
+
|
152
|
+
from swift.common.constraints import MAX_FILE_SIZE, valid_api_version, \
|
153
|
+
ACCOUNT_LISTING_LIMIT, CONTAINER_LISTING_LIMIT
|
154
|
+
from swift.common.http import is_success, is_client_error, HTTP_NOT_FOUND, \
|
155
|
+
HTTP_CONFLICT
|
156
|
+
from swift.common.request_helpers import get_sys_meta_prefix, \
|
157
|
+
copy_header_subset, get_reserved_name, split_reserved_name, \
|
158
|
+
constrain_req_limit
|
159
|
+
from swift.common.middleware import app_property
|
160
|
+
from swift.common.middleware.symlink import TGT_OBJ_SYMLINK_HDR, \
|
161
|
+
TGT_ETAG_SYSMETA_SYMLINK_HDR, SYMLOOP_EXTEND, ALLOW_RESERVED_NAMES, \
|
162
|
+
TGT_BYTES_SYSMETA_SYMLINK_HDR, TGT_ACCT_SYMLINK_HDR
|
163
|
+
from swift.common.swob import HTTPPreconditionFailed, HTTPServiceUnavailable, \
|
164
|
+
HTTPBadRequest, str_to_wsgi, bytes_to_wsgi, wsgi_quote, \
|
165
|
+
wsgi_to_str, wsgi_unquote, Request, HTTPNotFound, HTTPException, \
|
166
|
+
HTTPRequestEntityTooLarge, HTTPInternalServerError, HTTPNotAcceptable, \
|
167
|
+
HTTPConflict, HTTPLengthRequired
|
168
|
+
from swift.common.storage_policy import POLICIES
|
169
|
+
from swift.common.utils import get_logger, Timestamp, drain_and_close, \
|
170
|
+
config_true_value, close_if_possible, closing_if_possible, \
|
171
|
+
FileLikeIter, split_path, parse_content_type, parse_header, RESERVED_STR
|
172
|
+
from swift.common.wsgi import WSGIContext, make_pre_authed_request
|
173
|
+
from swift.proxy.controllers.base import get_container_info
|
174
|
+
|
175
|
+
|
176
|
+
DELETE_MARKER_CONTENT_TYPE = 'application/x-deleted;swift_versions_deleted=1'
|
177
|
+
CLIENT_VERSIONS_ENABLED = 'x-versions-enabled'
|
178
|
+
SYSMETA_VERSIONS_ENABLED = \
|
179
|
+
get_sys_meta_prefix('container') + 'versions-enabled'
|
180
|
+
SYSMETA_VERSIONS_CONT = get_sys_meta_prefix('container') + 'versions-container'
|
181
|
+
SYSMETA_PARENT_CONT = get_sys_meta_prefix('container') + 'parent-container'
|
182
|
+
SYSMETA_VERSIONS_SYMLINK = get_sys_meta_prefix('object') + 'versions-symlink'
|
183
|
+
|
184
|
+
|
185
|
+
def build_listing(*to_splice, **kwargs):
|
186
|
+
reverse = kwargs.pop('reverse')
|
187
|
+
limit = kwargs.pop('limit')
|
188
|
+
if kwargs:
|
189
|
+
raise TypeError('Invalid keyword arguments received: %r' % kwargs)
|
190
|
+
|
191
|
+
def merge_key(item):
|
192
|
+
if 'subdir' in item:
|
193
|
+
return item['subdir']
|
194
|
+
return item['name']
|
195
|
+
|
196
|
+
return json.dumps(sorted(
|
197
|
+
itertools.chain(*to_splice),
|
198
|
+
key=merge_key,
|
199
|
+
reverse=reverse,
|
200
|
+
)[:limit]).encode('ascii')
|
201
|
+
|
202
|
+
|
203
|
+
def non_expiry_header(header):
|
204
|
+
return header.lower() not in ('x-delete-at', 'x-delete-after')
|
205
|
+
|
206
|
+
|
207
|
+
class ByteCountingReader(object):
|
208
|
+
"""
|
209
|
+
Counts bytes read from file_like so we know how big the object is that
|
210
|
+
the client just PUT.
|
211
|
+
|
212
|
+
This is particularly important when the client sends a chunk-encoded body,
|
213
|
+
so we don't have a Content-Length header available.
|
214
|
+
"""
|
215
|
+
def __init__(self, file_like):
|
216
|
+
self.file_like = file_like
|
217
|
+
self.bytes_read = 0
|
218
|
+
|
219
|
+
def read(self, amt=-1):
|
220
|
+
chunk = self.file_like.read(amt)
|
221
|
+
self.bytes_read += len(chunk)
|
222
|
+
return chunk
|
223
|
+
|
224
|
+
|
225
|
+
class ObjectVersioningContext(WSGIContext):
|
226
|
+
def __init__(self, wsgi_app, logger):
|
227
|
+
super(ObjectVersioningContext, self).__init__(wsgi_app)
|
228
|
+
self.logger = logger
|
229
|
+
|
230
|
+
def _build_versions_object_prefix(self, object_name):
|
231
|
+
return get_reserved_name(object_name, '')
|
232
|
+
|
233
|
+
def _build_versions_container_name(self, container_name):
|
234
|
+
return get_reserved_name('versions', container_name)
|
235
|
+
|
236
|
+
def _build_versions_object_name(self, object_name, ts):
|
237
|
+
inv = ~Timestamp(ts)
|
238
|
+
return get_reserved_name(object_name, inv.internal)
|
239
|
+
|
240
|
+
def _split_version_from_name(self, versioned_name):
|
241
|
+
try:
|
242
|
+
name, inv = split_reserved_name(versioned_name)
|
243
|
+
ts = ~Timestamp(inv)
|
244
|
+
except ValueError:
|
245
|
+
return versioned_name, None
|
246
|
+
return name, ts
|
247
|
+
|
248
|
+
def _split_versions_container_name(self, versions_container):
|
249
|
+
try:
|
250
|
+
versions, container_name = split_reserved_name(versions_container)
|
251
|
+
except ValueError:
|
252
|
+
return versions_container
|
253
|
+
|
254
|
+
if versions != 'versions':
|
255
|
+
return versions_container
|
256
|
+
|
257
|
+
return container_name
|
258
|
+
|
259
|
+
|
260
|
+
class ObjectContext(ObjectVersioningContext):
|
261
|
+
|
262
|
+
def _get_source_object(self, req, path_info):
|
263
|
+
# make a pre_auth request in case the user has write access
|
264
|
+
# to container, but not READ. This was allowed in previous version
|
265
|
+
# (i.e., before middleware) so keeping the same behavior here
|
266
|
+
get_req = make_pre_authed_request(
|
267
|
+
req.environ, path=wsgi_quote(path_info) + '?symlink=get',
|
268
|
+
headers={'X-Newest': 'True'}, method='GET', swift_source='OV')
|
269
|
+
source_resp = get_req.get_response(self.app)
|
270
|
+
|
271
|
+
if source_resp.content_length is None or \
|
272
|
+
source_resp.content_length > MAX_FILE_SIZE:
|
273
|
+
close_if_possible(source_resp.app_iter)
|
274
|
+
return HTTPRequestEntityTooLarge(request=req)
|
275
|
+
|
276
|
+
return source_resp
|
277
|
+
|
278
|
+
def _put_versioned_obj(self, req, put_path_info, source_resp):
|
279
|
+
# Create a new Request object to PUT to the versions container, copying
|
280
|
+
# all headers from the source object apart from x-timestamp.
|
281
|
+
put_req = make_pre_authed_request(
|
282
|
+
req.environ, path=wsgi_quote(put_path_info), method='PUT',
|
283
|
+
headers={'X-Backend-Allow-Reserved-Names': 'true'},
|
284
|
+
swift_source='OV')
|
285
|
+
copy_header_subset(source_resp, put_req,
|
286
|
+
lambda k: k.lower() != 'x-timestamp')
|
287
|
+
put_req.environ['wsgi.input'] = FileLikeIter(source_resp.app_iter)
|
288
|
+
slo_size = put_req.headers.get('X-Object-Sysmeta-Slo-Size')
|
289
|
+
if slo_size:
|
290
|
+
put_req.headers['Content-Type'] += '; swift_bytes=%s' % slo_size
|
291
|
+
put_req.environ['swift.content_type_overridden'] = True
|
292
|
+
put_resp = put_req.get_response(self.app)
|
293
|
+
drain_and_close(put_resp)
|
294
|
+
# the PUT should have already drained source_resp
|
295
|
+
close_if_possible(source_resp.app_iter)
|
296
|
+
return put_resp
|
297
|
+
|
298
|
+
def _put_versioned_obj_from_client(self, req, versions_cont, api_version,
|
299
|
+
account_name, object_name):
|
300
|
+
vers_obj_name = self._build_versions_object_name(
|
301
|
+
object_name, req.timestamp.internal)
|
302
|
+
put_path_info = "/%s/%s/%s/%s" % (
|
303
|
+
api_version, account_name, versions_cont, vers_obj_name)
|
304
|
+
# Consciously *do not* set swift_source here -- this req is in charge
|
305
|
+
# of reading bytes from the client, don't let it look like that data
|
306
|
+
# movement is due to some internal-to-swift thing
|
307
|
+
put_req = make_pre_authed_request(
|
308
|
+
req.environ, path=wsgi_quote(put_path_info), method='PUT',
|
309
|
+
headers={'X-Backend-Allow-Reserved-Names': 'true'},
|
310
|
+
swift_source='OV')
|
311
|
+
# move the client request body over
|
312
|
+
# note that the WSGI environ may be *further* manipulated; hold on to
|
313
|
+
# a reference to the byte counter so we can get the bytes_read
|
314
|
+
if req.message_length() is None:
|
315
|
+
put_req.headers['transfer-encoding'] = \
|
316
|
+
req.headers.get('transfer-encoding')
|
317
|
+
else:
|
318
|
+
put_req.content_length = req.content_length
|
319
|
+
byte_counter = ByteCountingReader(req.environ['wsgi.input'])
|
320
|
+
put_req.environ['wsgi.input'] = byte_counter
|
321
|
+
req.body = b''
|
322
|
+
# move metadata over, including sysmeta
|
323
|
+
|
324
|
+
copy_header_subset(req, put_req, non_expiry_header)
|
325
|
+
if 'swift.content_type_overridden' in req.environ:
|
326
|
+
put_req.environ['swift.content_type_overridden'] = \
|
327
|
+
req.environ.pop('swift.content_type_overridden')
|
328
|
+
|
329
|
+
# do the write
|
330
|
+
put_resp = put_req.get_response(self.app)
|
331
|
+
close_if_possible(put_req.environ['wsgi.input'])
|
332
|
+
|
333
|
+
if put_resp.status_int == HTTP_NOT_FOUND:
|
334
|
+
drain_and_close(put_resp)
|
335
|
+
raise HTTPInternalServerError(
|
336
|
+
request=req, content_type='text/plain',
|
337
|
+
body=b'The versions container does not exist. You may '
|
338
|
+
b'want to re-enable object versioning.')
|
339
|
+
|
340
|
+
self._check_response_error(req, put_resp)
|
341
|
+
drain_and_close(put_resp)
|
342
|
+
put_bytes = byte_counter.bytes_read
|
343
|
+
# N.B. this is essentially the same hack that symlink does in
|
344
|
+
# _validate_etag_and_update_sysmeta to deal with SLO
|
345
|
+
slo_size = put_req.headers.get('X-Object-Sysmeta-Slo-Size')
|
346
|
+
if slo_size:
|
347
|
+
put_bytes = slo_size
|
348
|
+
put_content_type = parse_content_type(
|
349
|
+
put_req.headers['Content-Type'])[0]
|
350
|
+
|
351
|
+
return (put_resp, vers_obj_name, put_bytes, put_content_type)
|
352
|
+
|
353
|
+
def _put_symlink_to_version(self, req, versions_cont, put_vers_obj_name,
|
354
|
+
api_version, account_name, object_name,
|
355
|
+
put_etag, put_bytes, put_content_type):
|
356
|
+
|
357
|
+
req.method = 'PUT'
|
358
|
+
# inch x-timestamp forward, just in case
|
359
|
+
req.ensure_x_timestamp()
|
360
|
+
req.headers['X-Timestamp'] = Timestamp(
|
361
|
+
req.timestamp, offset=1).internal
|
362
|
+
req.headers[TGT_ETAG_SYSMETA_SYMLINK_HDR] = put_etag
|
363
|
+
req.headers[TGT_BYTES_SYSMETA_SYMLINK_HDR] = put_bytes
|
364
|
+
# N.B. in stack mode DELETE we use content_type from listing
|
365
|
+
req.headers['Content-Type'] = put_content_type
|
366
|
+
req.headers[TGT_OBJ_SYMLINK_HDR] = wsgi_quote('%s/%s' % (
|
367
|
+
versions_cont, put_vers_obj_name))
|
368
|
+
req.headers[SYSMETA_VERSIONS_SYMLINK] = 'true'
|
369
|
+
req.headers[SYMLOOP_EXTEND] = 'true'
|
370
|
+
req.headers[ALLOW_RESERVED_NAMES] = 'true'
|
371
|
+
req.headers['X-Backend-Allow-Reserved-Names'] = 'true'
|
372
|
+
not_for_symlink_headers = (
|
373
|
+
'ETag', 'X-If-Delete-At', TGT_ACCT_SYMLINK_HDR,
|
374
|
+
'X-Object-Manifest', 'X-Static-Large-Object',
|
375
|
+
'X-Object-Sysmeta-Slo-Etag', 'X-Object-Sysmeta-Slo-Size',
|
376
|
+
)
|
377
|
+
for header in not_for_symlink_headers:
|
378
|
+
req.headers.pop(header, None)
|
379
|
+
|
380
|
+
# *do* set swift_source here; this PUT is an implementation detail
|
381
|
+
req.environ['swift.source'] = 'OV'
|
382
|
+
req.body = b''
|
383
|
+
resp = req.get_response(self.app)
|
384
|
+
resp.headers['ETag'] = put_etag
|
385
|
+
resp.headers['X-Object-Version-Id'] = self._split_version_from_name(
|
386
|
+
put_vers_obj_name)[1].internal
|
387
|
+
return resp
|
388
|
+
|
389
|
+
def _check_response_error(self, req, resp):
|
390
|
+
"""
|
391
|
+
Raise Error Response in case of error
|
392
|
+
"""
|
393
|
+
if is_success(resp.status_int):
|
394
|
+
return
|
395
|
+
body = resp.body
|
396
|
+
drain_and_close(resp)
|
397
|
+
if is_client_error(resp.status_int):
|
398
|
+
# missing container or bad permissions
|
399
|
+
if resp.status_int == 404:
|
400
|
+
raise HTTPPreconditionFailed(request=req)
|
401
|
+
raise HTTPException(body=body, status=resp.status,
|
402
|
+
headers=resp.headers)
|
403
|
+
# could not version the data, bail
|
404
|
+
raise HTTPServiceUnavailable(request=req)
|
405
|
+
|
406
|
+
def _copy_current(self, req, versions_cont, api_version, account_name,
|
407
|
+
object_name):
|
408
|
+
'''
|
409
|
+
Check if the current version of the object is a versions-symlink
|
410
|
+
if not, it's because this object was added to the container when
|
411
|
+
versioning was not enabled. We'll need to copy it into the versions
|
412
|
+
containers now.
|
413
|
+
|
414
|
+
:param req: original request.
|
415
|
+
:param versions_cont: container where previous versions of the object
|
416
|
+
are stored.
|
417
|
+
:param api_version: api version.
|
418
|
+
:param account_name: account name.
|
419
|
+
:param object_name: name of object of original request
|
420
|
+
'''
|
421
|
+
# validate the write access to the versioned container before
|
422
|
+
# making any backend requests
|
423
|
+
if 'swift.authorize' in req.environ:
|
424
|
+
container_info = get_container_info(
|
425
|
+
req.environ, self.app, swift_source='OV')
|
426
|
+
req.acl = container_info.get('write_acl')
|
427
|
+
aresp = req.environ['swift.authorize'](req)
|
428
|
+
if aresp:
|
429
|
+
raise aresp
|
430
|
+
|
431
|
+
get_resp = self._get_source_object(req, req.path_info)
|
432
|
+
|
433
|
+
if get_resp.status_int == HTTP_NOT_FOUND:
|
434
|
+
# nothing to version, proceed with original request
|
435
|
+
drain_and_close(get_resp)
|
436
|
+
return get_resp
|
437
|
+
|
438
|
+
# check for any other errors
|
439
|
+
self._check_response_error(req, get_resp)
|
440
|
+
|
441
|
+
if get_resp.headers.get(SYSMETA_VERSIONS_SYMLINK) == 'true':
|
442
|
+
# existing object is a VW symlink; no action required
|
443
|
+
drain_and_close(get_resp)
|
444
|
+
return get_resp
|
445
|
+
|
446
|
+
# if there's an existing object, then copy it to
|
447
|
+
# X-Versions-Location
|
448
|
+
ts_source = get_resp.headers.get(
|
449
|
+
'x-timestamp',
|
450
|
+
calendar.timegm(time.strptime(
|
451
|
+
get_resp.headers['last-modified'],
|
452
|
+
'%a, %d %b %Y %H:%M:%S GMT')))
|
453
|
+
vers_obj_name = self._build_versions_object_name(
|
454
|
+
object_name, ts_source)
|
455
|
+
|
456
|
+
put_path_info = "/%s/%s/%s/%s" % (
|
457
|
+
api_version, account_name, versions_cont, vers_obj_name)
|
458
|
+
put_resp = self._put_versioned_obj(req, put_path_info, get_resp)
|
459
|
+
|
460
|
+
if put_resp.status_int == HTTP_NOT_FOUND:
|
461
|
+
raise HTTPInternalServerError(
|
462
|
+
request=req, content_type='text/plain',
|
463
|
+
body=b'The versions container does not exist. You may '
|
464
|
+
b'want to re-enable object versioning.')
|
465
|
+
|
466
|
+
self._check_response_error(req, put_resp)
|
467
|
+
|
468
|
+
def handle_put(self, req, versions_cont, api_version,
|
469
|
+
account_name, object_name, is_enabled):
|
470
|
+
"""
|
471
|
+
Check if the current version of the object is a versions-symlink
|
472
|
+
if not, it's because this object was added to the container when
|
473
|
+
versioning was not enabled. We'll need to copy it into the versions
|
474
|
+
containers now that versioning is enabled.
|
475
|
+
|
476
|
+
Also, put the new data from the client into the versions container
|
477
|
+
and add a static symlink in the versioned container.
|
478
|
+
|
479
|
+
:param req: original request.
|
480
|
+
:param versions_cont: container where previous versions of the object
|
481
|
+
are stored.
|
482
|
+
:param api_version: api version.
|
483
|
+
:param account_name: account name.
|
484
|
+
:param object_name: name of object of original request
|
485
|
+
"""
|
486
|
+
# handle object request for a disabled versioned container.
|
487
|
+
if not is_enabled:
|
488
|
+
return req.get_response(self.app)
|
489
|
+
|
490
|
+
# attempt to copy current object to versions container
|
491
|
+
self._copy_current(req, versions_cont, api_version, account_name,
|
492
|
+
object_name)
|
493
|
+
|
494
|
+
# write client's put directly to versioned container
|
495
|
+
req.ensure_x_timestamp()
|
496
|
+
put_resp, put_vers_obj_name, put_bytes, put_content_type = \
|
497
|
+
self._put_versioned_obj_from_client(req, versions_cont,
|
498
|
+
api_version, account_name,
|
499
|
+
object_name)
|
500
|
+
|
501
|
+
# and add an static symlink to original container
|
502
|
+
target_etag = put_resp.headers['Etag']
|
503
|
+
return self._put_symlink_to_version(req, versions_cont,
|
504
|
+
put_vers_obj_name, api_version,
|
505
|
+
account_name, object_name,
|
506
|
+
target_etag, put_bytes,
|
507
|
+
put_content_type)
|
508
|
+
|
509
|
+
def handle_delete(self, req, versions_cont, api_version,
|
510
|
+
account_name, container_name,
|
511
|
+
object_name, is_enabled):
|
512
|
+
"""
|
513
|
+
Handle DELETE requests.
|
514
|
+
|
515
|
+
Copy current version of object to versions_container and write a
|
516
|
+
delete marker before proceeding with original request.
|
517
|
+
|
518
|
+
:param req: original request.
|
519
|
+
:param versions_cont: container where previous versions of the object
|
520
|
+
are stored.
|
521
|
+
:param api_version: api version.
|
522
|
+
:param account_name: account name.
|
523
|
+
:param object_name: name of object of original request
|
524
|
+
"""
|
525
|
+
# handle object request for a disabled versioned container.
|
526
|
+
if not is_enabled:
|
527
|
+
return req.get_response(self.app)
|
528
|
+
|
529
|
+
self._copy_current(req, versions_cont, api_version,
|
530
|
+
account_name, object_name)
|
531
|
+
|
532
|
+
req.ensure_x_timestamp()
|
533
|
+
marker_name = self._build_versions_object_name(
|
534
|
+
object_name, req.timestamp.internal)
|
535
|
+
marker_path = "/%s/%s/%s/%s" % (
|
536
|
+
api_version, account_name, versions_cont, marker_name)
|
537
|
+
marker_headers = {
|
538
|
+
# Definitive source of truth is Content-Type, and since we add
|
539
|
+
# a swift_* param, we know users haven't set it themselves.
|
540
|
+
# This is still open to users POSTing to update the content-type
|
541
|
+
# but they're just shooting themselves in the foot then.
|
542
|
+
'content-type': DELETE_MARKER_CONTENT_TYPE,
|
543
|
+
'content-length': '0',
|
544
|
+
'x-auth-token': req.headers.get('x-auth-token'),
|
545
|
+
'X-Backend-Allow-Reserved-Names': 'true',
|
546
|
+
}
|
547
|
+
marker_req = make_pre_authed_request(
|
548
|
+
req.environ, path=wsgi_quote(marker_path),
|
549
|
+
headers=marker_headers, method='PUT', swift_source='OV')
|
550
|
+
marker_req.environ['swift.content_type_overridden'] = True
|
551
|
+
marker_resp = marker_req.get_response(self.app)
|
552
|
+
self._check_response_error(req, marker_resp)
|
553
|
+
drain_and_close(marker_resp)
|
554
|
+
|
555
|
+
# successfully copied and created delete marker; safe to delete
|
556
|
+
resp = req.get_response(self.app)
|
557
|
+
if resp.is_success or resp.status_int == 404:
|
558
|
+
resp.headers['X-Object-Version-Id'] = \
|
559
|
+
self._split_version_from_name(marker_name)[1].internal
|
560
|
+
resp.headers['X-Backend-Content-Type'] = DELETE_MARKER_CONTENT_TYPE
|
561
|
+
drain_and_close(resp)
|
562
|
+
return resp
|
563
|
+
|
564
|
+
def handle_post(self, req, versions_cont, account):
|
565
|
+
'''
|
566
|
+
Handle a POST request to an object in a versioned container.
|
567
|
+
|
568
|
+
If the response is a 307 because the POST went to a symlink,
|
569
|
+
follow the symlink and send the request to the versioned object
|
570
|
+
|
571
|
+
:param req: original request.
|
572
|
+
:param versions_cont: container where previous versions of the object
|
573
|
+
are stored.
|
574
|
+
:param account: account name.
|
575
|
+
'''
|
576
|
+
# create eventual post request before
|
577
|
+
# encryption middleware changes the request headers
|
578
|
+
post_req = make_pre_authed_request(
|
579
|
+
req.environ, path=wsgi_quote(req.path_info), method='POST',
|
580
|
+
headers={'X-Backend-Allow-Reserved-Names': 'true'},
|
581
|
+
swift_source='OV')
|
582
|
+
copy_header_subset(req, post_req, non_expiry_header)
|
583
|
+
|
584
|
+
# send original request
|
585
|
+
resp = req.get_response(self.app)
|
586
|
+
|
587
|
+
# if it's a versioning symlink, send post to versioned object
|
588
|
+
if resp.status_int == 307 and config_true_value(
|
589
|
+
resp.headers.get(SYSMETA_VERSIONS_SYMLINK, 'false')):
|
590
|
+
loc = wsgi_unquote(resp.headers['Location'])
|
591
|
+
|
592
|
+
# Only follow if the version container matches
|
593
|
+
if split_path(loc, 4, 4, True)[1:3] == [
|
594
|
+
account, versions_cont]:
|
595
|
+
drain_and_close(resp)
|
596
|
+
post_req.path_info = loc
|
597
|
+
resp = post_req.get_response(self.app)
|
598
|
+
return resp
|
599
|
+
|
600
|
+
def _check_head(self, req, auth_token_header):
|
601
|
+
obj_head_headers = {
|
602
|
+
'X-Newest': 'True',
|
603
|
+
}
|
604
|
+
obj_head_headers.update(auth_token_header)
|
605
|
+
head_req = make_pre_authed_request(
|
606
|
+
req.environ, path=wsgi_quote(req.path_info) + '?symlink=get',
|
607
|
+
method='HEAD', headers=obj_head_headers, swift_source='OV')
|
608
|
+
hresp = head_req.get_response(self.app)
|
609
|
+
head_is_tombstone = False
|
610
|
+
symlink_target = None
|
611
|
+
if hresp.status_int == HTTP_NOT_FOUND:
|
612
|
+
head_is_tombstone = True
|
613
|
+
else:
|
614
|
+
head_is_tombstone = False
|
615
|
+
# if there's any other kind of error with a broken link...
|
616
|
+
# I guess give up?
|
617
|
+
self._check_response_error(req, hresp)
|
618
|
+
if hresp.headers.get(SYSMETA_VERSIONS_SYMLINK) == 'true':
|
619
|
+
symlink_target = hresp.headers.get(TGT_OBJ_SYMLINK_HDR)
|
620
|
+
drain_and_close(hresp)
|
621
|
+
return head_is_tombstone, symlink_target
|
622
|
+
|
623
|
+
def handle_delete_version(self, req, versions_cont, api_version,
|
624
|
+
account_name, container_name,
|
625
|
+
object_name, is_enabled, version):
|
626
|
+
if version == 'null':
|
627
|
+
# let the request go directly through to the is_latest link
|
628
|
+
return
|
629
|
+
auth_token_header = {'X-Auth-Token': req.headers.get('X-Auth-Token')}
|
630
|
+
head_is_tombstone, symlink_target = self._check_head(
|
631
|
+
req, auth_token_header)
|
632
|
+
|
633
|
+
versions_obj = self._build_versions_object_name(
|
634
|
+
object_name, version)
|
635
|
+
req_obj_path = '%s/%s' % (versions_cont, versions_obj)
|
636
|
+
if head_is_tombstone or not symlink_target or (
|
637
|
+
wsgi_unquote(symlink_target) != wsgi_unquote(req_obj_path)):
|
638
|
+
# If there's no current version (i.e., tombstone or unversioned
|
639
|
+
# object) or if current version links to another version, then
|
640
|
+
# just delete the version requested to be deleted
|
641
|
+
req.path_info = "/%s/%s/%s/%s" % (
|
642
|
+
api_version, account_name, versions_cont, versions_obj)
|
643
|
+
req.headers['X-Backend-Allow-Reserved-Names'] = 'true'
|
644
|
+
if head_is_tombstone or not symlink_target:
|
645
|
+
resp_version_id = 'null'
|
646
|
+
else:
|
647
|
+
_, vers_obj_name = wsgi_unquote(symlink_target).split('/', 1)
|
648
|
+
resp_version_id = self._split_version_from_name(
|
649
|
+
vers_obj_name)[1].internal
|
650
|
+
else:
|
651
|
+
# if version-id is the latest version, delete the link too
|
652
|
+
# First, kill the link...
|
653
|
+
req.environ['QUERY_STRING'] = ''
|
654
|
+
link_resp = req.get_response(self.app)
|
655
|
+
self._check_response_error(req, link_resp)
|
656
|
+
drain_and_close(link_resp)
|
657
|
+
|
658
|
+
# *then* the backing data
|
659
|
+
req.path_info = "/%s/%s/%s/%s" % (
|
660
|
+
api_version, account_name, versions_cont, versions_obj)
|
661
|
+
req.headers['X-Backend-Allow-Reserved-Names'] = 'true'
|
662
|
+
resp_version_id = 'null'
|
663
|
+
resp = req.get_response(self.app)
|
664
|
+
resp.headers['X-Object-Version-Id'] = version
|
665
|
+
resp.headers['X-Object-Current-Version-Id'] = resp_version_id
|
666
|
+
return resp
|
667
|
+
|
668
|
+
def handle_put_version(self, req, versions_cont, api_version, account_name,
|
669
|
+
container, object_name, is_enabled, version):
|
670
|
+
"""
|
671
|
+
Handle a PUT?version-id request and create/update the is_latest link to
|
672
|
+
point to the specific version. Expects a valid 'version' id.
|
673
|
+
"""
|
674
|
+
if req.is_chunked:
|
675
|
+
has_body = (req.body_file.read(1) != b'')
|
676
|
+
elif req.content_length is None:
|
677
|
+
raise HTTPLengthRequired(request=req)
|
678
|
+
else:
|
679
|
+
has_body = (req.content_length != 0)
|
680
|
+
if has_body:
|
681
|
+
raise HTTPBadRequest(
|
682
|
+
body='PUT version-id requests require a zero byte body',
|
683
|
+
request=req,
|
684
|
+
content_type='text/plain')
|
685
|
+
versions_obj_name = self._build_versions_object_name(
|
686
|
+
object_name, version)
|
687
|
+
versioned_obj_path = "/%s/%s/%s/%s" % (
|
688
|
+
api_version, account_name, versions_cont, versions_obj_name)
|
689
|
+
obj_head_headers = {'X-Backend-Allow-Reserved-Names': 'true'}
|
690
|
+
head_req = make_pre_authed_request(
|
691
|
+
req.environ, path=wsgi_quote(versioned_obj_path) + '?symlink=get',
|
692
|
+
method='HEAD', headers=obj_head_headers, swift_source='OV')
|
693
|
+
head_resp = head_req.get_response(self.app)
|
694
|
+
if head_resp.status_int == HTTP_NOT_FOUND:
|
695
|
+
drain_and_close(head_resp)
|
696
|
+
if is_success(get_container_info(
|
697
|
+
head_req.environ, self.app, swift_source='OV')['status']):
|
698
|
+
raise HTTPNotFound(
|
699
|
+
request=req, content_type='text/plain',
|
700
|
+
body=b'The specified version does not exist')
|
701
|
+
else:
|
702
|
+
raise HTTPInternalServerError(
|
703
|
+
request=req, content_type='text/plain',
|
704
|
+
body=b'The versions container does not exist. You may '
|
705
|
+
b'want to re-enable object versioning.')
|
706
|
+
|
707
|
+
self._check_response_error(req, head_resp)
|
708
|
+
drain_and_close(head_resp)
|
709
|
+
|
710
|
+
put_etag = head_resp.headers['ETag']
|
711
|
+
put_bytes = head_resp.content_length
|
712
|
+
put_content_type = head_resp.headers['Content-Type']
|
713
|
+
resp = self._put_symlink_to_version(
|
714
|
+
req, versions_cont, versions_obj_name, api_version, account_name,
|
715
|
+
object_name, put_etag, put_bytes, put_content_type)
|
716
|
+
return resp
|
717
|
+
|
718
|
+
def handle_versioned_request(self, req, versions_cont, api_version,
|
719
|
+
account, container, obj, is_enabled, version):
|
720
|
+
"""
|
721
|
+
Handle 'version-id' request for object resource. When a request
|
722
|
+
contains a ``version-id=<id>`` parameter, the request is acted upon
|
723
|
+
the actual version of that object. Version-aware operations
|
724
|
+
require that the container is versioned, but do not require that
|
725
|
+
the versioning is currently enabled. Users should be able to
|
726
|
+
operate on older versions of an object even if versioning is
|
727
|
+
currently suspended.
|
728
|
+
|
729
|
+
PUT and POST requests are not allowed as that would overwrite
|
730
|
+
the contents of the versioned object.
|
731
|
+
|
732
|
+
:param req: The original request
|
733
|
+
:param versions_cont: container holding versions of the requested obj
|
734
|
+
:param api_version: should be v1 unless swift bumps api version
|
735
|
+
:param account: account name string
|
736
|
+
:param container: container name string
|
737
|
+
:param object: object name string
|
738
|
+
:param is_enabled: is versioning currently enabled
|
739
|
+
:param version: version of the object to act on
|
740
|
+
"""
|
741
|
+
# ?version-id requests are allowed for GET, HEAD, DELETE reqs
|
742
|
+
if req.method == 'POST':
|
743
|
+
raise HTTPBadRequest(
|
744
|
+
'%s to a specific version is not allowed' % req.method,
|
745
|
+
request=req)
|
746
|
+
elif not versions_cont and version != 'null':
|
747
|
+
raise HTTPBadRequest(
|
748
|
+
'version-aware operations require that the container is '
|
749
|
+
'versioned', request=req)
|
750
|
+
if version != 'null':
|
751
|
+
try:
|
752
|
+
Timestamp(version)
|
753
|
+
except ValueError:
|
754
|
+
raise HTTPBadRequest('Invalid version parameter', request=req)
|
755
|
+
|
756
|
+
if req.method == 'DELETE':
|
757
|
+
return self.handle_delete_version(
|
758
|
+
req, versions_cont, api_version, account,
|
759
|
+
container, obj, is_enabled, version)
|
760
|
+
elif req.method == 'PUT':
|
761
|
+
return self.handle_put_version(
|
762
|
+
req, versions_cont, api_version, account,
|
763
|
+
container, obj, is_enabled, version)
|
764
|
+
if version == 'null':
|
765
|
+
resp = req.get_response(self.app)
|
766
|
+
if resp.is_success:
|
767
|
+
if get_reserved_name('versions', '') in wsgi_unquote(
|
768
|
+
resp.headers.get('Content-Location', '')):
|
769
|
+
# Have a latest version, but it's got a real version-id.
|
770
|
+
# Since the user specifically asked for null, return 404
|
771
|
+
close_if_possible(resp.app_iter)
|
772
|
+
raise HTTPNotFound(request=req)
|
773
|
+
resp.headers['X-Object-Version-Id'] = 'null'
|
774
|
+
if req.method == 'HEAD':
|
775
|
+
drain_and_close(resp)
|
776
|
+
return resp
|
777
|
+
else:
|
778
|
+
# Re-write the path; most everything else goes through normally
|
779
|
+
req.path_info = "/%s/%s/%s/%s" % (
|
780
|
+
api_version, account, versions_cont,
|
781
|
+
self._build_versions_object_name(obj, version))
|
782
|
+
req.headers['X-Backend-Allow-Reserved-Names'] = 'true'
|
783
|
+
|
784
|
+
resp = req.get_response(self.app)
|
785
|
+
if resp.is_success:
|
786
|
+
resp.headers['X-Object-Version-Id'] = version
|
787
|
+
|
788
|
+
# Well, except for some delete marker business...
|
789
|
+
is_del_marker = DELETE_MARKER_CONTENT_TYPE == resp.headers.get(
|
790
|
+
'X-Backend-Content-Type', resp.headers['Content-Type'])
|
791
|
+
|
792
|
+
if req.method == 'HEAD':
|
793
|
+
drain_and_close(resp)
|
794
|
+
|
795
|
+
if is_del_marker:
|
796
|
+
hdrs = {'X-Object-Version-Id': version,
|
797
|
+
'Content-Type': DELETE_MARKER_CONTENT_TYPE}
|
798
|
+
raise HTTPNotFound(request=req, headers=hdrs)
|
799
|
+
return resp
|
800
|
+
|
801
|
+
def handle_request(self, req, versions_cont, api_version, account,
|
802
|
+
container, obj, is_enabled):
|
803
|
+
if req.method == 'PUT':
|
804
|
+
return self.handle_put(
|
805
|
+
req, versions_cont, api_version, account, obj,
|
806
|
+
is_enabled)
|
807
|
+
elif req.method == 'POST':
|
808
|
+
return self.handle_post(req, versions_cont, account)
|
809
|
+
elif req.method == 'DELETE':
|
810
|
+
return self.handle_delete(
|
811
|
+
req, versions_cont, api_version, account,
|
812
|
+
container, obj, is_enabled)
|
813
|
+
|
814
|
+
# GET/HEAD/OPTIONS
|
815
|
+
resp = req.get_response(self.app)
|
816
|
+
|
817
|
+
resp.headers['X-Object-Version-Id'] = 'null'
|
818
|
+
# Check for a "real" version
|
819
|
+
loc = wsgi_unquote(resp.headers.get('Content-Location', ''))
|
820
|
+
if loc:
|
821
|
+
_, acct, cont, version_obj = split_path(loc, 4, 4, True)
|
822
|
+
if acct == account and cont == versions_cont:
|
823
|
+
_, version = self._split_version_from_name(version_obj)
|
824
|
+
if version is not None:
|
825
|
+
resp.headers['X-Object-Version-Id'] = version.internal
|
826
|
+
content_loc = wsgi_quote('/%s/%s/%s/%s' % (
|
827
|
+
api_version, account, container, obj,
|
828
|
+
)) + '?version-id=%s' % (version.internal,)
|
829
|
+
resp.headers['Content-Location'] = content_loc
|
830
|
+
symlink_target = wsgi_unquote(resp.headers.get('X-Symlink-Target', ''))
|
831
|
+
if symlink_target:
|
832
|
+
cont, version_obj = split_path('/%s' % symlink_target, 2, 2, True)
|
833
|
+
if cont == versions_cont:
|
834
|
+
_, version = self._split_version_from_name(version_obj)
|
835
|
+
if version is not None:
|
836
|
+
resp.headers['X-Object-Version-Id'] = version.internal
|
837
|
+
symlink_target = wsgi_quote('%s/%s' % (container, obj)) + \
|
838
|
+
'?version-id=%s' % (version.internal,)
|
839
|
+
resp.headers['X-Symlink-Target'] = symlink_target
|
840
|
+
return resp
|
841
|
+
|
842
|
+
|
843
|
+
class ContainerContext(ObjectVersioningContext):
|
844
|
+
def handle_request(self, req, start_response):
|
845
|
+
"""
|
846
|
+
Handle request for container resource.
|
847
|
+
|
848
|
+
On PUT, POST set version location and enabled flag sysmeta.
|
849
|
+
For container listings of a versioned container, update the object's
|
850
|
+
bytes and etag to use the target's instead of using the symlink info.
|
851
|
+
"""
|
852
|
+
app_resp = self._app_call(req.environ)
|
853
|
+
_, account, container, _ = req.split_path(3, 4, True)
|
854
|
+
location = ''
|
855
|
+
curr_bytes = 0
|
856
|
+
bytes_idx = -1
|
857
|
+
for i, (header, value) in enumerate(self._response_headers):
|
858
|
+
if header == 'X-Container-Bytes-Used':
|
859
|
+
curr_bytes = value
|
860
|
+
bytes_idx = i
|
861
|
+
if header.lower() == SYSMETA_VERSIONS_CONT:
|
862
|
+
location = value
|
863
|
+
if header.lower() == SYSMETA_VERSIONS_ENABLED:
|
864
|
+
self._response_headers.extend([
|
865
|
+
(CLIENT_VERSIONS_ENABLED.title(), value)])
|
866
|
+
|
867
|
+
if location:
|
868
|
+
location = wsgi_unquote(location)
|
869
|
+
|
870
|
+
# update bytes header
|
871
|
+
if bytes_idx > -1:
|
872
|
+
head_req = make_pre_authed_request(
|
873
|
+
req.environ, method='HEAD', swift_source='OV',
|
874
|
+
path=wsgi_quote('/v1/%s/%s' % (account, location)),
|
875
|
+
headers={'X-Backend-Allow-Reserved-Names': 'true'})
|
876
|
+
vresp = head_req.get_response(self.app)
|
877
|
+
if vresp.is_success:
|
878
|
+
ver_bytes = vresp.headers.get('X-Container-Bytes-Used', 0)
|
879
|
+
self._response_headers[bytes_idx] = (
|
880
|
+
'X-Container-Bytes-Used',
|
881
|
+
str(int(curr_bytes) + int(ver_bytes)))
|
882
|
+
drain_and_close(vresp)
|
883
|
+
elif is_success(self._get_status_int()):
|
884
|
+
# If client is doing a version-aware listing for a container that
|
885
|
+
# (as best we could tell) has never had versioning enabled,
|
886
|
+
# err on the side of there being data anyway -- the metadata we
|
887
|
+
# found may not be the most up-to-date.
|
888
|
+
|
889
|
+
# Note that any extra listing request we make will likely 404.
|
890
|
+
try:
|
891
|
+
location = self._build_versions_container_name(container)
|
892
|
+
except ValueError:
|
893
|
+
# may be internal listing to a reserved namespace container
|
894
|
+
pass
|
895
|
+
# else, we won't need location anyway
|
896
|
+
|
897
|
+
if is_success(self._get_status_int()) and req.method == 'GET':
|
898
|
+
with closing_if_possible(app_resp):
|
899
|
+
body = b''.join(app_resp)
|
900
|
+
try:
|
901
|
+
listing = json.loads(body)
|
902
|
+
except ValueError:
|
903
|
+
app_resp = [body]
|
904
|
+
else:
|
905
|
+
for item in listing:
|
906
|
+
if not all(x in item for x in (
|
907
|
+
'symlink_path',
|
908
|
+
'symlink_etag',
|
909
|
+
'symlink_bytes')):
|
910
|
+
continue
|
911
|
+
path = wsgi_unquote(bytes_to_wsgi(
|
912
|
+
item['symlink_path'].encode('utf-8')))
|
913
|
+
_, tgt_acct, tgt_container, tgt_obj = split_path(
|
914
|
+
path, 4, 4, True)
|
915
|
+
if tgt_container != location:
|
916
|
+
# if the archive container changed, leave the extra
|
917
|
+
# info unmodified
|
918
|
+
continue
|
919
|
+
_, meta = parse_header(item['hash'])
|
920
|
+
tgt_bytes = int(item.pop('symlink_bytes'))
|
921
|
+
item['bytes'] = tgt_bytes
|
922
|
+
item['version_symlink'] = True
|
923
|
+
item['hash'] = item.pop('symlink_etag') + ''.join(
|
924
|
+
'; %s=%s' % (k, v) for k, v in meta.items())
|
925
|
+
tgt_obj, version = self._split_version_from_name(tgt_obj)
|
926
|
+
if version is not None and 'versions' not in req.params:
|
927
|
+
sp = wsgi_quote('/v1/%s/%s/%s' % (
|
928
|
+
tgt_acct, container, tgt_obj,
|
929
|
+
)) + '?version-id=' + version.internal
|
930
|
+
item['symlink_path'] = sp
|
931
|
+
|
932
|
+
if 'versions' in req.params:
|
933
|
+
return self._list_versions(
|
934
|
+
req, start_response, location,
|
935
|
+
listing)
|
936
|
+
|
937
|
+
body = json.dumps(listing).encode('ascii')
|
938
|
+
self.update_content_length(len(body))
|
939
|
+
app_resp = [body]
|
940
|
+
|
941
|
+
start_response(self._response_status,
|
942
|
+
self._response_headers,
|
943
|
+
self._response_exc_info)
|
944
|
+
return app_resp
|
945
|
+
|
946
|
+
def handle_delete(self, req, start_response):
|
947
|
+
"""
|
948
|
+
Handle request to delete a user's container.
|
949
|
+
|
950
|
+
As part of deleting a container, this middleware will also delete
|
951
|
+
the hidden container holding object versions.
|
952
|
+
|
953
|
+
Before a user's container can be deleted, swift must check
|
954
|
+
if there are still old object versions from that container.
|
955
|
+
Only after disabling versioning and deleting *all* object versions
|
956
|
+
can a container be deleted.
|
957
|
+
"""
|
958
|
+
container_info = get_container_info(req.environ, self.app,
|
959
|
+
swift_source='OV')
|
960
|
+
|
961
|
+
versions_cont = unquote(container_info.get(
|
962
|
+
'sysmeta', {}).get('versions-container', ''))
|
963
|
+
|
964
|
+
if versions_cont:
|
965
|
+
account = req.split_path(3, 3, True)[1]
|
966
|
+
# using a HEAD request here as opposed to get_container_info
|
967
|
+
# to make sure we get an up-to-date value
|
968
|
+
versions_req = make_pre_authed_request(
|
969
|
+
req.environ, method='HEAD', swift_source='OV',
|
970
|
+
path=wsgi_quote('/v1/%s/%s' % (
|
971
|
+
account, str_to_wsgi(versions_cont))),
|
972
|
+
headers={'X-Backend-Allow-Reserved-Names': 'true'})
|
973
|
+
vresp = versions_req.get_response(self.app)
|
974
|
+
drain_and_close(vresp)
|
975
|
+
if vresp.is_success and int(vresp.headers.get(
|
976
|
+
'X-Container-Object-Count', 0)) > 0:
|
977
|
+
raise HTTPConflict(
|
978
|
+
'Delete all versions before deleting container.',
|
979
|
+
request=req)
|
980
|
+
elif not vresp.is_success and vresp.status_int != 404:
|
981
|
+
raise HTTPInternalServerError(
|
982
|
+
'Error deleting versioned container')
|
983
|
+
else:
|
984
|
+
versions_req.method = 'DELETE'
|
985
|
+
resp = versions_req.get_response(self.app)
|
986
|
+
drain_and_close(resp)
|
987
|
+
if not is_success(resp.status_int) and resp.status_int != 404:
|
988
|
+
raise HTTPInternalServerError(
|
989
|
+
'Error deleting versioned container')
|
990
|
+
|
991
|
+
app_resp = self._app_call(req.environ)
|
992
|
+
|
993
|
+
start_response(self._response_status,
|
994
|
+
self._response_headers,
|
995
|
+
self._response_exc_info)
|
996
|
+
return app_resp
|
997
|
+
|
998
|
+
def enable_versioning(self, req, start_response):
|
999
|
+
container_info = get_container_info(req.environ, self.app,
|
1000
|
+
swift_source='OV')
|
1001
|
+
|
1002
|
+
# if container is already configured to use old style versioning,
|
1003
|
+
# we don't allow user to enable object versioning here. They must
|
1004
|
+
# choose which middleware to use, only one style of versioning
|
1005
|
+
# is supported for a given container
|
1006
|
+
versions_cont = container_info.get(
|
1007
|
+
'sysmeta', {}).get('versions-location')
|
1008
|
+
legacy_versions_cont = container_info.get('versions')
|
1009
|
+
if versions_cont or legacy_versions_cont:
|
1010
|
+
raise HTTPBadRequest(
|
1011
|
+
'Cannot enable object versioning on a container '
|
1012
|
+
'that is already using the legacy versioned writes '
|
1013
|
+
'feature.',
|
1014
|
+
request=req)
|
1015
|
+
|
1016
|
+
# versioning and container-sync do not yet work well together
|
1017
|
+
# container-sync needs to be enhanced to sync previous versions
|
1018
|
+
sync_to = container_info.get('sync_to')
|
1019
|
+
if sync_to:
|
1020
|
+
raise HTTPBadRequest(
|
1021
|
+
'Cannot enable object versioning on a container '
|
1022
|
+
'configured as source of container syncing.',
|
1023
|
+
request=req)
|
1024
|
+
|
1025
|
+
versions_cont = container_info.get(
|
1026
|
+
'sysmeta', {}).get('versions-container')
|
1027
|
+
is_enabled = config_true_value(
|
1028
|
+
req.headers[CLIENT_VERSIONS_ENABLED])
|
1029
|
+
|
1030
|
+
req.headers[SYSMETA_VERSIONS_ENABLED] = is_enabled
|
1031
|
+
|
1032
|
+
# TODO: a POST request to a primary container that doesn't exist
|
1033
|
+
# will fail, so we will create and delete the versions container
|
1034
|
+
# for no reason
|
1035
|
+
if config_true_value(is_enabled):
|
1036
|
+
(version, account, container, _) = req.split_path(3, 4, True)
|
1037
|
+
|
1038
|
+
# Attempt to use same policy as primary container, otherwise
|
1039
|
+
# use default policy
|
1040
|
+
if is_success(container_info['status']):
|
1041
|
+
primary_policy_idx = container_info['storage_policy']
|
1042
|
+
if POLICIES[primary_policy_idx].is_deprecated:
|
1043
|
+
# Do an auth check now, so we don't leak information
|
1044
|
+
# about the container
|
1045
|
+
aresp = req.environ['swift.authorize'](req)
|
1046
|
+
if aresp:
|
1047
|
+
raise aresp
|
1048
|
+
|
1049
|
+
# Proxy controller would catch the deprecated policy, too,
|
1050
|
+
# but waiting until then would mean the error message
|
1051
|
+
# would be a generic "Error enabling object versioning".
|
1052
|
+
raise HTTPBadRequest(
|
1053
|
+
'Cannot enable object versioning on a container '
|
1054
|
+
'that uses a deprecated storage policy.',
|
1055
|
+
request=req)
|
1056
|
+
hdrs = {'X-Storage-Policy': POLICIES[primary_policy_idx].name}
|
1057
|
+
else:
|
1058
|
+
if req.method == 'PUT' and \
|
1059
|
+
'X-Storage-Policy' in req.headers:
|
1060
|
+
hdrs = {'X-Storage-Policy':
|
1061
|
+
req.headers['X-Storage-Policy']}
|
1062
|
+
else:
|
1063
|
+
hdrs = {}
|
1064
|
+
hdrs['X-Backend-Allow-Reserved-Names'] = 'true'
|
1065
|
+
|
1066
|
+
versions_cont = self._build_versions_container_name(container)
|
1067
|
+
versions_cont_path = "/%s/%s/%s" % (
|
1068
|
+
version, account, versions_cont)
|
1069
|
+
ver_cont_req = make_pre_authed_request(
|
1070
|
+
req.environ, path=wsgi_quote(versions_cont_path),
|
1071
|
+
method='PUT', headers=hdrs, swift_source='OV')
|
1072
|
+
resp = ver_cont_req.get_response(self.app)
|
1073
|
+
# Should always be short; consume the body
|
1074
|
+
drain_and_close(resp)
|
1075
|
+
if is_success(resp.status_int) or resp.status_int == HTTP_CONFLICT:
|
1076
|
+
req.headers[SYSMETA_VERSIONS_CONT] = wsgi_quote(versions_cont)
|
1077
|
+
else:
|
1078
|
+
raise HTTPInternalServerError(
|
1079
|
+
'Error enabling object versioning')
|
1080
|
+
|
1081
|
+
# make original request
|
1082
|
+
app_resp = self._app_call(req.environ)
|
1083
|
+
|
1084
|
+
# if we just created a versions container but the original
|
1085
|
+
# request failed, delete the versions container
|
1086
|
+
# and let user retry later
|
1087
|
+
if not is_success(self._get_status_int()) and \
|
1088
|
+
SYSMETA_VERSIONS_CONT in req.headers:
|
1089
|
+
versions_cont_path = "/%s/%s/%s" % (
|
1090
|
+
version, account, versions_cont)
|
1091
|
+
ver_cont_req = make_pre_authed_request(
|
1092
|
+
req.environ, path=wsgi_quote(versions_cont_path),
|
1093
|
+
method='DELETE', headers=hdrs, swift_source='OV')
|
1094
|
+
|
1095
|
+
# TODO: what if this one fails??
|
1096
|
+
resp = ver_cont_req.get_response(self.app)
|
1097
|
+
drain_and_close(resp)
|
1098
|
+
|
1099
|
+
if self._response_headers is None:
|
1100
|
+
self._response_headers = []
|
1101
|
+
for key, val in self._response_headers:
|
1102
|
+
if key.lower() == SYSMETA_VERSIONS_ENABLED:
|
1103
|
+
self._response_headers.extend([
|
1104
|
+
(CLIENT_VERSIONS_ENABLED.title(), val)])
|
1105
|
+
|
1106
|
+
start_response(self._response_status,
|
1107
|
+
self._response_headers,
|
1108
|
+
self._response_exc_info)
|
1109
|
+
return app_resp
|
1110
|
+
|
1111
|
+
def _list_versions(self, req, start_response, location, primary_listing):
|
1112
|
+
# Only supports JSON listings
|
1113
|
+
req.environ['swift.format_listing'] = False
|
1114
|
+
if not req.accept.best_match(['application/json']):
|
1115
|
+
raise HTTPNotAcceptable(request=req)
|
1116
|
+
|
1117
|
+
params = req.params
|
1118
|
+
if 'version_marker' in params:
|
1119
|
+
if 'marker' not in params:
|
1120
|
+
raise HTTPBadRequest('version_marker param requires marker')
|
1121
|
+
|
1122
|
+
if params['version_marker'] != 'null':
|
1123
|
+
try:
|
1124
|
+
ts = Timestamp(params.pop('version_marker'))
|
1125
|
+
except ValueError:
|
1126
|
+
raise HTTPBadRequest('invalid version_marker param')
|
1127
|
+
|
1128
|
+
params['marker'] = self._build_versions_object_name(
|
1129
|
+
params['marker'], ts)
|
1130
|
+
elif 'marker' in params:
|
1131
|
+
params['marker'] = self._build_versions_object_prefix(
|
1132
|
+
params['marker']) + ':' # just past all numbers
|
1133
|
+
|
1134
|
+
delim = params.get('delimiter', '')
|
1135
|
+
# Exclude the set of chars used in version_id from user delimiters
|
1136
|
+
if set(delim).intersection('0123456789.%s' % RESERVED_STR):
|
1137
|
+
raise HTTPBadRequest('invalid delimiter param')
|
1138
|
+
|
1139
|
+
null_listing = []
|
1140
|
+
subdir_set = set()
|
1141
|
+
current_versions = {}
|
1142
|
+
is_latest_set = set()
|
1143
|
+
for item in primary_listing:
|
1144
|
+
if 'name' not in item:
|
1145
|
+
subdir_set.add(item['subdir'])
|
1146
|
+
else:
|
1147
|
+
if item.get('version_symlink'):
|
1148
|
+
path = wsgi_to_str(wsgi_unquote(bytes_to_wsgi(
|
1149
|
+
item['symlink_path'].encode('utf-8'))))
|
1150
|
+
current_versions[path] = item
|
1151
|
+
else:
|
1152
|
+
null_listing.append(dict(
|
1153
|
+
item, version_id='null', is_latest=True))
|
1154
|
+
is_latest_set.add(item['name'])
|
1155
|
+
|
1156
|
+
account = req.split_path(3, 3, True)[1]
|
1157
|
+
versions_req = make_pre_authed_request(
|
1158
|
+
req.environ, method='GET', swift_source='OV',
|
1159
|
+
path=wsgi_quote('/v1/%s/%s' % (account, location)),
|
1160
|
+
headers={'X-Backend-Allow-Reserved-Names': 'true'},
|
1161
|
+
)
|
1162
|
+
# NB: Not using self._build_versions_object_name here because
|
1163
|
+
# we don't want to bookend the prefix with RESERVED_NAME as user
|
1164
|
+
# could be using just part of object name as the prefix.
|
1165
|
+
if 'prefix' in params:
|
1166
|
+
params['prefix'] = get_reserved_name(params['prefix'])
|
1167
|
+
|
1168
|
+
# NB: no end_marker support (yet)
|
1169
|
+
if get_container_info(versions_req.environ, self.app,
|
1170
|
+
swift_source='OV')['status'] == 404:
|
1171
|
+
# we don't usually like to LBYL like this, but 404s tend to be
|
1172
|
+
# expensive (since we check all primaries and a bunch of handoffs)
|
1173
|
+
# and we expect this to be a reasonably common way to listing
|
1174
|
+
# objects since it's more complete from the user's perspective
|
1175
|
+
# (see also: s3api and that client ecosystem)
|
1176
|
+
versions_resp = None
|
1177
|
+
else:
|
1178
|
+
versions_req.params = {
|
1179
|
+
k: params.get(k, '') for k in (
|
1180
|
+
'prefix', 'marker', 'limit', 'delimiter', 'reverse')}
|
1181
|
+
versions_resp = versions_req.get_response(self.app)
|
1182
|
+
|
1183
|
+
if versions_resp is None \
|
1184
|
+
or versions_resp.status_int == HTTP_NOT_FOUND:
|
1185
|
+
subdir_listing = [{'subdir': s} for s in subdir_set]
|
1186
|
+
broken_listing = []
|
1187
|
+
for item in current_versions.values():
|
1188
|
+
linked_name = wsgi_to_str(wsgi_unquote(bytes_to_wsgi(
|
1189
|
+
item['symlink_path'].encode('utf8')))).split('/', 4)[-1]
|
1190
|
+
name, ts = self._split_version_from_name(linked_name)
|
1191
|
+
if ts is None:
|
1192
|
+
continue
|
1193
|
+
is_latest = False
|
1194
|
+
if name not in is_latest_set:
|
1195
|
+
is_latest_set.add(name)
|
1196
|
+
is_latest = True
|
1197
|
+
broken_listing.append({
|
1198
|
+
'name': name,
|
1199
|
+
'is_latest': is_latest,
|
1200
|
+
'version_id': ts.internal,
|
1201
|
+
'content_type': item['content_type'],
|
1202
|
+
'bytes': item['bytes'],
|
1203
|
+
'hash': item['hash'],
|
1204
|
+
'last_modified': item['last_modified'],
|
1205
|
+
})
|
1206
|
+
limit = constrain_req_limit(req, CONTAINER_LISTING_LIMIT)
|
1207
|
+
body = build_listing(
|
1208
|
+
null_listing, subdir_listing, broken_listing,
|
1209
|
+
reverse=config_true_value(params.get('reverse', 'no')),
|
1210
|
+
limit=limit)
|
1211
|
+
self.update_content_length(len(body))
|
1212
|
+
app_resp = [body]
|
1213
|
+
drain_and_close(versions_resp)
|
1214
|
+
elif is_success(versions_resp.status_int):
|
1215
|
+
try:
|
1216
|
+
listing = json.loads(versions_resp.body)
|
1217
|
+
except ValueError:
|
1218
|
+
app_resp = [body]
|
1219
|
+
else:
|
1220
|
+
versions_listing = []
|
1221
|
+
for item in listing:
|
1222
|
+
if 'name' not in item:
|
1223
|
+
# remove reserved chars from subdir
|
1224
|
+
subdir = split_reserved_name(item['subdir'])[0]
|
1225
|
+
subdir_set.add(subdir)
|
1226
|
+
else:
|
1227
|
+
name, ts = self._split_version_from_name(item['name'])
|
1228
|
+
if ts is None:
|
1229
|
+
continue
|
1230
|
+
path = '/v1/%s/%s/%s' % (
|
1231
|
+
wsgi_to_str(account),
|
1232
|
+
wsgi_to_str(location),
|
1233
|
+
item['name'])
|
1234
|
+
|
1235
|
+
if path in current_versions:
|
1236
|
+
item['is_latest'] = True
|
1237
|
+
is_latest_set.add(name)
|
1238
|
+
del current_versions[path]
|
1239
|
+
elif (item['content_type'] ==
|
1240
|
+
DELETE_MARKER_CONTENT_TYPE
|
1241
|
+
and name not in is_latest_set):
|
1242
|
+
item['is_latest'] = True
|
1243
|
+
is_latest_set.add(name)
|
1244
|
+
else:
|
1245
|
+
item['is_latest'] = False
|
1246
|
+
|
1247
|
+
item['name'] = name
|
1248
|
+
item['version_id'] = ts.internal
|
1249
|
+
versions_listing.append(item)
|
1250
|
+
|
1251
|
+
subdir_listing = [{'subdir': s} for s in subdir_set]
|
1252
|
+
broken_listing = []
|
1253
|
+
for item in current_versions.values():
|
1254
|
+
link_path = wsgi_to_str(wsgi_unquote(bytes_to_wsgi(
|
1255
|
+
item['symlink_path'].encode('utf-8'))))
|
1256
|
+
name, ts = self._split_version_from_name(
|
1257
|
+
link_path.split('/', 1)[1])
|
1258
|
+
if ts is None:
|
1259
|
+
continue
|
1260
|
+
broken_listing.append({
|
1261
|
+
'name': name,
|
1262
|
+
'is_latest': True,
|
1263
|
+
'version_id': ts.internal,
|
1264
|
+
'content_type': item['content_type'],
|
1265
|
+
'bytes': item['bytes'],
|
1266
|
+
'hash': item['hash'],
|
1267
|
+
'last_modified': item['last_modified'],
|
1268
|
+
})
|
1269
|
+
|
1270
|
+
limit = constrain_req_limit(req, CONTAINER_LISTING_LIMIT)
|
1271
|
+
body = build_listing(
|
1272
|
+
null_listing, versions_listing,
|
1273
|
+
subdir_listing, broken_listing,
|
1274
|
+
reverse=config_true_value(params.get('reverse', 'no')),
|
1275
|
+
limit=limit,
|
1276
|
+
)
|
1277
|
+
self.update_content_length(len(body))
|
1278
|
+
app_resp = [body]
|
1279
|
+
else:
|
1280
|
+
return versions_resp(versions_req.environ, start_response)
|
1281
|
+
|
1282
|
+
start_response(self._response_status,
|
1283
|
+
self._response_headers,
|
1284
|
+
self._response_exc_info)
|
1285
|
+
return app_resp
|
1286
|
+
|
1287
|
+
|
1288
|
+
class AccountContext(ObjectVersioningContext):
|
1289
|
+
def list_containers(self, req, api_version, account, start_response):
|
1290
|
+
app_resp = self._app_call(req.environ)
|
1291
|
+
|
1292
|
+
if is_success(self._get_status_int()):
|
1293
|
+
with closing_if_possible(app_resp):
|
1294
|
+
body = b''.join(app_resp)
|
1295
|
+
try:
|
1296
|
+
listing = json.loads(body)
|
1297
|
+
except ValueError:
|
1298
|
+
app_resp = [body]
|
1299
|
+
else:
|
1300
|
+
# list hidden versions containers
|
1301
|
+
# It might be necessary to issue multiple listing requests
|
1302
|
+
# because of paging limitations, hence the while loop.
|
1303
|
+
params = req.params
|
1304
|
+
versions_dict = {}
|
1305
|
+
versions_req = make_pre_authed_request(
|
1306
|
+
req.environ, method='GET', swift_source='OV',
|
1307
|
+
path=wsgi_quote('/v1/%s' % account),
|
1308
|
+
headers={'X-Backend-Allow-Reserved-Names': 'true'},
|
1309
|
+
)
|
1310
|
+
if 'prefix' in params:
|
1311
|
+
try:
|
1312
|
+
params['prefix'] = \
|
1313
|
+
self._build_versions_container_name(
|
1314
|
+
params['prefix'])
|
1315
|
+
except ValueError:
|
1316
|
+
# don't touch params['prefix'],
|
1317
|
+
# RESERVED_STR probably came from looping around
|
1318
|
+
pass
|
1319
|
+
else:
|
1320
|
+
params['prefix'] = get_reserved_name('versions')
|
1321
|
+
|
1322
|
+
for p in ('marker', 'end_marker'):
|
1323
|
+
if p in params:
|
1324
|
+
try:
|
1325
|
+
params[p] = \
|
1326
|
+
self._build_versions_container_name(
|
1327
|
+
params[p])
|
1328
|
+
except ValueError:
|
1329
|
+
# don't touch params[p]
|
1330
|
+
pass
|
1331
|
+
|
1332
|
+
versions_req.params = params
|
1333
|
+
versions_resp = versions_req.get_response(self.app)
|
1334
|
+
try:
|
1335
|
+
versions_listing = json.loads(versions_resp.body)
|
1336
|
+
except ValueError:
|
1337
|
+
versions_listing = []
|
1338
|
+
finally:
|
1339
|
+
close_if_possible(versions_resp.app_iter)
|
1340
|
+
|
1341
|
+
# create a dict from versions listing to facilitate
|
1342
|
+
# look-up by name. Ignore 'subdir' items
|
1343
|
+
for item in [item for item in versions_listing
|
1344
|
+
if 'name' in item]:
|
1345
|
+
container_name = self._split_versions_container_name(
|
1346
|
+
item['name'])
|
1347
|
+
versions_dict[container_name] = item
|
1348
|
+
|
1349
|
+
# update bytes from original listing with bytes from
|
1350
|
+
# versions cont
|
1351
|
+
if len(versions_dict) > 0:
|
1352
|
+
# ignore 'subdir' items
|
1353
|
+
for item in [item for item in listing if 'name' in item]:
|
1354
|
+
if item['name'] in versions_dict:
|
1355
|
+
v_info = versions_dict.pop(item['name'])
|
1356
|
+
item['bytes'] = item['bytes'] + v_info['bytes']
|
1357
|
+
|
1358
|
+
# if there are items left in versions_dict, it indicates an
|
1359
|
+
# error scenario where there are orphan hidden containers
|
1360
|
+
# (possibly storing data) that should have been deleted
|
1361
|
+
# along with the primary container. In this case, let's add
|
1362
|
+
# those containers to listing so users can be aware and
|
1363
|
+
# clean them up
|
1364
|
+
for key, item in versions_dict.items():
|
1365
|
+
item['name'] = key
|
1366
|
+
item['count'] = 0 # None of these are current
|
1367
|
+
listing.append(item)
|
1368
|
+
|
1369
|
+
limit = constrain_req_limit(req, ACCOUNT_LISTING_LIMIT)
|
1370
|
+
body = build_listing(
|
1371
|
+
listing,
|
1372
|
+
reverse=config_true_value(params.get('reverse', 'no')),
|
1373
|
+
limit=limit,
|
1374
|
+
)
|
1375
|
+
self.update_content_length(len(body))
|
1376
|
+
app_resp = [body]
|
1377
|
+
|
1378
|
+
start_response(self._response_status,
|
1379
|
+
self._response_headers,
|
1380
|
+
self._response_exc_info)
|
1381
|
+
return app_resp
|
1382
|
+
|
1383
|
+
|
1384
|
+
class ObjectVersioningMiddleware(object):
|
1385
|
+
|
1386
|
+
def __init__(self, app, conf):
|
1387
|
+
self.app = app
|
1388
|
+
self.conf = conf
|
1389
|
+
self.logger = get_logger(conf, log_route='object_versioning')
|
1390
|
+
|
1391
|
+
# Pass these along so get_container_info will have the configured
|
1392
|
+
# odds to skip cache
|
1393
|
+
_pipeline_final_app = app_property('_pipeline_final_app')
|
1394
|
+
_pipeline_request_logging_app = app_property(
|
1395
|
+
'_pipeline_request_logging_app')
|
1396
|
+
|
1397
|
+
def account_request(self, req, api_version, account, start_response):
|
1398
|
+
account_ctx = AccountContext(self.app, self.logger)
|
1399
|
+
if req.method == 'GET':
|
1400
|
+
return account_ctx.list_containers(
|
1401
|
+
req, api_version, account, start_response)
|
1402
|
+
else:
|
1403
|
+
return self.app(req.environ, start_response)
|
1404
|
+
|
1405
|
+
def container_request(self, req, start_response):
|
1406
|
+
container_ctx = ContainerContext(self.app, self.logger)
|
1407
|
+
if req.method in ('PUT', 'POST') and \
|
1408
|
+
CLIENT_VERSIONS_ENABLED in req.headers:
|
1409
|
+
return container_ctx.enable_versioning(req, start_response)
|
1410
|
+
elif req.method == 'DELETE':
|
1411
|
+
return container_ctx.handle_delete(req, start_response)
|
1412
|
+
|
1413
|
+
# send request and translate sysmeta headers from response
|
1414
|
+
return container_ctx.handle_request(req, start_response)
|
1415
|
+
|
1416
|
+
def object_request(self, req, api_version, account, container, obj):
|
1417
|
+
"""
|
1418
|
+
Handle request for object resource.
|
1419
|
+
|
1420
|
+
Note that account, container, obj should be unquoted by caller
|
1421
|
+
if the url path is under url encoding (e.g. %FF)
|
1422
|
+
|
1423
|
+
:param req: swift.common.swob.Request instance
|
1424
|
+
:param api_version: should be v1 unless swift bumps api version
|
1425
|
+
:param account: account name string
|
1426
|
+
:param container: container name string
|
1427
|
+
:param object: object name string
|
1428
|
+
"""
|
1429
|
+
resp = None
|
1430
|
+
container_info = get_container_info(
|
1431
|
+
req.environ, self.app, swift_source='OV')
|
1432
|
+
|
1433
|
+
versions_cont = container_info.get(
|
1434
|
+
'sysmeta', {}).get('versions-container', '')
|
1435
|
+
is_enabled = config_true_value(container_info.get(
|
1436
|
+
'sysmeta', {}).get('versions-enabled'))
|
1437
|
+
|
1438
|
+
if versions_cont:
|
1439
|
+
versions_cont = wsgi_unquote(str_to_wsgi(
|
1440
|
+
versions_cont)).split('/')[0]
|
1441
|
+
|
1442
|
+
if req.params.get('version-id'):
|
1443
|
+
vw_ctx = ObjectContext(self.app, self.logger)
|
1444
|
+
resp = vw_ctx.handle_versioned_request(
|
1445
|
+
req, versions_cont, api_version, account, container, obj,
|
1446
|
+
is_enabled, req.params['version-id'])
|
1447
|
+
elif versions_cont:
|
1448
|
+
# handle object request for a enabled versioned container
|
1449
|
+
vw_ctx = ObjectContext(self.app, self.logger)
|
1450
|
+
resp = vw_ctx.handle_request(
|
1451
|
+
req, versions_cont, api_version, account, container, obj,
|
1452
|
+
is_enabled)
|
1453
|
+
|
1454
|
+
if resp:
|
1455
|
+
return resp
|
1456
|
+
else:
|
1457
|
+
return self.app
|
1458
|
+
|
1459
|
+
def __call__(self, env, start_response):
|
1460
|
+
req = Request(env)
|
1461
|
+
try:
|
1462
|
+
(api_version, account, container, obj) = req.split_path(2, 4, True)
|
1463
|
+
bad_path = False
|
1464
|
+
except ValueError:
|
1465
|
+
bad_path = True
|
1466
|
+
|
1467
|
+
# use of bad_path bool is to avoid recursive tracebacks
|
1468
|
+
if bad_path or not valid_api_version(api_version):
|
1469
|
+
return self.app(env, start_response)
|
1470
|
+
|
1471
|
+
try:
|
1472
|
+
if not container:
|
1473
|
+
return self.account_request(req, api_version, account,
|
1474
|
+
start_response)
|
1475
|
+
if container and not obj:
|
1476
|
+
return self.container_request(req, start_response)
|
1477
|
+
else:
|
1478
|
+
return self.object_request(
|
1479
|
+
req, api_version, account, container,
|
1480
|
+
obj)(env, start_response)
|
1481
|
+
except HTTPException as error_response:
|
1482
|
+
return error_response(env, start_response)
|