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
@@ -60,32 +60,32 @@ Static Large Object when the multipart upload is completed.
60
60
  """
61
61
 
62
62
  import binascii
63
- from hashlib import md5
63
+ import copy
64
64
  import os
65
65
  import re
66
66
  import time
67
67
 
68
- import six
68
+ from swift.common import constraints
69
+ from swift.common.swob import Range, bytes_to_wsgi, normalize_etag, \
70
+ wsgi_to_str
71
+ from swift.common.utils import json, public, reiterate, md5, Timestamp
72
+ from swift.common.request_helpers import get_container_update_override_key, \
73
+ get_param
69
74
 
70
- from swift.common.swob import Range, bytes_to_wsgi
71
- from swift.common.utils import json, public, reiterate
72
- from swift.common.db import utf8encode
73
- from swift.common.request_helpers import get_container_update_override_key
74
-
75
- from six.moves.urllib.parse import quote, urlparse
75
+ from urllib.parse import quote, urlparse
76
76
 
77
77
  from swift.common.middleware.s3api.controllers.base import Controller, \
78
78
  bucket_operation, object_operation, check_container_existence
79
79
  from swift.common.middleware.s3api.s3response import InvalidArgument, \
80
- ErrorResponse, MalformedXML, BadDigest, \
80
+ ErrorResponse, MalformedXML, BadDigest, KeyTooLongError, \
81
81
  InvalidPart, BucketAlreadyExists, EntityTooSmall, InvalidPartOrder, \
82
82
  InvalidRequest, HTTPOk, HTTPNoContent, NoSuchKey, NoSuchUpload, \
83
- NoSuchBucket, BucketAlreadyOwnedByYou
84
- from swift.common.middleware.s3api.exception import BadSwiftRequest
83
+ NoSuchBucket, BucketAlreadyOwnedByYou, ServiceUnavailable
85
84
  from swift.common.middleware.s3api.utils import unique_id, \
86
85
  MULTIUPLOAD_SUFFIX, S3Timestamp, sysmeta_header
87
86
  from swift.common.middleware.s3api.etree import Element, SubElement, \
88
87
  fromstring, tostring, XMLSyntaxError, DocumentInvalid
88
+ from swift.common.storage_policy import POLICIES
89
89
 
90
90
  DEFAULT_MAX_PARTS_LISTING = 1000
91
91
  DEFAULT_MAX_UPLOADS = 1000
@@ -94,19 +94,82 @@ MAX_COMPLETE_UPLOAD_BODY_SIZE = 2048 * 1024
94
94
 
95
95
 
96
96
  def _get_upload_info(req, app, upload_id):
97
+ """
98
+ Make a HEAD request for existing upload object metadata. Tries the upload
99
+ marker first, and then falls back to the manifest object.
100
+
101
+ :param req: an S3Request object.
102
+ :param app: the wsgi app.
103
+ :param upload_id: the upload id.
104
+ :returns: a tuple of (S3Response, boolean) where the boolean is True if the
105
+ response is from the upload marker and False otherwise.
106
+ :raises: NoSuchUpload if neither the marker nor the manifest were found.
107
+ """
97
108
 
98
109
  container = req.container_name + MULTIUPLOAD_SUFFIX
99
110
  obj = '%s/%s' % (req.object_name, upload_id)
100
111
 
112
+ # XXX: if we leave the copy-source header, somewhere later we might
113
+ # drop in a ?version-id=... query string that's utterly inappropriate
114
+ # for the upload marker. Until we get around to fixing that, just pop
115
+ # it off for now...
116
+ copy_source = req.headers.pop('X-Amz-Copy-Source', None)
101
117
  try:
102
- return req.get_response(app, 'HEAD', container=container, obj=obj)
118
+ resp = req.get_response(app, 'HEAD', container=container, obj=obj)
119
+ return resp, True
103
120
  except NoSuchKey:
121
+ # ensure consistent path and policy are logged despite manifest HEAD
122
+ upload_marker_path = req.environ.get('s3api.backend_path')
123
+ policy_index = req.policy_index
124
+ try:
125
+ resp = req.get_response(app, 'HEAD')
126
+ if resp.sysmeta_headers.get(sysmeta_header(
127
+ 'object', 'upload-id')) == upload_id:
128
+ return resp, False
129
+ except NoSuchKey:
130
+ pass
131
+ finally:
132
+ # Ops often find it more useful for us to log the upload marker
133
+ # path, so put it back
134
+ if upload_marker_path is not None:
135
+ req.environ['s3api.backend_path'] = upload_marker_path
136
+ if policy_index is not None:
137
+ req.policy_index = policy_index
104
138
  raise NoSuchUpload(upload_id=upload_id)
105
-
106
-
107
- def _check_upload_info(req, app, upload_id):
108
-
109
- _get_upload_info(req, app, upload_id)
139
+ finally:
140
+ # ...making sure to restore any copy-source before returning
141
+ if copy_source is not None:
142
+ req.headers['X-Amz-Copy-Source'] = copy_source
143
+
144
+
145
+ def _make_complete_body(req, s3_etag, yielded_anything):
146
+ result_elem = Element('CompleteMultipartUploadResult')
147
+
148
+ # NOTE: boto with sig v4 appends port to HTTP_HOST value at
149
+ # the request header when the port is non default value and it
150
+ # makes req.host_url like as http://localhost:8080:8080/path
151
+ # that obviously invalid. Probably it should be resolved at
152
+ # swift.common.swob though, tentatively we are parsing and
153
+ # reconstructing the correct host_url info here.
154
+ # in detail, https://github.com/boto/boto/pull/3513
155
+ parsed_url = urlparse(req.host_url)
156
+ host_url = '%s://%s' % (parsed_url.scheme, parsed_url.hostname)
157
+ # Why are we doing our own port parsing? Because py3 decided
158
+ # to start raising ValueErrors on access after parsing such
159
+ # an invalid port
160
+ netloc = parsed_url.netloc.split('@')[-1].split(']')[-1]
161
+ if ':' in netloc:
162
+ port = netloc.split(':', 2)[1]
163
+ host_url += ':%s' % port
164
+
165
+ SubElement(result_elem, 'Location').text = host_url + req.path
166
+ SubElement(result_elem, 'Bucket').text = req.container_name
167
+ SubElement(result_elem, 'Key').text = wsgi_to_str(req.object_name)
168
+ SubElement(result_elem, 'ETag').text = '"%s"' % s3_etag
169
+ body = tostring(result_elem, xml_declaration=not yielded_anything)
170
+ if yielded_anything:
171
+ return b'\n' + body
172
+ return body
110
173
 
111
174
 
112
175
  class PartController(Controller):
@@ -130,18 +193,10 @@ class PartController(Controller):
130
193
  raise InvalidArgument('ResourceType', 'partNumber',
131
194
  'Unexpected query string parameter')
132
195
 
133
- try:
134
- part_number = int(req.params['partNumber'])
135
- if part_number < 1 or self.conf.max_upload_part_num < part_number:
136
- raise Exception()
137
- except Exception:
138
- err_msg = 'Part number must be an integer between 1 and %d,' \
139
- ' inclusive' % self.conf.max_upload_part_num
140
- raise InvalidArgument('partNumber', req.params['partNumber'],
141
- err_msg)
142
-
143
- upload_id = req.params['uploadId']
144
- _check_upload_info(req, self.app, upload_id)
196
+ part_number = req.validate_part_number()
197
+
198
+ upload_id = get_param(req, 'uploadId')
199
+ _get_upload_info(req, self.app, upload_id)
145
200
 
146
201
  req.container_name += MULTIUPLOAD_SUFFIX
147
202
  req.object_name = '%s/%s/%d' % (req.object_name, upload_id,
@@ -235,8 +290,6 @@ class UploadsController(Controller):
235
290
 
236
291
  :return (non_delimited_uploads, common_prefixes)
237
292
  """
238
- if six.PY2:
239
- (prefix, delimiter) = utf8encode(prefix, delimiter)
240
293
  non_delimited_uploads = []
241
294
  common_prefixes = set()
242
295
  for upload in uploads:
@@ -249,19 +302,19 @@ class UploadsController(Controller):
249
302
  non_delimited_uploads.append(upload)
250
303
  return non_delimited_uploads, sorted(common_prefixes)
251
304
 
252
- encoding_type = req.params.get('encoding-type')
305
+ encoding_type = get_param(req, 'encoding-type')
253
306
  if encoding_type is not None and encoding_type != 'url':
254
307
  err_msg = 'Invalid Encoding Method specified in Request'
255
308
  raise InvalidArgument('encoding-type', encoding_type, err_msg)
256
309
 
257
- keymarker = req.params.get('key-marker', '')
258
- uploadid = req.params.get('upload-id-marker', '')
310
+ keymarker = get_param(req, 'key-marker', '')
311
+ uploadid = get_param(req, 'upload-id-marker', '')
259
312
  maxuploads = req.get_validated_param(
260
313
  'max-uploads', DEFAULT_MAX_UPLOADS, DEFAULT_MAX_UPLOADS)
261
314
 
262
315
  query = {
263
316
  'format': 'json',
264
- 'limit': maxuploads + 1,
317
+ 'marker': '',
265
318
  }
266
319
 
267
320
  if uploadid and keymarker:
@@ -269,15 +322,11 @@ class UploadsController(Controller):
269
322
  elif keymarker:
270
323
  query.update({'marker': '%s/~' % (keymarker)})
271
324
  if 'prefix' in req.params:
272
- query.update({'prefix': req.params['prefix']})
325
+ query.update({'prefix': get_param(req, 'prefix')})
273
326
 
274
327
  container = req.container_name + MULTIUPLOAD_SUFFIX
275
- try:
276
- resp = req.get_response(self.app, container=container, query=query)
277
- objects = json.loads(resp.body)
278
- except NoSuchBucket:
279
- # Assume NoSuchBucket as no uploads
280
- objects = []
328
+ uploads = []
329
+ prefixes = []
281
330
 
282
331
  def object_to_upload(object_info):
283
332
  obj, upid = object_info['name'].rsplit('/', 1)
@@ -286,24 +335,34 @@ class UploadsController(Controller):
286
335
  'last_modified': object_info['last_modified']}
287
336
  return obj_dict
288
337
 
289
- # uploads is a list consists of dict, {key, upload_id, last_modified}
290
- # Note that pattern matcher will drop whole segments objects like as
291
- # object_name/upload_id/1.
292
- pattern = re.compile('/[0-9]+$')
293
- uploads = [object_to_upload(obj) for obj in objects if
294
- pattern.search(obj.get('name', '')) is None]
295
-
296
- prefixes = []
297
- if 'delimiter' in req.params:
298
- prefix = req.params.get('prefix', '')
299
- delimiter = req.params['delimiter']
300
- uploads, prefixes = separate_uploads(uploads, prefix, delimiter)
338
+ is_segment = re.compile('.*/[0-9]+$')
301
339
 
340
+ while len(uploads) < maxuploads:
341
+ try:
342
+ resp = req.get_response(self.app, container=container,
343
+ query=query)
344
+ objects = json.loads(resp.body)
345
+ except NoSuchBucket:
346
+ # Assume NoSuchBucket as no uploads
347
+ objects = []
348
+ if not objects:
349
+ break
350
+
351
+ new_uploads = [object_to_upload(obj) for obj in objects
352
+ if not is_segment.match(obj.get('name', ''))]
353
+ new_prefixes = []
354
+ if 'delimiter' in req.params:
355
+ prefix = get_param(req, 'prefix', '')
356
+ delimiter = get_param(req, 'delimiter')
357
+ new_uploads, new_prefixes = separate_uploads(
358
+ new_uploads, prefix, delimiter)
359
+ uploads.extend(new_uploads)
360
+ prefixes.extend(new_prefixes)
361
+ query['marker'] = objects[-1]['name']
362
+
363
+ truncated = len(uploads) >= maxuploads
302
364
  if len(uploads) > maxuploads:
303
365
  uploads = uploads[:maxuploads]
304
- truncated = True
305
- else:
306
- truncated = False
307
366
 
308
367
  nextkeymarker = ''
309
368
  nextuploadmarker = ''
@@ -318,9 +377,10 @@ class UploadsController(Controller):
318
377
  SubElement(result_elem, 'NextKeyMarker').text = nextkeymarker
319
378
  SubElement(result_elem, 'NextUploadIdMarker').text = nextuploadmarker
320
379
  if 'delimiter' in req.params:
321
- SubElement(result_elem, 'Delimiter').text = req.params['delimiter']
380
+ SubElement(result_elem, 'Delimiter').text = \
381
+ get_param(req, 'delimiter')
322
382
  if 'prefix' in req.params:
323
- SubElement(result_elem, 'Prefix').text = req.params['prefix']
383
+ SubElement(result_elem, 'Prefix').text = get_param(req, 'prefix')
324
384
  SubElement(result_elem, 'MaxUploads').text = str(maxuploads)
325
385
  if encoding_type is not None:
326
386
  SubElement(result_elem, 'EncodingType').text = encoding_type
@@ -344,7 +404,7 @@ class UploadsController(Controller):
344
404
  SubElement(owner_elem, 'DisplayName').text = req.user_id
345
405
  SubElement(upload_elem, 'StorageClass').text = 'STANDARD'
346
406
  SubElement(upload_elem, 'Initiated').text = \
347
- u['last_modified'][:-3] + 'Z'
407
+ S3Timestamp.from_isoformat(u['last_modified']).s3xmlformat
348
408
 
349
409
  for p in prefixes:
350
410
  elem = SubElement(result_elem, 'CommonPrefixes')
@@ -361,11 +421,15 @@ class UploadsController(Controller):
361
421
  """
362
422
  Handles Initiate Multipart Upload.
363
423
  """
424
+ if len(req.object_name) > constraints.MAX_OBJECT_NAME_LENGTH:
425
+ # Note that we can still run into trouble where the MPU is just
426
+ # within the limit, which means the segment names will go over
427
+ raise KeyTooLongError()
428
+
364
429
  # Create a unique S3 upload id from UUID to avoid duplicates.
365
430
  upload_id = unique_id()
366
431
 
367
- orig_container = req.container_name
368
- seg_container = orig_container + MULTIUPLOAD_SUFFIX
432
+ seg_container = req.container_name + MULTIUPLOAD_SUFFIX
369
433
  content_type = req.headers.get('Content-Type')
370
434
  if content_type:
371
435
  req.headers[sysmeta_header('object', 'has-content-type')] = 'yes'
@@ -376,15 +440,25 @@ class UploadsController(Controller):
376
440
  req.headers['Content-Type'] = 'application/directory'
377
441
 
378
442
  try:
379
- req.container_name = seg_container
380
- req.get_container_info(self.app)
443
+ seg_req = copy.copy(req)
444
+ seg_req.environ = copy.copy(req.environ)
445
+ seg_req.container_name = seg_container
446
+ seg_req.get_container_info(self.app)
381
447
  except NoSuchBucket:
382
448
  try:
383
- req.get_response(self.app, 'PUT', seg_container, '')
449
+ # multi-upload bucket doesn't exist, create one with
450
+ # same storage policy and acls as the primary bucket
451
+ info = req.get_container_info(self.app)
452
+ policy_name = POLICIES[info['storage_policy']].name
453
+ hdrs = {'X-Storage-Policy': policy_name}
454
+ if info.get('read_acl'):
455
+ hdrs['X-Container-Read'] = info['read_acl']
456
+ if info.get('write_acl'):
457
+ hdrs['X-Container-Write'] = info['write_acl']
458
+ seg_req.get_response(self.app, 'PUT', seg_container, '',
459
+ headers=hdrs)
384
460
  except (BucketAlreadyExists, BucketAlreadyOwnedByYou):
385
461
  pass
386
- finally:
387
- req.container_name = orig_container
388
462
 
389
463
  obj = '%s/%s' % (req.object_name, upload_id)
390
464
 
@@ -395,7 +469,7 @@ class UploadsController(Controller):
395
469
 
396
470
  result_elem = Element('InitiateMultipartUploadResult')
397
471
  SubElement(result_elem, 'Bucket').text = req.container_name
398
- SubElement(result_elem, 'Key').text = req.object_name
472
+ SubElement(result_elem, 'Key').text = wsgi_to_str(req.object_name)
399
473
  SubElement(result_elem, 'UploadId').text = upload_id
400
474
 
401
475
  body = tostring(result_elem)
@@ -427,13 +501,13 @@ class UploadController(Controller):
427
501
  except ValueError:
428
502
  return False
429
503
 
430
- encoding_type = req.params.get('encoding-type')
504
+ encoding_type = get_param(req, 'encoding-type')
431
505
  if encoding_type is not None and encoding_type != 'url':
432
506
  err_msg = 'Invalid Encoding Method specified in Request'
433
507
  raise InvalidArgument('encoding-type', encoding_type, err_msg)
434
508
 
435
- upload_id = req.params['uploadId']
436
- _check_upload_info(req, self.app, upload_id)
509
+ upload_id = get_param(req, 'uploadId')
510
+ _get_upload_info(req, self.app, upload_id)
437
511
 
438
512
  maxparts = req.get_validated_param(
439
513
  'max-parts', DEFAULT_MAX_PARTS_LISTING,
@@ -441,17 +515,26 @@ class UploadController(Controller):
441
515
  part_num_marker = req.get_validated_param(
442
516
  'part-number-marker', 0)
443
517
 
518
+ object_name = wsgi_to_str(req.object_name)
444
519
  query = {
445
520
  'format': 'json',
446
- 'limit': maxparts + 1,
447
- 'prefix': '%s/%s/' % (req.object_name, upload_id),
448
- 'delimiter': '/'
521
+ 'prefix': '%s/%s/' % (object_name, upload_id),
522
+ 'delimiter': '/',
523
+ 'marker': '',
449
524
  }
450
525
 
451
526
  container = req.container_name + MULTIUPLOAD_SUFFIX
452
- resp = req.get_response(self.app, container=container, obj='',
453
- query=query)
454
- objects = json.loads(resp.body)
527
+ # Because the parts are out of order in Swift, we list up to the
528
+ # maximum number of parts and then apply the marker and limit options.
529
+ objects = []
530
+ while True:
531
+ resp = req.get_response(self.app, container=container, obj='',
532
+ query=query)
533
+ new_objects = json.loads(resp.body)
534
+ if not new_objects:
535
+ break
536
+ objects.extend(new_objects)
537
+ query['marker'] = new_objects[-1]['name']
455
538
 
456
539
  last_part = 0
457
540
 
@@ -477,10 +560,9 @@ class UploadController(Controller):
477
560
 
478
561
  result_elem = Element('ListPartsResult')
479
562
  SubElement(result_elem, 'Bucket').text = req.container_name
480
- name = req.object_name
481
563
  if encoding_type == 'url':
482
- name = quote(name)
483
- SubElement(result_elem, 'Key').text = name
564
+ object_name = quote(object_name)
565
+ SubElement(result_elem, 'Key').text = object_name
484
566
  SubElement(result_elem, 'UploadId').text = upload_id
485
567
 
486
568
  initiator_elem = SubElement(result_elem, 'Initiator')
@@ -496,7 +578,7 @@ class UploadController(Controller):
496
578
  SubElement(result_elem, 'MaxParts').text = str(maxparts)
497
579
  if 'encoding-type' in req.params:
498
580
  SubElement(result_elem, 'EncodingType').text = \
499
- req.params['encoding-type']
581
+ get_param(req, 'encoding-type')
500
582
  SubElement(result_elem, 'IsTruncated').text = \
501
583
  'true' if truncated else 'false'
502
584
 
@@ -504,7 +586,7 @@ class UploadController(Controller):
504
586
  part_elem = SubElement(result_elem, 'Part')
505
587
  SubElement(part_elem, 'PartNumber').text = i['name'].split('/')[-1]
506
588
  SubElement(part_elem, 'LastModified').text = \
507
- i['last_modified'][:-3] + 'Z'
589
+ S3Timestamp.from_isoformat(i['last_modified']).s3xmlformat
508
590
  SubElement(part_elem, 'ETag').text = '"%s"' % i['hash']
509
591
  SubElement(part_elem, 'Size').text = str(i['bytes'])
510
592
 
@@ -519,8 +601,8 @@ class UploadController(Controller):
519
601
  """
520
602
  Handles Abort Multipart Upload.
521
603
  """
522
- upload_id = req.params['uploadId']
523
- _check_upload_info(req, self.app, upload_id)
604
+ upload_id = get_param(req, 'uploadId')
605
+ _get_upload_info(req, self.app, upload_id)
524
606
 
525
607
  # First check to see if this multi-part upload was already
526
608
  # completed. Look in the primary container, if the object exists,
@@ -533,9 +615,10 @@ class UploadController(Controller):
533
615
  # must be a multipart upload abort.
534
616
  # We must delete any uploaded segments for this UploadID and then
535
617
  # delete the object in the main container as well
618
+ object_name = wsgi_to_str(req.object_name)
536
619
  query = {
537
620
  'format': 'json',
538
- 'prefix': '%s/%s/' % (req.object_name, upload_id),
621
+ 'prefix': '%s/%s/' % (object_name, upload_id),
539
622
  'delimiter': '/',
540
623
  }
541
624
 
@@ -543,10 +626,15 @@ class UploadController(Controller):
543
626
 
544
627
  # Iterate over the segment objects and delete them individually
545
628
  objects = json.loads(resp.body)
546
- for o in objects:
547
- container = req.container_name + MULTIUPLOAD_SUFFIX
548
- obj = bytes_to_wsgi(o['name'].encode('utf-8'))
549
- req.get_response(self.app, container=container, obj=obj)
629
+ while objects:
630
+ for o in objects:
631
+ container = req.container_name + MULTIUPLOAD_SUFFIX
632
+ obj = bytes_to_wsgi(o['name'].encode('utf-8'))
633
+ req.get_response(self.app, container=container, obj=obj)
634
+ query['marker'] = objects[-1]['name']
635
+ resp = req.get_response(self.app, 'GET', container, '',
636
+ query=query)
637
+ objects = json.loads(resp.body)
550
638
 
551
639
  return HTTPNoContent()
552
640
 
@@ -557,13 +645,24 @@ class UploadController(Controller):
557
645
  """
558
646
  Handles Complete Multipart Upload.
559
647
  """
560
- upload_id = req.params['uploadId']
561
- resp = _get_upload_info(req, self.app, upload_id)
562
- headers = {'Accept': 'application/json'}
648
+ upload_id = get_param(req, 'uploadId')
649
+ resp, is_marker = _get_upload_info(req, self.app, upload_id)
650
+ if (is_marker and
651
+ resp.sw_headers.get('X-Backend-Timestamp') >= Timestamp.now()):
652
+ # Somehow the marker was created in the future w.r.t. this thread's
653
+ # clock. The manifest PUT may succeed but the subsequent marker
654
+ # DELETE will fail, so don't attempt either.
655
+ raise ServiceUnavailable
656
+
657
+ headers = {'Accept': 'application/json',
658
+ sysmeta_header('object', 'upload-id'): upload_id}
563
659
  for key, val in resp.headers.items():
564
660
  _key = key.lower()
565
661
  if _key.startswith('x-amz-meta-'):
566
662
  headers['x-object-meta-' + _key[11:]] = val
663
+ elif _key in ('content-encoding', 'content-language',
664
+ 'content-disposition', 'expires', 'cache-control'):
665
+ headers[key] = val
567
666
 
568
667
  hct_header = sysmeta_header('object', 'has-content-type')
569
668
  if resp.sysmeta_headers.get(hct_header) == 'yes':
@@ -582,7 +681,7 @@ class UploadController(Controller):
582
681
  headers['Content-Type'] = content_type
583
682
 
584
683
  container = req.container_name + MULTIUPLOAD_SUFFIX
585
- s3_etag_hasher = md5()
684
+ s3_etag_hasher = md5(usedforsecurity=False)
586
685
  manifest = []
587
686
  previous_number = 0
588
687
  try:
@@ -592,7 +691,8 @@ class UploadController(Controller):
592
691
  if 'content-md5' in req.headers:
593
692
  # If an MD5 was provided, we need to verify it.
594
693
  # Note that S3Request already took care of translating to ETag
595
- if req.headers['etag'] != md5(xml).hexdigest():
694
+ if req.headers['etag'] != md5(
695
+ xml, usedforsecurity=False).hexdigest():
596
696
  raise BadDigest(content_md5=req.headers['content-md5'])
597
697
  # We're only interested in the body here, in the
598
698
  # multipart-upload controller -- *don't* let it get
@@ -608,18 +708,20 @@ class UploadController(Controller):
608
708
  raise InvalidPartOrder(upload_id=upload_id)
609
709
  previous_number = part_number
610
710
 
611
- etag = part_elem.find('./ETag').text
612
- if len(etag) >= 2 and etag[0] == '"' and etag[-1] == '"':
613
- # strip double quotes
614
- etag = etag[1:-1]
711
+ etag = normalize_etag(part_elem.find('./ETag').text)
712
+ if etag is None:
713
+ raise InvalidPart(upload_id=upload_id,
714
+ part_number=part_number,
715
+ e_tag=etag)
615
716
  if len(etag) != 32 or any(c not in '0123456789abcdef'
616
717
  for c in etag):
617
718
  raise InvalidPart(upload_id=upload_id,
618
- part_number=part_number)
619
-
719
+ part_number=part_number,
720
+ e_tag=etag)
620
721
  manifest.append({
621
722
  'path': '/%s/%s/%s/%d' % (
622
- container, req.object_name, upload_id, part_number),
723
+ wsgi_to_str(container), wsgi_to_str(req.object_name),
724
+ upload_id, part_number),
623
725
  'etag': etag})
624
726
  s3_etag_hasher.update(binascii.a2b_hex(etag))
625
727
  except (XMLSyntaxError, DocumentInvalid):
@@ -632,7 +734,15 @@ class UploadController(Controller):
632
734
  raise
633
735
 
634
736
  s3_etag = '%s-%d' % (s3_etag_hasher.hexdigest(), len(manifest))
635
- headers[sysmeta_header('object', 'etag')] = s3_etag
737
+ s3_etag_header = sysmeta_header('object', 'etag')
738
+ if resp.sysmeta_headers.get(s3_etag_header) == s3_etag:
739
+ # This header should only already be present if the upload marker
740
+ # has been cleaned up and the current target uses the same
741
+ # upload-id; assuming the segments to use haven't changed, the work
742
+ # is already done
743
+ return HTTPOk(body=_make_complete_body(req, s3_etag, False),
744
+ content_type='application/xml')
745
+ headers[s3_etag_header] = s3_etag
636
746
  # Leave base header value blank; SLO will populate
637
747
  c_etag = '; s3_etag=%s' % s3_etag
638
748
  headers[get_container_update_override_key('etag')] = c_etag
@@ -696,8 +806,8 @@ class UploadController(Controller):
696
806
  status=body['Response Status'],
697
807
  msg='\n'.join(': '.join(err)
698
808
  for err in body['Errors']))
699
- except BadSwiftRequest as e:
700
- msg = str(e)
809
+ except InvalidRequest as err_resp:
810
+ msg = err_resp._msg
701
811
  if too_small_message in msg:
702
812
  raise EntityTooSmall(msg)
703
813
  elif ', Etag Mismatch' in msg:
@@ -712,37 +822,13 @@ class UploadController(Controller):
712
822
  try:
713
823
  req.get_response(self.app, 'DELETE', container, obj)
714
824
  except NoSuchKey:
715
- # We know that this existed long enough for us to HEAD
825
+ # The important thing is that we wrote out a tombstone to
826
+ # make sure the marker got cleaned up. If it's already
827
+ # gone (e.g., because of concurrent completes or a retried
828
+ # complete), so much the better.
716
829
  pass
717
830
 
718
- result_elem = Element('CompleteMultipartUploadResult')
719
-
720
- # NOTE: boto with sig v4 appends port to HTTP_HOST value at
721
- # the request header when the port is non default value and it
722
- # makes req.host_url like as http://localhost:8080:8080/path
723
- # that obviously invalid. Probably it should be resolved at
724
- # swift.common.swob though, tentatively we are parsing and
725
- # reconstructing the correct host_url info here.
726
- # in detail, https://github.com/boto/boto/pull/3513
727
- parsed_url = urlparse(req.host_url)
728
- host_url = '%s://%s' % (parsed_url.scheme, parsed_url.hostname)
729
- # Why are we doing our own port parsing? Because py3 decided
730
- # to start raising ValueErrors on access after parsing such
731
- # an invalid port
732
- netloc = parsed_url.netloc.split('@')[-1].split(']')[-1]
733
- if ':' in netloc:
734
- port = netloc.split(':', 2)[1]
735
- host_url += ':%s' % port
736
-
737
- SubElement(result_elem, 'Location').text = host_url + req.path
738
- SubElement(result_elem, 'Bucket').text = req.container_name
739
- SubElement(result_elem, 'Key').text = req.object_name
740
- SubElement(result_elem, 'ETag').text = '"%s"' % s3_etag
741
- resp.headers.pop('ETag', None)
742
- if yielded_anything:
743
- yield b'\n'
744
- yield tostring(result_elem,
745
- xml_declaration=not yielded_anything)
831
+ yield _make_complete_body(req, s3_etag, yielded_anything)
746
832
  except ErrorResponse as err_resp:
747
833
  if yielded_anything:
748
834
  err_resp.xml_declaration = False