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.
Files changed (206) hide show
  1. swift/__init__.py +29 -50
  2. swift/account/auditor.py +21 -118
  3. swift/account/backend.py +33 -28
  4. swift/account/reaper.py +37 -28
  5. swift/account/replicator.py +22 -0
  6. swift/account/server.py +60 -26
  7. swift/account/utils.py +28 -11
  8. swift-2.23.3.data/scripts/swift-account-audit → swift/cli/account_audit.py +23 -13
  9. swift-2.23.3.data/scripts/swift-config → swift/cli/config.py +2 -2
  10. swift/cli/container_deleter.py +5 -11
  11. swift-2.23.3.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +8 -7
  12. swift/cli/dispersion_report.py +10 -9
  13. swift-2.23.3.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +63 -21
  14. swift/cli/form_signature.py +3 -7
  15. swift-2.23.3.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +8 -2
  16. swift/cli/info.py +154 -14
  17. swift/cli/manage_shard_ranges.py +705 -37
  18. swift-2.23.3.data/scripts/swift-oldies → swift/cli/oldies.py +25 -14
  19. swift-2.23.3.data/scripts/swift-orphans → swift/cli/orphans.py +7 -3
  20. swift/cli/recon.py +196 -67
  21. swift-2.23.3.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +17 -20
  22. swift-2.23.3.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
  23. swift/cli/relinker.py +807 -126
  24. swift/cli/reload.py +135 -0
  25. swift/cli/ringbuilder.py +217 -20
  26. swift/cli/ringcomposer.py +0 -1
  27. swift/cli/shard-info.py +4 -3
  28. swift/common/base_storage_server.py +9 -20
  29. swift/common/bufferedhttp.py +48 -74
  30. swift/common/constraints.py +20 -15
  31. swift/common/container_sync_realms.py +9 -11
  32. swift/common/daemon.py +25 -8
  33. swift/common/db.py +195 -128
  34. swift/common/db_auditor.py +168 -0
  35. swift/common/db_replicator.py +95 -55
  36. swift/common/digest.py +141 -0
  37. swift/common/direct_client.py +144 -33
  38. swift/common/error_limiter.py +93 -0
  39. swift/common/exceptions.py +25 -1
  40. swift/common/header_key_dict.py +2 -9
  41. swift/common/http_protocol.py +373 -0
  42. swift/common/internal_client.py +129 -59
  43. swift/common/linkat.py +3 -4
  44. swift/common/manager.py +284 -67
  45. swift/common/memcached.py +390 -145
  46. swift/common/middleware/__init__.py +4 -0
  47. swift/common/middleware/account_quotas.py +211 -46
  48. swift/common/middleware/acl.py +3 -8
  49. swift/common/middleware/backend_ratelimit.py +230 -0
  50. swift/common/middleware/bulk.py +22 -34
  51. swift/common/middleware/catch_errors.py +1 -3
  52. swift/common/middleware/cname_lookup.py +6 -11
  53. swift/common/middleware/container_quotas.py +1 -1
  54. swift/common/middleware/container_sync.py +39 -17
  55. swift/common/middleware/copy.py +12 -0
  56. swift/common/middleware/crossdomain.py +22 -9
  57. swift/common/middleware/crypto/__init__.py +2 -1
  58. swift/common/middleware/crypto/crypto_utils.py +11 -15
  59. swift/common/middleware/crypto/decrypter.py +28 -11
  60. swift/common/middleware/crypto/encrypter.py +12 -17
  61. swift/common/middleware/crypto/keymaster.py +8 -15
  62. swift/common/middleware/crypto/kms_keymaster.py +2 -1
  63. swift/common/middleware/dlo.py +15 -11
  64. swift/common/middleware/domain_remap.py +5 -4
  65. swift/common/middleware/etag_quoter.py +128 -0
  66. swift/common/middleware/formpost.py +73 -70
  67. swift/common/middleware/gatekeeper.py +8 -1
  68. swift/common/middleware/keystoneauth.py +33 -3
  69. swift/common/middleware/list_endpoints.py +4 -4
  70. swift/common/middleware/listing_formats.py +85 -49
  71. swift/common/middleware/memcache.py +4 -95
  72. swift/common/middleware/name_check.py +3 -2
  73. swift/common/middleware/proxy_logging.py +160 -92
  74. swift/common/middleware/ratelimit.py +17 -10
  75. swift/common/middleware/read_only.py +6 -4
  76. swift/common/middleware/recon.py +59 -22
  77. swift/common/middleware/s3api/acl_handlers.py +25 -3
  78. swift/common/middleware/s3api/acl_utils.py +6 -1
  79. swift/common/middleware/s3api/controllers/__init__.py +6 -0
  80. swift/common/middleware/s3api/controllers/acl.py +3 -2
  81. swift/common/middleware/s3api/controllers/bucket.py +242 -137
  82. swift/common/middleware/s3api/controllers/logging.py +2 -2
  83. swift/common/middleware/s3api/controllers/multi_delete.py +43 -20
  84. swift/common/middleware/s3api/controllers/multi_upload.py +219 -133
  85. swift/common/middleware/s3api/controllers/obj.py +112 -8
  86. swift/common/middleware/s3api/controllers/object_lock.py +44 -0
  87. swift/common/middleware/s3api/controllers/s3_acl.py +2 -2
  88. swift/common/middleware/s3api/controllers/tagging.py +57 -0
  89. swift/common/middleware/s3api/controllers/versioning.py +36 -7
  90. swift/common/middleware/s3api/etree.py +22 -9
  91. swift/common/middleware/s3api/exception.py +0 -4
  92. swift/common/middleware/s3api/s3api.py +113 -41
  93. swift/common/middleware/s3api/s3request.py +384 -218
  94. swift/common/middleware/s3api/s3response.py +126 -23
  95. swift/common/middleware/s3api/s3token.py +16 -17
  96. swift/common/middleware/s3api/schema/delete.rng +1 -1
  97. swift/common/middleware/s3api/subresource.py +7 -10
  98. swift/common/middleware/s3api/utils.py +27 -10
  99. swift/common/middleware/slo.py +665 -358
  100. swift/common/middleware/staticweb.py +64 -37
  101. swift/common/middleware/symlink.py +51 -18
  102. swift/common/middleware/tempauth.py +76 -58
  103. swift/common/middleware/tempurl.py +191 -173
  104. swift/common/middleware/versioned_writes/__init__.py +51 -0
  105. swift/common/middleware/{versioned_writes.py → versioned_writes/legacy.py} +27 -26
  106. swift/common/middleware/versioned_writes/object_versioning.py +1482 -0
  107. swift/common/middleware/x_profile/exceptions.py +1 -4
  108. swift/common/middleware/x_profile/html_viewer.py +18 -19
  109. swift/common/middleware/x_profile/profile_model.py +1 -2
  110. swift/common/middleware/xprofile.py +10 -10
  111. swift-2.23.3.data/scripts/swift-container-server → swift/common/recon.py +13 -8
  112. swift/common/registry.py +147 -0
  113. swift/common/request_helpers.py +324 -57
  114. swift/common/ring/builder.py +67 -25
  115. swift/common/ring/composite_builder.py +1 -1
  116. swift/common/ring/ring.py +177 -51
  117. swift/common/ring/utils.py +1 -1
  118. swift/common/splice.py +10 -6
  119. swift/common/statsd_client.py +205 -0
  120. swift/common/storage_policy.py +49 -44
  121. swift/common/swob.py +86 -102
  122. swift/common/{utils.py → utils/__init__.py} +2163 -2772
  123. swift/common/utils/base.py +131 -0
  124. swift/common/utils/config.py +433 -0
  125. swift/common/utils/ipaddrs.py +256 -0
  126. swift/common/utils/libc.py +345 -0
  127. swift/common/utils/logs.py +859 -0
  128. swift/common/utils/timestamp.py +412 -0
  129. swift/common/wsgi.py +553 -535
  130. swift/container/auditor.py +14 -100
  131. swift/container/backend.py +490 -231
  132. swift/container/reconciler.py +126 -37
  133. swift/container/replicator.py +96 -22
  134. swift/container/server.py +358 -165
  135. swift/container/sharder.py +1540 -684
  136. swift/container/sync.py +94 -88
  137. swift/container/updater.py +53 -32
  138. swift/obj/auditor.py +153 -35
  139. swift/obj/diskfile.py +466 -217
  140. swift/obj/expirer.py +406 -124
  141. swift/obj/mem_diskfile.py +7 -4
  142. swift/obj/mem_server.py +1 -0
  143. swift/obj/reconstructor.py +523 -262
  144. swift/obj/replicator.py +249 -188
  145. swift/obj/server.py +207 -122
  146. swift/obj/ssync_receiver.py +145 -85
  147. swift/obj/ssync_sender.py +113 -54
  148. swift/obj/updater.py +652 -139
  149. swift/obj/watchers/__init__.py +0 -0
  150. swift/obj/watchers/dark_data.py +213 -0
  151. swift/proxy/controllers/account.py +11 -11
  152. swift/proxy/controllers/base.py +848 -604
  153. swift/proxy/controllers/container.py +433 -92
  154. swift/proxy/controllers/info.py +3 -2
  155. swift/proxy/controllers/obj.py +1000 -489
  156. swift/proxy/server.py +185 -112
  157. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/AUTHORS +58 -11
  158. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/METADATA +51 -56
  159. swift-2.35.0.dist-info/RECORD +201 -0
  160. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/WHEEL +1 -1
  161. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/entry_points.txt +43 -0
  162. swift-2.35.0.dist-info/pbr.json +1 -0
  163. swift/locale/de/LC_MESSAGES/swift.po +0 -1216
  164. swift/locale/en_GB/LC_MESSAGES/swift.po +0 -1207
  165. swift/locale/es/LC_MESSAGES/swift.po +0 -1085
  166. swift/locale/fr/LC_MESSAGES/swift.po +0 -909
  167. swift/locale/it/LC_MESSAGES/swift.po +0 -894
  168. swift/locale/ja/LC_MESSAGES/swift.po +0 -965
  169. swift/locale/ko_KR/LC_MESSAGES/swift.po +0 -964
  170. swift/locale/pt_BR/LC_MESSAGES/swift.po +0 -881
  171. swift/locale/ru/LC_MESSAGES/swift.po +0 -891
  172. swift/locale/tr_TR/LC_MESSAGES/swift.po +0 -832
  173. swift/locale/zh_CN/LC_MESSAGES/swift.po +0 -833
  174. swift/locale/zh_TW/LC_MESSAGES/swift.po +0 -838
  175. swift-2.23.3.data/scripts/swift-account-auditor +0 -23
  176. swift-2.23.3.data/scripts/swift-account-info +0 -51
  177. swift-2.23.3.data/scripts/swift-account-reaper +0 -23
  178. swift-2.23.3.data/scripts/swift-account-replicator +0 -34
  179. swift-2.23.3.data/scripts/swift-account-server +0 -23
  180. swift-2.23.3.data/scripts/swift-container-auditor +0 -23
  181. swift-2.23.3.data/scripts/swift-container-info +0 -55
  182. swift-2.23.3.data/scripts/swift-container-reconciler +0 -21
  183. swift-2.23.3.data/scripts/swift-container-replicator +0 -34
  184. swift-2.23.3.data/scripts/swift-container-sharder +0 -37
  185. swift-2.23.3.data/scripts/swift-container-sync +0 -23
  186. swift-2.23.3.data/scripts/swift-container-updater +0 -23
  187. swift-2.23.3.data/scripts/swift-dispersion-report +0 -24
  188. swift-2.23.3.data/scripts/swift-form-signature +0 -20
  189. swift-2.23.3.data/scripts/swift-init +0 -119
  190. swift-2.23.3.data/scripts/swift-object-auditor +0 -29
  191. swift-2.23.3.data/scripts/swift-object-expirer +0 -33
  192. swift-2.23.3.data/scripts/swift-object-info +0 -60
  193. swift-2.23.3.data/scripts/swift-object-reconstructor +0 -33
  194. swift-2.23.3.data/scripts/swift-object-relinker +0 -41
  195. swift-2.23.3.data/scripts/swift-object-replicator +0 -37
  196. swift-2.23.3.data/scripts/swift-object-server +0 -27
  197. swift-2.23.3.data/scripts/swift-object-updater +0 -23
  198. swift-2.23.3.data/scripts/swift-proxy-server +0 -23
  199. swift-2.23.3.data/scripts/swift-recon +0 -24
  200. swift-2.23.3.data/scripts/swift-ring-builder +0 -24
  201. swift-2.23.3.data/scripts/swift-ring-builder-analyzer +0 -22
  202. swift-2.23.3.data/scripts/swift-ring-composer +0 -22
  203. swift-2.23.3.dist-info/RECORD +0 -220
  204. swift-2.23.3.dist-info/pbr.json +0 -1
  205. {swift-2.23.3.dist-info → swift-2.35.0.dist-info}/LICENSE +0 -0
  206. {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)