swift 2.23.2__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 (208) 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.2.data/scripts/swift-account-audit → swift/cli/account_audit.py +23 -13
  9. swift-2.23.2.data/scripts/swift-config → swift/cli/config.py +2 -2
  10. swift/cli/container_deleter.py +5 -11
  11. swift-2.23.2.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +8 -7
  12. swift/cli/dispersion_report.py +10 -9
  13. swift-2.23.2.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +63 -21
  14. swift/cli/form_signature.py +3 -7
  15. swift-2.23.2.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +8 -2
  16. swift/cli/info.py +183 -29
  17. swift/cli/manage_shard_ranges.py +708 -37
  18. swift-2.23.2.data/scripts/swift-oldies → swift/cli/oldies.py +25 -14
  19. swift-2.23.2.data/scripts/swift-orphans → swift/cli/orphans.py +7 -3
  20. swift/cli/recon.py +196 -67
  21. swift-2.23.2.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +17 -20
  22. swift-2.23.2.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 +198 -127
  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 +396 -147
  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 -81
  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 +52 -19
  102. swift/common/middleware/tempauth.py +76 -58
  103. swift/common/middleware/tempurl.py +192 -174
  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.2.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} +2191 -2762
  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 +555 -536
  130. swift/container/auditor.py +14 -100
  131. swift/container/backend.py +552 -227
  132. swift/container/reconciler.py +126 -37
  133. swift/container/replicator.py +96 -22
  134. swift/container/server.py +397 -176
  135. swift/container/sharder.py +1580 -639
  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 +213 -122
  146. swift/obj/ssync_receiver.py +145 -85
  147. swift/obj/ssync_sender.py +113 -54
  148. swift/obj/updater.py +653 -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 +452 -86
  154. swift/proxy/controllers/info.py +3 -2
  155. swift/proxy/controllers/obj.py +1009 -490
  156. swift/proxy/server.py +185 -112
  157. swift-2.35.0.dist-info/AUTHORS +501 -0
  158. swift-2.35.0.dist-info/LICENSE +202 -0
  159. {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/METADATA +52 -61
  160. swift-2.35.0.dist-info/RECORD +201 -0
  161. {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/WHEEL +1 -1
  162. {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/entry_points.txt +43 -0
  163. swift-2.35.0.dist-info/pbr.json +1 -0
  164. swift/locale/de/LC_MESSAGES/swift.po +0 -1216
  165. swift/locale/en_GB/LC_MESSAGES/swift.po +0 -1207
  166. swift/locale/es/LC_MESSAGES/swift.po +0 -1085
  167. swift/locale/fr/LC_MESSAGES/swift.po +0 -909
  168. swift/locale/it/LC_MESSAGES/swift.po +0 -894
  169. swift/locale/ja/LC_MESSAGES/swift.po +0 -965
  170. swift/locale/ko_KR/LC_MESSAGES/swift.po +0 -964
  171. swift/locale/pt_BR/LC_MESSAGES/swift.po +0 -881
  172. swift/locale/ru/LC_MESSAGES/swift.po +0 -891
  173. swift/locale/tr_TR/LC_MESSAGES/swift.po +0 -832
  174. swift/locale/zh_CN/LC_MESSAGES/swift.po +0 -833
  175. swift/locale/zh_TW/LC_MESSAGES/swift.po +0 -838
  176. swift-2.23.2.data/scripts/swift-account-auditor +0 -23
  177. swift-2.23.2.data/scripts/swift-account-info +0 -51
  178. swift-2.23.2.data/scripts/swift-account-reaper +0 -23
  179. swift-2.23.2.data/scripts/swift-account-replicator +0 -34
  180. swift-2.23.2.data/scripts/swift-account-server +0 -23
  181. swift-2.23.2.data/scripts/swift-container-auditor +0 -23
  182. swift-2.23.2.data/scripts/swift-container-info +0 -51
  183. swift-2.23.2.data/scripts/swift-container-reconciler +0 -21
  184. swift-2.23.2.data/scripts/swift-container-replicator +0 -34
  185. swift-2.23.2.data/scripts/swift-container-sharder +0 -33
  186. swift-2.23.2.data/scripts/swift-container-sync +0 -23
  187. swift-2.23.2.data/scripts/swift-container-updater +0 -23
  188. swift-2.23.2.data/scripts/swift-dispersion-report +0 -24
  189. swift-2.23.2.data/scripts/swift-form-signature +0 -20
  190. swift-2.23.2.data/scripts/swift-init +0 -119
  191. swift-2.23.2.data/scripts/swift-object-auditor +0 -29
  192. swift-2.23.2.data/scripts/swift-object-expirer +0 -33
  193. swift-2.23.2.data/scripts/swift-object-info +0 -60
  194. swift-2.23.2.data/scripts/swift-object-reconstructor +0 -33
  195. swift-2.23.2.data/scripts/swift-object-relinker +0 -41
  196. swift-2.23.2.data/scripts/swift-object-replicator +0 -37
  197. swift-2.23.2.data/scripts/swift-object-server +0 -27
  198. swift-2.23.2.data/scripts/swift-object-updater +0 -23
  199. swift-2.23.2.data/scripts/swift-proxy-server +0 -23
  200. swift-2.23.2.data/scripts/swift-recon +0 -24
  201. swift-2.23.2.data/scripts/swift-ring-builder +0 -24
  202. swift-2.23.2.data/scripts/swift-ring-builder-analyzer +0 -22
  203. swift-2.23.2.data/scripts/swift-ring-composer +0 -22
  204. swift-2.23.2.dist-info/DESCRIPTION.rst +0 -166
  205. swift-2.23.2.dist-info/RECORD +0 -220
  206. swift-2.23.2.dist-info/metadata.json +0 -1
  207. swift-2.23.2.dist-info/pbr.json +0 -1
  208. {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/top_level.txt +0 -0
@@ -270,11 +270,27 @@ A GET request with the query parameters::
270
270
  will return the contents of the original manifest as it was sent by the client.
271
271
  The main purpose for both calls is solely debugging.
272
272
 
273
- When the manifest object is uploaded you are more or less guaranteed that
274
- every segment in the manifest exists and matched the specifications.
275
- However, there is nothing that prevents the user from breaking the
276
- SLO download by deleting/replacing a segment referenced in the manifest. It is
277
- left to the user to use caution in handling the segments.
273
+ A GET request to a manifest object with the query parameter::
274
+
275
+ ?part-number=<n>
276
+
277
+ will return the contents of the ``nth`` segment. Segments are indexed from 1,
278
+ so ``n`` must be an integer between 1 and the total number of segments in the
279
+ manifest. The response status will be ``206 Partial Content`` and its headers
280
+ will include: an ``X-Parts-Count`` header equal to the total number of
281
+ segments; a ``Content-Length`` header equal to the length of the specified
282
+ segment; a ``Content-Range`` header describing the byte range of the specified
283
+ part within the SLO. A HEAD request with a ``part-number`` parameter will also
284
+ return a response with status ``206 Partial Content`` and the same headers.
285
+
286
+ .. note::
287
+
288
+ When the manifest object is uploaded you are more or less guaranteed that
289
+ every segment in the manifest exists and matched the specifications.
290
+ However, there is nothing that prevents the user from breaking the SLO
291
+ download by deleting/replacing a segment referenced in the manifest. It is
292
+ left to the user to use caution in handling the segments.
293
+
278
294
 
279
295
  -----------------------
280
296
  Deleting a Large Object
@@ -290,6 +306,16 @@ A ``DELETE`` with a query parameter::
290
306
  will delete all the segments referenced in the manifest and then the manifest
291
307
  itself. The failure response will be similar to the bulk delete middleware.
292
308
 
309
+ A ``DELETE`` with the query parameters::
310
+
311
+ ?multipart-manifest=delete&async=yes
312
+
313
+ will schedule all the segments referenced in the manifest to be deleted
314
+ asynchronously and then delete the manifest itself. Note that segments will
315
+ continue to appear in listings and be counted for quotas until they are
316
+ cleaned up by the object-expirer. This option is only available when all
317
+ segments are in the same container and none of them are nested SLOs.
318
+
293
319
  ------------------------
294
320
  Modifying a Large Object
295
321
  ------------------------
@@ -313,17 +339,15 @@ metadata which can be used for stats and billing purposes.
313
339
  """
314
340
 
315
341
  import base64
316
- from cgi import parse_header
317
342
  from collections import defaultdict
318
343
  from datetime import datetime
319
344
  import json
320
345
  import mimetypes
321
346
  import re
322
347
  import time
323
- from hashlib import md5
324
-
325
- import six
326
348
 
349
+ from swift.cli.container_deleter import make_delete_jobs
350
+ from swift.common.header_key_dict import HeaderKeyDict
327
351
  from swift.common.exceptions import ListingIterError, SegmentError
328
352
  from swift.common.middleware.listing_formats import \
329
353
  MAX_CONTAINER_LISTING_CONTENT_LENGTH
@@ -331,21 +355,26 @@ from swift.common.swob import Request, HTTPBadRequest, HTTPServerError, \
331
355
  HTTPMethodNotAllowed, HTTPRequestEntityTooLarge, HTTPLengthRequired, \
332
356
  HTTPOk, HTTPPreconditionFailed, HTTPException, HTTPNotFound, \
333
357
  HTTPUnauthorized, HTTPConflict, HTTPUnprocessableEntity, \
334
- HTTPServiceUnavailable, Response, Range, \
335
- RESPONSE_REASONS, str_to_wsgi, wsgi_to_str, wsgi_quote
358
+ HTTPServiceUnavailable, Response, Range, normalize_etag, \
359
+ RESPONSE_REASONS, str_to_wsgi, bytes_to_wsgi, wsgi_to_str, wsgi_quote
336
360
  from swift.common.utils import get_logger, config_true_value, \
337
- get_valid_utf8_str, override_bytes_from_content_type, split_path, \
338
- register_swift_info, RateLimitedIterator, quote, close_if_possible, \
339
- closing_if_possible, LRUCache, StreamingPile, strict_b64decode, \
340
- Timestamp
361
+ override_bytes_from_content_type, split_path, \
362
+ RateLimitedIterator, quote, closing_if_possible, \
363
+ LRUCache, StreamingPile, strict_b64decode, Timestamp, friendly_close, \
364
+ md5, parse_header
365
+ from swift.common.registry import register_swift_info
341
366
  from swift.common.request_helpers import SegmentedIterable, \
342
367
  get_sys_meta_prefix, update_etag_is_at_header, resolve_etag_is_at_header, \
343
- get_container_update_override_key
368
+ get_container_update_override_key, update_ignore_range_header, \
369
+ get_param, get_valid_part_num
344
370
  from swift.common.constraints import check_utf8
345
- from swift.common.http import HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, is_success
346
- from swift.common.wsgi import WSGIContext, make_subrequest
371
+ from swift.common.http import HTTP_NOT_FOUND, HTTP_UNAUTHORIZED
372
+ from swift.common.wsgi import WSGIContext, make_subrequest, make_env, \
373
+ make_pre_authed_request
347
374
  from swift.common.middleware.bulk import get_response_body, \
348
375
  ACCEPTABLE_FORMATS, Bulk
376
+ from swift.obj import expirer
377
+ from swift.proxy.controllers.base import get_container_info
349
378
 
350
379
 
351
380
  DEFAULT_RATE_LIMIT_UNDER_SIZE = 1024 ** 2 # 1 MiB
@@ -431,12 +460,12 @@ def parse_and_validate_input(req_body, req_path):
431
460
  continue
432
461
 
433
462
  if segment_type == 'path':
434
- if not isinstance(seg_dict['path'], six.string_types):
463
+ if not isinstance(seg_dict['path'], str):
435
464
  errors.append(b"Index %d: \"path\" must be a string" %
436
465
  seg_index)
437
466
  continue
438
467
  if not (seg_dict.get('etag') is None or
439
- isinstance(seg_dict['etag'], six.string_types)):
468
+ isinstance(seg_dict['etag'], str)):
440
469
  errors.append(b'Index %d: "etag" must be a string or null '
441
470
  b'(if provided)' % seg_index)
442
471
  continue
@@ -517,6 +546,183 @@ def parse_and_validate_input(req_body, req_path):
517
546
  return parsed_data
518
547
 
519
548
 
549
+ def _annotate_segments(segments, logger=None):
550
+ """
551
+ Decode any inlined data and update sub_slo segments bytes from content-type
552
+ when available; then annotate segment dicts in segments list with
553
+ 'segment_length'.
554
+
555
+ N.B. raw_data segments don't have a bytes key and range-segments need to
556
+ calculate their length from their range key but afterwards all segments
557
+ dicts will have 'segment_length' representing the length of the segment.
558
+ """
559
+ for seg_dict in segments:
560
+ if 'data' in seg_dict:
561
+ seg_dict['raw_data'] = base64.b64decode(seg_dict.pop('data'))
562
+ segment_length = len(seg_dict['raw_data'])
563
+ else:
564
+ if config_true_value(seg_dict.get('sub_slo')):
565
+ override_bytes_from_content_type(
566
+ seg_dict, logger=logger)
567
+ seg_range = seg_dict.get('range')
568
+ if seg_range is not None:
569
+ # The range is of the form N-M, where N and M are both
570
+ # positive decimal integers. We know this because this
571
+ # middleware is the only thing that creates the SLO
572
+ # manifests stored in the cluster.
573
+ range_start, range_end = [
574
+ int(x) for x in seg_range.split('-')]
575
+ segment_length = (range_end - range_start) + 1
576
+ else:
577
+ segment_length = int(seg_dict['bytes'])
578
+ seg_dict['segment_length'] = segment_length
579
+
580
+
581
+ def calculate_byterange_for_part_num(req, segments, part_num):
582
+ """
583
+ Helper function to calculate the byterange for a part_num response.
584
+
585
+ N.B. as a side-effect of calculating the single tuple representing the
586
+ byterange required for a part_num response this function will also mutate
587
+ the request's Range header so that swob knows to return 206.
588
+
589
+ :param req: the request object
590
+ :param segments: the list of seg_dicts
591
+ :param part_num: the part number of the object to return
592
+
593
+ :returns: a tuple representing the byterange
594
+ """
595
+ start = 0
596
+ for seg in segments[:part_num - 1]:
597
+ start += seg['segment_length']
598
+ last = start + segments[part_num - 1]['segment_length']
599
+ # We need to mutate the request's Range header so that swob knows to
600
+ # handle these partial content requests correctly.
601
+ req.range = "bytes=%d-%d" % (start, last - 1)
602
+ return start, last - 1
603
+
604
+
605
+ def calculate_byteranges(req, segments, resp_attrs, part_num):
606
+ """
607
+ Calculate the byteranges based on the request, segments, and part number.
608
+
609
+ N.B. as a side-effect of calculating the single tuple representing the
610
+ byterange required for a part_num response this function will also mutate
611
+ the request's Range header so that swob knows to return 206.
612
+
613
+ :param req: the request object
614
+ :param segments: the list of seg_dicts
615
+ :param resp_attrs: the slo response attributes
616
+ :param part_num: the part number of the object to return
617
+
618
+ :returns: a list of tuples representing byteranges
619
+ """
620
+ if req.range:
621
+ byteranges = [
622
+ # For some reason, swob.Range.ranges_for_length adds 1 to the
623
+ # last byte's position.
624
+ (start, end - 1) for start, end
625
+ in req.range.ranges_for_length(resp_attrs.slo_size)]
626
+ elif part_num:
627
+ byteranges = [
628
+ calculate_byterange_for_part_num(req, segments, part_num)]
629
+ else:
630
+ byteranges = [(0, resp_attrs.slo_size - 1)]
631
+
632
+ return byteranges
633
+
634
+
635
+ class RespAttrs(object):
636
+ """
637
+ Encapsulate properties of a GET or HEAD response that are pertinent to
638
+ handling a potential SLO response.
639
+
640
+ Instances of this class are typically constructed using the
641
+ ``from_headers`` method.
642
+
643
+ :param is_slo: True if the response appears to be an SLO manifest, False
644
+ otherwise.
645
+ :param timestamp: an instance of :class:`~swift.common.utils.Timestamp`.
646
+ :param manifest_etag: the Etag of the manifest object, or None if
647
+ ``is_slo`` is False.
648
+ :param slo_etag: the Etag of the SLO.
649
+ :param slo_size: the size of the SLO.
650
+ """
651
+ def __init__(self, is_slo, timestamp, manifest_etag, slo_etag, slo_size):
652
+ self.is_slo = bool(is_slo)
653
+ self.timestamp = Timestamp(timestamp or 0)
654
+ # manifest_etag is unambiguous, but json_md5 is even more explicit
655
+ self.json_md5 = manifest_etag or ''
656
+ self.slo_etag = slo_etag or ''
657
+ try:
658
+ # even though it's from sysmeta, we have to worry about empty
659
+ # values - see test_get_invalid_sysmeta_passthrough
660
+ self.slo_size = int(slo_size)
661
+ except (ValueError, TypeError):
662
+ self.slo_size = -1
663
+ self.is_legacy = not self._has_size_and_etag()
664
+
665
+ def _has_size_and_etag(self):
666
+ return self.slo_size >= 0 and self.slo_etag
667
+
668
+ @classmethod
669
+ def from_headers(cls, response_headers):
670
+ """
671
+ Inspect response headers and extract any resp_attrs we can find.
672
+
673
+ :param response_headers: list of tuples from a object response
674
+ :returns: an instance of RespAttrs to represent the response headers
675
+ """
676
+ is_slo = False
677
+ timestamp = None
678
+ found_etag = None
679
+ slo_etag = None
680
+ slo_size = None
681
+ for header, value in response_headers:
682
+ header = header.lower()
683
+ if header == 'x-static-large-object':
684
+ is_slo = config_true_value(value)
685
+ elif header == 'x-backend-timestamp':
686
+ timestamp = value
687
+ elif header == 'etag':
688
+ found_etag = value
689
+ elif header == SYSMETA_SLO_ETAG:
690
+ slo_etag = value
691
+ elif header == SYSMETA_SLO_SIZE:
692
+ slo_size = value
693
+ manifest_etag = found_etag if is_slo else None
694
+ return cls(is_slo, timestamp, manifest_etag, slo_etag, slo_size)
695
+
696
+ def update_from_segments(self, segments):
697
+ """
698
+ Always called if SLO has fetched the manifest response body, for
699
+ legacy manifests we'll calculate size/etag values we wouldn't have
700
+ gotten from sys-meta headers.
701
+ """
702
+ # we only have to set size/etag once; it doesn't matter if we got the
703
+ # values from sysmeta headers or segments
704
+ if self._has_size_and_etag():
705
+ return
706
+
707
+ calculated_size = 0
708
+ calculated_etag = md5(usedforsecurity=False)
709
+
710
+ for seg_dict in segments:
711
+ calculated_size += seg_dict['segment_length']
712
+
713
+ if 'raw_data' in seg_dict:
714
+ r = md5(seg_dict['raw_data'],
715
+ usedforsecurity=False).hexdigest()
716
+ elif seg_dict.get('range'):
717
+ r = '%s:%s;' % (seg_dict['hash'], seg_dict['range'])
718
+ else:
719
+ r = seg_dict['hash']
720
+ calculated_etag.update(r.encode('ascii'))
721
+
722
+ self.slo_size = calculated_size
723
+ self.slo_etag = calculated_etag.hexdigest()
724
+
725
+
520
726
  class SloGetContext(WSGIContext):
521
727
 
522
728
  max_slo_recursion_depth = 10
@@ -524,6 +730,8 @@ class SloGetContext(WSGIContext):
524
730
  def __init__(self, slo):
525
731
  self.slo = slo
526
732
  super(SloGetContext, self).__init__(slo.app)
733
+ # we'll know more after we look at the response metadata
734
+ self.segment_listing_needed = False
527
735
 
528
736
  def _fetch_sub_slo_segments(self, req, version, acc, con, obj):
529
737
  """
@@ -544,19 +752,23 @@ class SloGetContext(WSGIContext):
544
752
  method='GET',
545
753
  headers={'x-auth-token': req.headers.get('x-auth-token')},
546
754
  agent='%(orig)s SLO MultipartGET', swift_source='SLO')
755
+ params_copy = dict(req.params)
756
+ params_copy.pop('part-number', None)
757
+ sub_req.params = params_copy
547
758
  sub_resp = sub_req.get_response(self.slo.app)
548
759
 
549
760
  if not sub_resp.is_success:
550
- close_if_possible(sub_resp.app_iter)
551
- raise ListingIterError(
552
- 'while fetching %s, GET of submanifest %s '
553
- 'failed with status %d' % (req.path, sub_req.path,
554
- sub_resp.status_int))
761
+ # Error message should be short
762
+ body = sub_resp.body.decode('utf-8')
763
+ msg = ('while fetching %s, GET of submanifest %s '
764
+ 'failed with status %d (%s)')
765
+ raise ListingIterError(msg % (
766
+ req.path, sub_req.path, sub_resp.status_int,
767
+ body if len(body) <= 60 else body[:57] + '...'))
555
768
 
556
769
  try:
557
- with closing_if_possible(sub_resp.app_iter):
558
- return json.loads(b''.join(sub_resp.app_iter))
559
- except ValueError as err:
770
+ return self._parse_segments(sub_resp.app_iter)
771
+ except HTTPException as err:
560
772
  raise ListingIterError(
561
773
  'while fetching %s, JSON-decoding of submanifest %s '
562
774
  'failed with %s' % (req.path, sub_req.path, err))
@@ -567,32 +779,8 @@ class SloGetContext(WSGIContext):
567
779
  conobj=seg_dict['name'].lstrip('/')
568
780
  )
569
781
 
570
- def _segment_length(self, seg_dict):
571
- """
572
- Returns the number of bytes that will be fetched from the specified
573
- segment on a plain GET request for this SLO manifest.
574
- """
575
- if 'raw_data' in seg_dict:
576
- return len(seg_dict['raw_data'])
577
-
578
- seg_range = seg_dict.get('range')
579
- if seg_range is not None:
580
- # The range is of the form N-M, where N and M are both positive
581
- # decimal integers. We know this because this middleware is the
582
- # only thing that creates the SLO manifests stored in the
583
- # cluster.
584
- range_start, range_end = [int(x) for x in seg_range.split('-')]
585
- return (range_end - range_start) + 1
586
- else:
587
- return int(seg_dict['bytes'])
588
-
589
782
  def _segment_listing_iterator(self, req, version, account, segments,
590
783
  byteranges):
591
- for seg_dict in segments:
592
- if config_true_value(seg_dict.get('sub_slo')):
593
- override_bytes_from_content_type(seg_dict,
594
- logger=self.slo.logger)
595
-
596
784
  # We handle the range stuff here so that we can be smart about
597
785
  # skipping unused submanifests. For example, if our first segment is a
598
786
  # submanifest referencing 50 MiB total, but start_byte falls in
@@ -600,9 +788,6 @@ class SloGetContext(WSGIContext):
600
788
  #
601
789
  # If we were to make SegmentedIterable handle all the range
602
790
  # calculations, we would be unable to make this optimization.
603
- total_length = sum(self._segment_length(seg) for seg in segments)
604
- if not byteranges:
605
- byteranges = [(0, total_length - 1)]
606
791
 
607
792
  # Cache segments from sub-SLOs in case more than one byterange
608
793
  # includes data from a particular sub-SLO. We only cache a few sets
@@ -629,12 +814,26 @@ class SloGetContext(WSGIContext):
629
814
  first_byte, last_byte,
630
815
  cached_fetch_sub_slo_segments,
631
816
  recursion_depth=1):
817
+ """
818
+ Iterable that generates a filtered and annotated stream of segment
819
+ dicts describing the sub-segment ranges that would be used by the
820
+ SegmentedIterable to construct the bytes for a ranged response.
821
+
822
+ :param req: original request object
823
+ :param version: version
824
+ :param account: account
825
+ :param segments: segments dictionary
826
+ :param first_byte: offset into the large object for the first byte
827
+ that is returned to the client
828
+ :param last_byte: offset into the large object for the last byte
829
+ that is returned to the client
830
+ :param cached_fetch_sub_slo_segments: LRU cache used for fetching
831
+ sub-segments
832
+ :param recursion_depth: max number of recursive sub_slo calls
833
+ """
632
834
  last_sub_path = None
633
835
  for seg_dict in segments:
634
- if 'data' in seg_dict:
635
- seg_dict['raw_data'] = strict_b64decode(seg_dict.pop('data'))
636
-
637
- seg_length = self._segment_length(seg_dict)
836
+ seg_length = seg_dict['segment_length']
638
837
  if first_byte >= seg_length:
639
838
  # don't need any bytes from this segment
640
839
  first_byte -= seg_length
@@ -670,10 +869,7 @@ class SloGetContext(WSGIContext):
670
869
  "While processing manifest %r, "
671
870
  "max recursion depth was exceeded" % req.path)
672
871
 
673
- if six.PY2:
674
- sub_path = get_valid_utf8_str(seg_dict['name'])
675
- else:
676
- sub_path = seg_dict['name']
872
+ sub_path = seg_dict['name']
677
873
  sub_cont, sub_obj = split_path(sub_path, 2, 2, True)
678
874
  if last_sub_path != sub_path:
679
875
  sub_segments = cached_fetch_sub_slo_segments(
@@ -692,8 +888,6 @@ class SloGetContext(WSGIContext):
692
888
  recursion_depth=recursion_depth + 1):
693
889
  yield sub_seg_dict
694
890
  else:
695
- if six.PY2 and isinstance(seg_dict['name'], six.text_type):
696
- seg_dict['name'] = seg_dict['name'].encode("utf-8")
697
891
  yield dict(seg_dict,
698
892
  first_byte=max(0, first_byte) + range_start,
699
893
  last_byte=min(range_end, range_start + last_byte))
@@ -701,50 +895,221 @@ class SloGetContext(WSGIContext):
701
895
  first_byte -= seg_length
702
896
  last_byte -= seg_length
703
897
 
704
- def _need_to_refetch_manifest(self, req):
898
+ def _is_body_complete(self):
899
+ content_range = ''
900
+ for header, value in self._response_headers:
901
+ if header.lower() == 'content-range':
902
+ content_range = value
903
+ break
904
+ # e.g. Content-Range: bytes 0-14289/14290
905
+ match = re.match(r'bytes (\d+)-(\d+)/(\d+)$', content_range)
906
+ if not match:
907
+ # Malformed or missing, so we don't know what we got.
908
+ return False
909
+ first_byte, last_byte, length = [int(x) for x in match.groups()]
910
+ # If and only if we actually got back the full manifest body, then
911
+ # we can avoid re-fetching the object.
912
+ return first_byte == 0 and last_byte == length - 1
913
+
914
+ def _need_to_refetch_manifest(self, req, resp_attrs, is_part_num_request):
705
915
  """
706
- Just because a response shows that an object is a SLO manifest does not
707
- mean that response's body contains the entire SLO manifest. If it
708
- doesn't, we need to make a second request to actually get the whole
709
- thing.
916
+ Check if the segments will be needed to service the request and update
917
+ the segment_listing_needed attribute.
710
918
 
711
- Note: this assumes that X-Static-Large-Object has already been found.
919
+ :return: boolean indicating if we need to refetch, only if the segments
920
+ ARE needed we MAY need to refetch them!
712
921
  """
713
922
  if req.method == 'HEAD':
714
- # We've already looked for SYSMETA_SLO_ETAG/SIZE in the response
715
- # and didn't find them. We have to fetch the whole manifest and
716
- # recompute.
923
+ # There may be some cases in the future where a HEAD resp on even a
924
+ # modern manifest should refetch, e.g. lp bug #2029174
925
+ self.segment_listing_needed = (resp_attrs.is_legacy or
926
+ is_part_num_request)
927
+ # it will always be the case that a HEAD must re-fetch iff
928
+ # segment_listing_needed
929
+ return self.segment_listing_needed
930
+
931
+ last_resp_status_int = self._get_status_int()
932
+ # These are based on etag (or last-modified), but the SLO's etag is
933
+ # almost certainly not the manifest object's etag. Still, it's highly
934
+ # likely that the submitted If-None-Match won't match the manifest
935
+ # object's etag, so we can avoid re-fetching the manifest if we got a
936
+ # successful response.
937
+ if last_resp_status_int in (412, 304):
938
+ # a conditional response from a modern manifest would have an
939
+ # accurate SLO etag, AND comparison with the etag-is-at header, but
940
+ # for legacy manifests responses (who always need to calculate the
941
+ # correct etag, even for if-[un]modified-since errors) we can't say
942
+ # what the etag is or if it matches unless we calculate it from
943
+ # segments - so we always need them
944
+ self.segment_listing_needed = resp_attrs.is_legacy
945
+ # if we need them; we can't get them from the error
946
+ return self.segment_listing_needed
947
+
948
+ # This is GET request for an SLO object, if we're going to return a
949
+ # successful response we're going to need the segments, but this
950
+ # resp_iter may not contain the entire SLO manifest.
951
+ self.segment_listing_needed = True
952
+
953
+ # modern swift object-servers should ignore Range headers on manifests,
954
+ # but during upgrade if we get a range response we'll probably have to
955
+ # refetch
956
+ if last_resp_status_int == 416:
957
+ # if the range wasn't satisfiable we need to refetch
717
958
  return True
959
+ elif last_resp_status_int == 206:
960
+ # a partial response might included the whole content-range?!
961
+ return not self._is_body_complete()
962
+ else:
963
+ # a good number of error responses would have returned earlier for
964
+ # lacking is_slo sys-meta, at this point we've filtered all the
965
+ # other response codes, so this is a prefectly normal 200 response,
966
+ # no need to refetch
967
+ return False
968
+
969
+ def _refetch_manifest(self, req, resp_iter, orig_resp_attrs):
970
+ req.environ['swift.non_client_disconnect'] = True
971
+ friendly_close(resp_iter)
972
+ del req.environ['swift.non_client_disconnect']
973
+
974
+ headers_subset = ['x-auth-token', 'x-open-expired']
975
+ get_req = make_subrequest(
976
+ req.environ, method='GET',
977
+ headers={k: req.headers.get(k)
978
+ for k in headers_subset if k in req.headers},
979
+ agent='%(orig)s SLO MultipartGET', swift_source='SLO')
980
+ resp_iter = self._app_call(get_req.environ)
981
+ new_resp_attrs = RespAttrs.from_headers(self._response_headers)
982
+ if new_resp_attrs.timestamp < orig_resp_attrs.timestamp and \
983
+ not new_resp_attrs.is_slo:
984
+ # Our *orig_resp_attrs* saw *newer* data that indicated it was an
985
+ # SLO, but on refetch it's an older object or error; 503 seems
986
+ # reasonable?
987
+ friendly_close(resp_iter)
988
+ raise HTTPServiceUnavailable(request=req)
989
+ # else, the caller will know how to return this response
990
+ return new_resp_attrs, resp_iter
991
+
992
+ def _parse_segments(self, resp_iter):
993
+ """
994
+ Read the manifest body and parse segments.
718
995
 
719
- response_status = int(self._response_status[:3])
996
+ :returns: segments
997
+ :raises: HTTPServerError
998
+ """
999
+ segments = self._get_manifest_read(resp_iter)
1000
+ _annotate_segments(segments, logger=self.slo.logger)
1001
+ return segments
720
1002
 
721
- # These are based on etag, and the SLO's etag is almost certainly not
722
- # the manifest object's etag. Still, it's highly likely that the
723
- # submitted If-None-Match won't match the manifest object's etag, so
724
- # we can avoid re-fetching the manifest if we got a successful
725
- # response.
726
- if ((req.if_match or req.if_none_match) and
727
- not is_success(response_status)):
728
- return True
1003
+ def _return_manifest_response(self, req, start_response, resp_iter,
1004
+ is_format_raw):
1005
+ if is_format_raw:
1006
+ json_data = self.convert_segment_listing(resp_iter)
1007
+ # we've created a new response body
1008
+ resp_iter = [json_data]
1009
+ replace_headers = {
1010
+ # Note that we have to return the large object's content-type
1011
+ # (not application/json) so it's like what the client sent on
1012
+ # PUT. Otherwise, server-side copy won't work.
1013
+ 'Content-Length': len(json_data),
1014
+ 'Etag': md5(json_data, usedforsecurity=False).hexdigest(),
1015
+ }
1016
+ else:
1017
+ # we're going to return the manifest resp_iter as-is
1018
+ replace_headers = {
1019
+ 'Content-Type': 'application/json; charset=utf-8',
1020
+ }
1021
+ return self._return_response(req, start_response, resp_iter,
1022
+ replace_headers)
1023
+
1024
+ def _return_slo_response(self, req, start_response, resp_iter, resp_attrs):
1025
+ headers = {
1026
+ 'Etag': '"%s"' % resp_attrs.slo_etag,
1027
+ 'X-Manifest-Etag': resp_attrs.json_md5,
1028
+ # swob will fix this for a GET with Range
1029
+ 'Content-Length': str(resp_attrs.slo_size),
1030
+ # ignore bogus content-range, make swob figure it out
1031
+ 'Content-Range': None,
1032
+ }
1033
+ if self.segment_listing_needed:
1034
+ # consume existing resp_iter; we'll create a new one
1035
+ segments = self._parse_segments(resp_iter)
1036
+ resp_attrs.update_from_segments(segments)
1037
+ headers['Etag'] = '"%s"' % resp_attrs.slo_etag
1038
+ headers['Content-Length'] = str(resp_attrs.slo_size)
1039
+ part_num = get_valid_part_num(req)
1040
+ if part_num:
1041
+ headers['X-Parts-Count'] = len(segments)
1042
+
1043
+ if part_num and part_num > len(segments):
1044
+ if req.method == 'HEAD':
1045
+ resp_iter = []
1046
+ headers['Content-Length'] = '0'
1047
+ else:
1048
+ body = b'The requested part number is not satisfiable'
1049
+ resp_iter = [body]
1050
+ headers['Content-Length'] = len(body)
1051
+ headers['Content-Range'] = 'bytes */%d' % resp_attrs.slo_size
1052
+ self._response_status = '416 Requested Range Not Satisfiable'
1053
+ elif part_num and req.method == 'HEAD':
1054
+ resp_iter = []
1055
+ headers['Content-Length'] = \
1056
+ segments[part_num - 1].get('segment_length')
1057
+ start, end = calculate_byterange_for_part_num(
1058
+ req, segments, part_num)
1059
+ headers['Content-Range'] = \
1060
+ 'bytes {}-{}/{}'.format(start, end,
1061
+ resp_attrs.slo_size)
1062
+ # The RFC specifies 206 in the context of Range requests, and
1063
+ # Range headers MUST be ignored for HEADs [1], so a HEAD will
1064
+ # not normally return a 206. However, a part-number HEAD
1065
+ # returns Content-Length equal to the part size, rather than
1066
+ # the whole object size, so in this case we do return 206.
1067
+ # [1] https://www.rfc-editor.org/rfc/rfc9110#name-range
1068
+ self._response_status = '206 Partial Content'
1069
+ elif req.method == 'HEAD':
1070
+ resp_iter = []
1071
+ else:
1072
+ byteranges = calculate_byteranges(
1073
+ req, segments, resp_attrs, part_num)
1074
+ resp_iter = self._build_resp_iter(req, segments, byteranges)
1075
+ return self._return_response(req, start_response, resp_iter,
1076
+ replace_headers=headers)
1077
+
1078
+ def _return_response(self, req, start_response, resp_iter,
1079
+ replace_headers):
1080
+ if req.method == 'HEAD' or self._get_status_int() in (412, 304):
1081
+ # we should drain HEAD and unmet condition responses since they
1082
+ # don't have bodies
1083
+ friendly_close(resp_iter)
1084
+ resp_iter = b''
1085
+ resp_headers = HeaderKeyDict(self._response_headers, **replace_headers)
1086
+ resp = Response(
1087
+ status=self._response_status,
1088
+ headers=resp_headers,
1089
+ app_iter=resp_iter,
1090
+ request=req,
1091
+ conditional_response=True,
1092
+ conditional_etag=resolve_etag_is_at_header(req, resp_headers))
1093
+ return resp(req.environ, start_response)
729
1094
 
730
- if req.range and response_status in (206, 416):
731
- content_range = ''
732
- for header, value in self._response_headers:
733
- if header.lower() == 'content-range':
734
- content_range = value
735
- break
736
- # e.g. Content-Range: bytes 0-14289/14290
737
- match = re.match(r'bytes (\d+)-(\d+)/(\d+)$', content_range)
738
- if not match:
739
- # Malformed or missing, so we don't know what we got.
740
- return True
741
- first_byte, last_byte, length = [int(x) for x in match.groups()]
742
- # If and only if we actually got back the full manifest body, then
743
- # we can avoid re-fetching the object.
744
- got_everything = (first_byte == 0 and last_byte == length - 1)
745
- return not got_everything
746
-
747
- return False
1095
+ def _return_non_slo_response(self, req, start_response, resp_iter):
1096
+ # our "pass-through" response may have been from a manifest refetch w/o
1097
+ # range/conditional headers that turned out to be a real object, and
1098
+ # now we want out. But if the original client request included Range
1099
+ # or Conditional headers we can trust swob to do the right conversion
1100
+ # back into a 206/416/304/412 (as long as the response we have is a
1101
+ # normal successful response and we respect any forwarding middleware's
1102
+ # etag-is-at header that we stripped off for the refetch!)
1103
+ resp = Response(
1104
+ status=self._response_status,
1105
+ headers=self._response_headers,
1106
+ app_iter=resp_iter,
1107
+ request=req,
1108
+ conditional_response=self._get_status_int() == 200,
1109
+ conditional_etag=resolve_etag_is_at_header(
1110
+ req, self._response_headers)
1111
+ )
1112
+ return resp(req.environ, start_response)
748
1113
 
749
1114
  def handle_slo_get_or_head(self, req, start_response):
750
1115
  """
@@ -757,134 +1122,72 @@ class SloGetContext(WSGIContext):
757
1122
  large object manifest.
758
1123
  :param start_response: WSGI start_response callable
759
1124
  """
760
- if req.params.get('multipart-manifest') != 'get':
1125
+ is_manifest_get = get_param(req, 'multipart-manifest') == 'get'
1126
+ is_format_raw = is_manifest_get and get_param(req, 'format') == 'raw'
1127
+
1128
+ if not is_manifest_get:
761
1129
  # If this object is an SLO manifest, we may have saved off the
762
1130
  # large object etag during the original PUT. Send an
763
- # X-Backend-Etag-Is-At header so that, if the SLO etag *was*
764
- # saved, we can trust the object-server to respond appropriately
765
- # to If-Match/If-None-Match requests.
1131
+ # X-Backend-Etag-Is-At header so that, if the SLO etag *was* saved,
1132
+ # we can trust the object-server to respond appropriately to
1133
+ # If-Match/If-None-Match requests.
766
1134
  update_etag_is_at_header(req, SYSMETA_SLO_ETAG)
767
- resp_iter = self._app_call(req.environ)
1135
+ # Tell the object server that if it's a manifest,
1136
+ # we want the whole thing
1137
+ update_ignore_range_header(req, 'X-Static-Large-Object')
768
1138
 
769
- # make sure this response is for a static large object manifest
770
- slo_marker = slo_etag = slo_size = slo_timestamp = None
771
- for header, value in self._response_headers:
772
- header = header.lower()
773
- if header == SYSMETA_SLO_ETAG:
774
- slo_etag = value
775
- elif header == SYSMETA_SLO_SIZE:
776
- slo_size = value
777
- elif (header == 'x-static-large-object' and
778
- config_true_value(value)):
779
- slo_marker = value
780
- elif header == 'x-backend-timestamp':
781
- slo_timestamp = value
782
-
783
- if slo_marker and slo_etag and slo_size and slo_timestamp:
784
- break
785
-
786
- if not slo_marker:
787
- # Not a static large object manifest. Just pass it through.
788
- start_response(self._response_status,
789
- self._response_headers,
790
- self._response_exc_info)
791
- return resp_iter
792
-
793
- # Handle pass-through request for the manifest itself
794
- if req.params.get('multipart-manifest') == 'get':
795
- if req.params.get('format') == 'raw':
796
- resp_iter = self.convert_segment_listing(
797
- self._response_headers, resp_iter)
798
- else:
799
- new_headers = []
800
- for header, value in self._response_headers:
801
- if header.lower() == 'content-type':
802
- new_headers.append(('Content-Type',
803
- 'application/json; charset=utf-8'))
804
- else:
805
- new_headers.append((header, value))
806
- self._response_headers = new_headers
807
- start_response(self._response_status,
808
- self._response_headers,
809
- self._response_exc_info)
810
- return resp_iter
811
-
812
- is_conditional = self._response_status.startswith(('304', '412')) and (
813
- req.if_match or req.if_none_match)
814
- if slo_etag and slo_size and (
815
- req.method == 'HEAD' or is_conditional):
816
- # Since we have length and etag, we can respond immediately
817
- resp = Response(
818
- status=self._response_status,
819
- headers=self._response_headers,
820
- app_iter=resp_iter,
821
- request=req,
822
- conditional_etag=resolve_etag_is_at_header(
823
- req, self._response_headers),
824
- conditional_response=True)
825
- resp.headers.update({
826
- 'Etag': '"%s"' % slo_etag,
827
- 'X-Manifest-Etag': self._response_header_value('etag'),
828
- 'Content-Length': slo_size,
829
- })
830
- return resp(req.environ, start_response)
831
-
832
- if self._need_to_refetch_manifest(req):
833
- req.environ['swift.non_client_disconnect'] = True
834
- close_if_possible(resp_iter)
835
- del req.environ['swift.non_client_disconnect']
836
-
837
- get_req = make_subrequest(
838
- req.environ, method='GET',
839
- headers={'x-auth-token': req.headers.get('x-auth-token')},
840
- agent='%(orig)s SLO MultipartGET', swift_source='SLO')
841
- resp_iter = self._app_call(get_req.environ)
842
- slo_marker = config_true_value(self._response_header_value(
843
- 'x-static-large-object'))
844
- if not slo_marker: # will also catch non-2xx responses
845
- got_timestamp = self._response_header_value(
846
- 'x-backend-timestamp') or '0'
847
- if Timestamp(got_timestamp) >= Timestamp(slo_timestamp):
848
- # We've got a newer response available, so serve that.
849
- # Note that if there's data, it's going to be a 200 now,
850
- # not a 206, and we're not going to drop bytes in the
851
- # proxy on the client's behalf. Fortunately, the RFC is
852
- # pretty forgiving for a server; there's no guarantee that
853
- # a Range header will be respected.
854
- resp = Response(
855
- status=self._response_status,
856
- headers=self._response_headers,
857
- app_iter=resp_iter,
858
- request=req,
859
- conditional_etag=resolve_etag_is_at_header(
860
- req, self._response_headers),
861
- conditional_response=is_success(
862
- int(self._response_status[:3])))
863
- return resp(req.environ, start_response)
864
- else:
865
- # We saw newer data that indicated it's an SLO, but
866
- # couldn't fetch the whole thing; 503 seems reasonable?
867
- close_if_possible(resp_iter)
868
- raise HTTPServiceUnavailable(request=req)
869
- # NB: we might have gotten an out-of-date manifest -- that's OK;
870
- # we'll just try to serve the old data
871
-
872
- # Any Content-Range from a manifest is almost certainly wrong for the
873
- # full large object.
874
- resp_headers = [(h, v) for h, v in self._response_headers
875
- if not h.lower() == 'content-range']
876
-
877
- response = self.get_or_head_response(
878
- req, resp_headers, resp_iter)
879
- return response(req.environ, start_response)
880
-
881
- def convert_segment_listing(self, resp_headers, resp_iter):
1139
+ # process original request
1140
+ orig_path_info = req.path_info
1141
+ resp_iter = self._app_call(req.environ)
1142
+ resp_attrs = RespAttrs.from_headers(self._response_headers)
1143
+ if resp_attrs.is_slo and not is_manifest_get:
1144
+ try:
1145
+ # only validate part-number if the request is to an SLO
1146
+ part_num = get_valid_part_num(req)
1147
+ except HTTPException:
1148
+ friendly_close(resp_iter)
1149
+ raise
1150
+ # the next two calls hide a couple side effects, sorry:
1151
+ #
1152
+ # 1) regardless of the return value the "need_to_refetch" check
1153
+ # *may* also set self.segment_listing_needed = True (it's
1154
+ # commented to help you wrap your head around that one,
1155
+ # good luck)
1156
+ # 2) if we refetch, we overwrite the current resp_iter and
1157
+ # resp_attrs variables, partly because we *might* get back a NOT
1158
+ # resp_attrs.is_slo response (even if we had one to start), but
1159
+ # hopefully they're just the manifest resp we needed to refetch!
1160
+ if self._need_to_refetch_manifest(req, resp_attrs, part_num):
1161
+ # reset path in case it was modified during original request
1162
+ # (e.g. object versioning might re-write the path)
1163
+ req.path_info = orig_path_info
1164
+ resp_attrs, resp_iter = self._refetch_manifest(
1165
+ req, resp_iter, resp_attrs)
1166
+
1167
+ if not resp_attrs.is_slo:
1168
+ # even if the original resp_attrs may have been SLO we may have
1169
+ # refetched, this also handles the server error case
1170
+ return self._return_non_slo_response(
1171
+ req, start_response, resp_iter)
1172
+
1173
+ if is_manifest_get:
1174
+ # manifest pass through doesn't require resp_attrs
1175
+ return self._return_manifest_response(req, start_response,
1176
+ resp_iter, is_format_raw)
1177
+
1178
+ # this a GET/HEAD response for the SLO object (not the manifest)
1179
+ return self._return_slo_response(req, start_response, resp_iter,
1180
+ resp_attrs)
1181
+
1182
+ def convert_segment_listing(self, resp_iter):
882
1183
  """
883
1184
  Converts the manifest data to match with the format
884
1185
  that was put in through ?multipart-manifest=put
885
1186
 
886
- :param resp_headers: response headers
887
1187
  :param resp_iter: a response iterable
1188
+
1189
+ :raises HTTPServerError:
1190
+ :returns: the json-serialized raw format (as bytes)
888
1191
  """
889
1192
  segments = self._get_manifest_read(resp_iter)
890
1193
 
@@ -898,107 +1201,30 @@ class SloGetContext(WSGIContext):
898
1201
  seg_dict['size_bytes'] = seg_dict.pop('bytes', None)
899
1202
  seg_dict['etag'] = seg_dict.pop('hash', None)
900
1203
 
901
- json_data = json.dumps(segments) # convert to string
902
- if six.PY3:
903
- json_data = json_data.encode('utf-8')
904
-
905
- new_headers = []
906
- for header, value in resp_headers:
907
- if header.lower() == 'content-length':
908
- new_headers.append(('Content-Length', len(json_data)))
909
- else:
910
- new_headers.append((header, value))
911
- self._response_headers = new_headers
912
-
913
- return [json_data]
1204
+ json_data = json.dumps(segments, sort_keys=True) # convert to string
1205
+ return json_data.encode('utf-8')
914
1206
 
915
1207
  def _get_manifest_read(self, resp_iter):
916
1208
  with closing_if_possible(resp_iter):
917
1209
  resp_body = b''.join(resp_iter)
918
1210
  try:
919
1211
  segments = json.loads(resp_body)
920
- except ValueError:
921
- segments = []
922
-
1212
+ except ValueError as e:
1213
+ msg = 'Unable to load SLO manifest'
1214
+ self.slo.logger.error('%s: %s', msg, e)
1215
+ raise HTTPServerError(msg)
923
1216
  return segments
924
1217
 
925
- def get_or_head_response(self, req, resp_headers, resp_iter):
926
- segments = self._get_manifest_read(resp_iter)
927
- slo_etag = None
928
- content_length = None
929
- response_headers = []
930
- for header, value in resp_headers:
931
- lheader = header.lower()
932
- if lheader == 'etag':
933
- response_headers.append(('X-Manifest-Etag', value))
934
- elif lheader != 'content-length':
935
- response_headers.append((header, value))
936
-
937
- if lheader == SYSMETA_SLO_ETAG:
938
- slo_etag = value
939
- elif lheader == SYSMETA_SLO_SIZE:
940
- # it's from sysmeta, so we don't worry about non-integer
941
- # values here
942
- content_length = int(value)
943
-
944
- # Prep to calculate content_length & etag if necessary
945
- if slo_etag is None:
946
- calculated_etag = md5()
947
- if content_length is None:
948
- calculated_content_length = 0
949
-
950
- for seg_dict in segments:
951
- # Decode any inlined data; it's important that we do this *before*
952
- # calculating the segment length and etag
953
- if 'data' in seg_dict:
954
- seg_dict['raw_data'] = base64.b64decode(seg_dict.pop('data'))
955
-
956
- if slo_etag is None:
957
- if 'raw_data' in seg_dict:
958
- r = md5(seg_dict['raw_data']).hexdigest()
959
- elif seg_dict.get('range'):
960
- r = '%s:%s;' % (seg_dict['hash'], seg_dict['range'])
961
- else:
962
- r = seg_dict['hash']
963
- calculated_etag.update(r.encode('ascii'))
964
-
965
- if content_length is None:
966
- if config_true_value(seg_dict.get('sub_slo')):
967
- override_bytes_from_content_type(
968
- seg_dict, logger=self.slo.logger)
969
- calculated_content_length += self._segment_length(seg_dict)
970
-
971
- if slo_etag is None:
972
- slo_etag = calculated_etag.hexdigest()
973
- if content_length is None:
974
- content_length = calculated_content_length
1218
+ def _build_resp_iter(self, req, segments, byteranges):
1219
+ """
1220
+ Build a response iterable for a GET request.
975
1221
 
976
- response_headers.append(('Content-Length', str(content_length)))
977
- response_headers.append(('Etag', '"%s"' % slo_etag))
978
-
979
- if req.method == 'HEAD':
980
- return self._manifest_head_response(req, response_headers)
981
- else:
982
- return self._manifest_get_response(
983
- req, content_length, response_headers, segments)
984
-
985
- def _manifest_head_response(self, req, response_headers):
986
- conditional_etag = resolve_etag_is_at_header(req, response_headers)
987
- return HTTPOk(request=req, headers=response_headers, body=b'',
988
- conditional_etag=conditional_etag,
989
- conditional_response=True)
990
-
991
- def _manifest_get_response(self, req, content_length, response_headers,
992
- segments):
993
- if req.range:
994
- byteranges = [
995
- # For some reason, swob.Range.ranges_for_length adds 1 to the
996
- # last byte's position.
997
- (start, end - 1) for start, end
998
- in req.range.ranges_for_length(content_length)]
999
- else:
1000
- byteranges = []
1222
+ :param req: the request object
1223
+ :param segments: the list of seg_dicts
1224
+ :param byteranges: a list of tuples representing byteranges
1001
1225
 
1226
+ :returns: a segmented iterable
1227
+ """
1002
1228
  ver, account, _junk = req.split_path(3, 3, rest_with_last=True)
1003
1229
  account = wsgi_to_str(account)
1004
1230
  plain_listing_iter = self._segment_listing_iterator(
@@ -1042,15 +1268,8 @@ class SloGetContext(WSGIContext):
1042
1268
  # their Etag/Content Length no longer match the connection
1043
1269
  # will drop. In this case a 409 Conflict will be logged in
1044
1270
  # the proxy logs and the user will receive incomplete results.
1045
- return HTTPConflict(request=req)
1046
-
1047
- conditional_etag = resolve_etag_is_at_header(req, response_headers)
1048
- response = Response(request=req, content_length=content_length,
1049
- headers=response_headers,
1050
- conditional_response=True,
1051
- conditional_etag=conditional_etag,
1052
- app_iter=segmented_iter)
1053
- return response
1271
+ raise HTTPConflict(request=req)
1272
+ return segmented_iter
1054
1273
 
1055
1274
 
1056
1275
  class StaticLargeObject(object):
@@ -1077,13 +1296,15 @@ class StaticLargeObject(object):
1077
1296
  def __init__(self, app, conf,
1078
1297
  max_manifest_segments=DEFAULT_MAX_MANIFEST_SEGMENTS,
1079
1298
  max_manifest_size=DEFAULT_MAX_MANIFEST_SIZE,
1080
- yield_frequency=DEFAULT_YIELD_FREQUENCY):
1299
+ yield_frequency=DEFAULT_YIELD_FREQUENCY,
1300
+ allow_async_delete=True):
1081
1301
  self.conf = conf
1082
1302
  self.app = app
1083
1303
  self.logger = get_logger(conf, log_route='slo')
1084
1304
  self.max_manifest_segments = max_manifest_segments
1085
1305
  self.max_manifest_size = max_manifest_size
1086
1306
  self.yield_frequency = yield_frequency
1307
+ self.allow_async_delete = allow_async_delete
1087
1308
  self.max_get_time = int(self.conf.get('max_get_time', 86400))
1088
1309
  self.rate_limit_under_size = int(self.conf.get(
1089
1310
  'rate_limit_under_size', DEFAULT_RATE_LIMIT_UNDER_SIZE))
@@ -1101,6 +1322,8 @@ class StaticLargeObject(object):
1101
1322
  delete_concurrency=delete_concurrency,
1102
1323
  logger=self.logger)
1103
1324
 
1325
+ self.expirer_config = expirer.ExpirerConfig(conf, logger=self.logger)
1326
+
1104
1327
  def handle_multipart_get_or_head(self, req, start_response):
1105
1328
  """
1106
1329
  Handles the GET or HEAD of a SLO manifest.
@@ -1164,12 +1387,8 @@ class StaticLargeObject(object):
1164
1387
  path2indices[seg_dict['path']].append(index)
1165
1388
 
1166
1389
  def do_head(obj_name):
1167
- if six.PY2:
1168
- obj_path = '/'.join(['', vrs, account,
1169
- get_valid_utf8_str(obj_name).lstrip('/')])
1170
- else:
1171
- obj_path = '/'.join(['', vrs, account,
1172
- str_to_wsgi(obj_name.lstrip('/'))])
1390
+ obj_path = '/'.join(['', vrs, account,
1391
+ str_to_wsgi(obj_name.lstrip('/'))])
1173
1392
  obj_path = wsgi_quote(obj_path)
1174
1393
 
1175
1394
  sub_req = make_subrequest(
@@ -1312,26 +1531,26 @@ class StaticLargeObject(object):
1312
1531
  out_content_type, resp_dict, problem_segments, 'upload')
1313
1532
  return
1314
1533
 
1315
- slo_etag = md5()
1534
+ slo_etag = md5(usedforsecurity=False)
1316
1535
  for seg_data in data_for_storage:
1317
1536
  if 'data' in seg_data:
1318
1537
  raw_data = base64.b64decode(seg_data['data'])
1319
- r = md5(raw_data).hexdigest()
1538
+ r = md5(raw_data, usedforsecurity=False).hexdigest()
1320
1539
  elif seg_data.get('range'):
1321
1540
  r = '%s:%s;' % (seg_data['hash'], seg_data['range'])
1322
1541
  else:
1323
1542
  r = seg_data['hash']
1324
- slo_etag.update(r.encode('ascii') if six.PY3 else r)
1543
+ slo_etag.update(r.encode('ascii'))
1325
1544
 
1326
1545
  slo_etag = slo_etag.hexdigest()
1327
- client_etag = req.headers.get('Etag')
1328
- if client_etag and client_etag.strip('"') != slo_etag:
1546
+ client_etag = normalize_etag(req.headers.get('Etag'))
1547
+ if client_etag and client_etag != slo_etag:
1329
1548
  err = HTTPUnprocessableEntity(request=req)
1330
1549
  if heartbeat:
1331
1550
  resp_dict = {}
1332
1551
  resp_dict['Response Status'] = err.status
1333
1552
  err_body = err.body
1334
- if six.PY3 and isinstance(err_body, bytes):
1553
+ if isinstance(err_body, bytes):
1335
1554
  err_body = err_body.decode('utf-8', errors='replace')
1336
1555
  resp_dict['Response Body'] = err_body or '\n'.join(
1337
1556
  RESPONSE_REASONS.get(err.status_int, ['']))
@@ -1343,15 +1562,13 @@ class StaticLargeObject(object):
1343
1562
  yield chunk
1344
1563
  return
1345
1564
 
1346
- json_data = json.dumps(data_for_storage)
1347
- if six.PY3:
1348
- json_data = json_data.encode('utf-8')
1565
+ json_data = json.dumps(data_for_storage).encode('utf-8')
1349
1566
  req.body = json_data
1350
1567
  req.headers.update({
1351
1568
  SYSMETA_SLO_ETAG: slo_etag,
1352
1569
  SYSMETA_SLO_SIZE: total_size,
1353
1570
  'X-Static-Large-Object': 'True',
1354
- 'Etag': md5(json_data).hexdigest(),
1571
+ 'Etag': md5(json_data, usedforsecurity=False).hexdigest(),
1355
1572
  })
1356
1573
 
1357
1574
  # Ensure container listings have both etags. However, if any
@@ -1380,7 +1597,7 @@ class StaticLargeObject(object):
1380
1597
 
1381
1598
  if heartbeat:
1382
1599
  resp_body = resp.body
1383
- if six.PY3 and isinstance(resp_body, bytes):
1600
+ if isinstance(resp_body, bytes):
1384
1601
  resp_body = resp_body.decode('utf-8')
1385
1602
  resp_dict['Response Body'] = resp_body
1386
1603
  yield separator + get_response_body(
@@ -1406,14 +1623,14 @@ class StaticLargeObject(object):
1406
1623
  raise HTTPPreconditionFailed(
1407
1624
  request=req, body='Invalid UTF8 or contains NULL')
1408
1625
  vrs, account, container, obj = req.split_path(4, 4, True)
1409
- if six.PY2:
1410
- obj_path = ('/%s/%s' % (container, obj)).decode('utf-8')
1411
- else:
1412
- obj_path = '/%s/%s' % (wsgi_to_str(container), wsgi_to_str(obj))
1626
+ obj_path = '/%s/%s' % (wsgi_to_str(container), wsgi_to_str(obj))
1413
1627
 
1414
1628
  segments = [{
1415
1629
  'sub_slo': True,
1416
1630
  'name': obj_path}]
1631
+ if 'version-id' in req.params:
1632
+ segments[0]['version_id'] = req.params['version-id']
1633
+
1417
1634
  while segments:
1418
1635
  # We chose not to set the limit at max_manifest_segments
1419
1636
  # in the case this value was decreased by operators.
@@ -1434,7 +1651,7 @@ class StaticLargeObject(object):
1434
1651
  except HTTPException as err:
1435
1652
  # allow bulk delete response to report errors
1436
1653
  err_body = err.body
1437
- if six.PY3 and isinstance(err_body, bytes):
1654
+ if isinstance(err_body, bytes):
1438
1655
  err_body = err_body.decode('utf-8', errors='replace')
1439
1656
  seg_data['error'] = {'code': err.status_int,
1440
1657
  'message': err_body}
@@ -1443,8 +1660,6 @@ class StaticLargeObject(object):
1443
1660
  seg_data['sub_slo'] = False
1444
1661
  segments.append(seg_data)
1445
1662
  else:
1446
- if six.PY2:
1447
- seg_data['name'] = seg_data['name'].encode('utf-8')
1448
1663
  yield seg_data
1449
1664
 
1450
1665
  def get_slo_segments(self, obj_name, req):
@@ -1464,22 +1679,29 @@ class StaticLargeObject(object):
1464
1679
  vrs, account, _junk = req.split_path(2, 3, True)
1465
1680
  new_env = req.environ.copy()
1466
1681
  new_env['REQUEST_METHOD'] = 'GET'
1467
- del(new_env['wsgi.input'])
1682
+ del new_env['wsgi.input']
1468
1683
  new_env['QUERY_STRING'] = 'multipart-manifest=get'
1684
+ if 'version-id' in req.params:
1685
+ new_env['QUERY_STRING'] += \
1686
+ '&version-id=' + req.params['version-id']
1469
1687
  new_env['CONTENT_LENGTH'] = 0
1470
1688
  new_env['HTTP_USER_AGENT'] = \
1471
1689
  '%s MultipartDELETE' % new_env.get('HTTP_USER_AGENT')
1472
1690
  new_env['swift.source'] = 'SLO'
1473
- if six.PY2:
1474
- new_env['PATH_INFO'] = (
1475
- '/%s/%s/%s' % (vrs, account,
1476
- obj_name.lstrip('/').encode('utf-8'))
1477
- )
1478
- else:
1479
- new_env['PATH_INFO'] = (
1480
- '/%s/%s/%s' % (vrs, account, str_to_wsgi(obj_name.lstrip('/')))
1481
- )
1482
- resp = Request.blank('', new_env).get_response(self.app)
1691
+ new_env['PATH_INFO'] = (
1692
+ '/%s/%s/%s' % (vrs, account, str_to_wsgi(obj_name.lstrip('/')))
1693
+ )
1694
+ # Just request the last byte of non-SLO objects so we don't waste
1695
+ # a resources in friendly_close() below
1696
+ manifest_req = Request.blank('', new_env, range='bytes=-1')
1697
+ update_ignore_range_header(manifest_req, 'X-Static-Large-Object')
1698
+ resp = manifest_req.get_response(self.app)
1699
+
1700
+ if resp.is_success and config_true_value(resp.headers.get(
1701
+ 'X-Static-Large-Object')) and len(resp.body) == 1:
1702
+ # pre-2.24.0 object-server
1703
+ manifest_req = Request.blank('', new_env)
1704
+ resp = manifest_req.get_response(self.app)
1483
1705
 
1484
1706
  if resp.is_success:
1485
1707
  if config_true_value(resp.headers.get('X-Static-Large-Object')):
@@ -1488,6 +1710,8 @@ class StaticLargeObject(object):
1488
1710
  except ValueError:
1489
1711
  raise HTTPServerError('Unable to load SLO manifest')
1490
1712
  else:
1713
+ # Drain and close GET request (prevents socket leaks)
1714
+ friendly_close(resp)
1491
1715
  raise HTTPBadRequest('Not an SLO manifest')
1492
1716
  elif resp.status_int == HTTP_NOT_FOUND:
1493
1717
  raise HTTPNotFound('SLO manifest not found')
@@ -1496,6 +1720,81 @@ class StaticLargeObject(object):
1496
1720
  else:
1497
1721
  raise HTTPServerError('Unable to load SLO manifest or segment.')
1498
1722
 
1723
+ def handle_async_delete(self, req):
1724
+ if not check_utf8(wsgi_to_str(req.path_info)):
1725
+ raise HTTPPreconditionFailed(
1726
+ request=req, body='Invalid UTF8 or contains NULL')
1727
+ vrs, account, container, obj = req.split_path(4, 4, True)
1728
+ obj_path = '/%s/%s' % (wsgi_to_str(container), wsgi_to_str(obj))
1729
+ segments = [seg for seg in self.get_slo_segments(obj_path, req)
1730
+ if 'data' not in seg]
1731
+ if not segments:
1732
+ # Degenerate case: just delete the manifest
1733
+ return self.app
1734
+
1735
+ segment_containers, segment_objects = zip(*(
1736
+ split_path(seg['name'], 2, 2, True) for seg in segments))
1737
+ segment_containers = set(segment_containers)
1738
+ if len(segment_containers) > 1:
1739
+ container_csv = ', '.join(
1740
+ '"%s"' % quote(c) for c in segment_containers)
1741
+ raise HTTPBadRequest('All segments must be in one container. '
1742
+ 'Found segments in %s' % container_csv)
1743
+ if any(seg.get('sub_slo') for seg in segments):
1744
+ raise HTTPBadRequest('No segments may be large objects.')
1745
+
1746
+ # Auth checks
1747
+ segment_container = segment_containers.pop()
1748
+ if 'swift.authorize' in req.environ:
1749
+ container_info = get_container_info(
1750
+ req.environ, self.app, swift_source='SLO')
1751
+ req.acl = container_info.get('write_acl')
1752
+ aresp = req.environ['swift.authorize'](req)
1753
+ req.acl = None
1754
+ if aresp:
1755
+ return aresp
1756
+
1757
+ if bytes_to_wsgi(segment_container.encode('utf-8')) != container:
1758
+ path = '/%s/%s/%s' % (vrs, account, bytes_to_wsgi(
1759
+ segment_container.encode('utf-8')))
1760
+ seg_container_info = get_container_info(
1761
+ make_env(req.environ, path=path, swift_source='SLO'),
1762
+ self.app, swift_source='SLO')
1763
+ req.acl = seg_container_info.get('write_acl')
1764
+ aresp = req.environ['swift.authorize'](req)
1765
+ req.acl = None
1766
+ if aresp:
1767
+ return aresp
1768
+
1769
+ # Did our sanity checks; schedule segments to be deleted
1770
+ ts = req.ensure_x_timestamp()
1771
+ expirer_jobs = make_delete_jobs(
1772
+ wsgi_to_str(account), segment_container, segment_objects, ts)
1773
+ expiring_objects_account, expirer_cont = \
1774
+ self.expirer_config.get_expirer_account_and_container(
1775
+ ts, wsgi_to_str(account), wsgi_to_str(container),
1776
+ wsgi_to_str(obj))
1777
+ enqueue_req = make_pre_authed_request(
1778
+ req.environ,
1779
+ method='UPDATE',
1780
+ path="/v1/%s/%s" % (expiring_objects_account, expirer_cont),
1781
+ body=json.dumps(expirer_jobs),
1782
+ headers={'Content-Type': 'application/json',
1783
+ 'X-Backend-Storage-Policy-Index': '0',
1784
+ 'X-Backend-Allow-Private-Methods': 'True'},
1785
+ )
1786
+ resp = enqueue_req.get_response(self.app)
1787
+ if not resp.is_success:
1788
+ self.logger.error(
1789
+ 'Failed to enqueue expiration entries: %s\n%s',
1790
+ resp.status, resp.body)
1791
+ return HTTPServiceUnavailable()
1792
+ # consume the response (should be short)
1793
+ friendly_close(resp)
1794
+
1795
+ # Finally, delete the manifest
1796
+ return self.app
1797
+
1499
1798
  def handle_multipart_delete(self, req):
1500
1799
  """
1501
1800
  Will delete all the segments in the SLO manifest and then, if
@@ -1504,6 +1803,10 @@ class StaticLargeObject(object):
1504
1803
  :param req: a :class:`~swift.common.swob.Request` with an obj in path
1505
1804
  :returns: swob.Response whose app_iter set to Bulk.handle_delete_iter
1506
1805
  """
1806
+ if self.allow_async_delete and config_true_value(
1807
+ req.params.get('async')):
1808
+ return self.handle_async_delete(req)
1809
+
1507
1810
  req.headers['Content-Type'] = None # Ignore content-type from client
1508
1811
  resp = HTTPOk(request=req)
1509
1812
  try:
@@ -1594,6 +1897,8 @@ def filter_factory(global_conf, **local_conf):
1594
1897
  DEFAULT_MAX_MANIFEST_SIZE))
1595
1898
  yield_frequency = int(conf.get('yield_frequency',
1596
1899
  DEFAULT_YIELD_FREQUENCY))
1900
+ allow_async_delete = config_true_value(conf.get('allow_async_delete',
1901
+ 'true'))
1597
1902
 
1598
1903
  register_swift_info('slo',
1599
1904
  max_manifest_segments=max_manifest_segments,
@@ -1601,12 +1906,14 @@ def filter_factory(global_conf, **local_conf):
1601
1906
  yield_frequency=yield_frequency,
1602
1907
  # this used to be configurable; report it as 1 for
1603
1908
  # clients that might still care
1604
- min_segment_size=1)
1909
+ min_segment_size=1,
1910
+ allow_async_delete=allow_async_delete)
1605
1911
 
1606
1912
  def slo_filter(app):
1607
1913
  return StaticLargeObject(
1608
1914
  app, conf,
1609
1915
  max_manifest_segments=max_manifest_segments,
1610
1916
  max_manifest_size=max_manifest_size,
1611
- yield_frequency=yield_frequency)
1917
+ yield_frequency=yield_frequency,
1918
+ allow_async_delete=allow_async_delete)
1612
1919
  return slo_filter