"Fossies" - the Fresh Open Source Software Archive

Member "swift-2.21.0/swift/common/middleware/symlink.py" (25 Mar 2019, 24421 Bytes) of package /linux/misc/openstack/swift-2.21.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "symlink.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 2.19.1_vs_2.21.0.

    1 # Copyright (c) 2010-2017 OpenStack Foundation
    2 #
    3 # Licensed under the Apache License, Version 2.0 (the "License");
    4 # you may not use this file except in compliance with the License.
    5 # You may obtain a copy of the License at
    6 #
    7 #    http://www.apache.org/licenses/LICENSE-2.0
    8 #
    9 # Unless required by applicable law or agreed to in writing, software
   10 # distributed under the License is distributed on an "AS IS" BASIS,
   11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
   12 # implied.
   13 # See the License for the specific language governing permissions and
   14 # limitations under the License.
   15 
   16 """
   17 Symlink Middleware
   18 
   19 Symlinks are objects stored in Swift that contain a reference to another
   20 object (hereinafter, this is called "target object"). They are analogous to
   21 symbolic links in Unix-like operating systems. The existence of a symlink
   22 object does not affect the target object in any way. An important use case is
   23 to use a path in one container to access an object in a different container,
   24 with a different policy. This allows policy cost/performance trade-offs to be
   25 made on individual objects.
   26 
   27 Clients create a Swift symlink by performing a zero-length PUT request
   28 with the header ``X-Symlink-Target: <container>/<object>``. For a cross-account
   29 symlink, the header ``X-Symlink-Target-Account: <account>`` must be included.
   30 If omitted, it is inserted automatically with the account of the symlink
   31 object in the PUT request process.
   32 
   33 Symlinks must be zero-byte objects. Attempting to PUT a symlink
   34 with a non-empty request body will result in a 400-series error. Also, POST
   35 with X-Symlink-Target header always results in a 400-series error. The target
   36 object need not exist at symlink creation time. It is suggested to set the
   37 ``Content-Type`` of symlink objects to a distinct value such as
   38 ``application/symlink``.
   39 
   40 A GET/HEAD request to a symlink will result in a request to the target
   41 object referenced by the symlink's ``X-Symlink-Target-Account`` and
   42 ``X-Symlink-Target`` headers. The response of the GET/HEAD request will contain
   43 a ``Content-Location`` header with the path location of the target object. A
   44 GET/HEAD request to a symlink with the query parameter ``?symlink=get`` will
   45 result in the request targeting the symlink itself.
   46 
   47 A symlink can point to another symlink. Chained symlinks will be traversed
   48 until target is not a symlink. If the number of chained symlinks exceeds the
   49 limit ``symloop_max`` an error response will be produced. The value of
   50 ``symloop_max`` can be defined in the symlink config section of
   51 `proxy-server.conf`. If not specified, the default ``symloop_max`` value is 2.
   52 If a value less than 1 is specified, the default value will be used.
   53 
   54 A HEAD/GET request to a symlink object behaves as a normal HEAD/GET request
   55 to the target object. Therefore issuing a HEAD request to the symlink will
   56 return the target metadata, and issuing a GET request to the symlink will
   57 return the data and metadata of the target object. To return the symlink
   58 metadata (with its empty body) a GET/HEAD request with the ``?symlink=get``
   59 query parameter must be sent to a symlink object.
   60 
   61 A POST request to a symlink will result in a 307 TemporaryRedirect response.
   62 The response will contain a ``Location`` header with the path of the target
   63 object as the value. The request is never redirected to the target object by
   64 Swift. Nevertheless, the metadata in the POST request will be applied to the
   65 symlink because object servers cannot know for sure if the current object is a
   66 symlink or not in eventual consistency.
   67 
   68 A DELETE request to a symlink will delete the symlink itself. The target
   69 object will not be deleted.
   70 
   71 A COPY request, or a PUT request with a ``X-Copy-From`` header, to a symlink
   72 will copy the target object. The same request to a symlink with the query
   73 parameter ``?symlink=get`` will copy the symlink itself.
   74 
   75 An OPTIONS request to a symlink will respond with the options for the symlink
   76 only, the request will not be redirected to the target object. Please note that
   77 if the symlink's target object is in another container with CORS settings, the
   78 response will not reflect the settings.
   79 
   80 Tempurls can be used to GET/HEAD symlink objects, but PUT is not allowed and
   81 will result in a 400-series error. The GET/HEAD tempurls honor the scope of
   82 the tempurl key. Container tempurl will only work on symlinks where the target
   83 container is the same as the symlink. In case a symlink targets an object
   84 in a different container, a GET/HEAD request will result in a 401 Unauthorized
   85 error. The account level tempurl will allow cross container symlinks.
   86 
   87 If a symlink object is overwritten while it is in a versioned container, the
   88 symlink object itself is versioned, not the referenced object.
   89 
   90 A GET request with query parameter ``?format=json`` to a container which
   91 contains symlinks will respond with additional information ``symlink_path``
   92 for each symlink object in the container listing. The ``symlink_path`` value
   93 is the target path of the symlink. Clients can differentiate symlinks and
   94 other objects by this function. Note that responses of any other format
   95 (e.g.``?format=xml``) won't include ``symlink_path`` info.
   96 
   97 Errors
   98 
   99 * PUT with the header ``X-Symlink-Target`` with non-zero Content-Length
  100   will produce a 400 BadRequest error.
  101 
  102 * POST with the header ``X-Symlink-Target`` will produce a
  103   400 BadRequest error.
  104 
  105 * GET/HEAD traversing more than ``symloop_max`` chained symlinks will
  106   produce a 409 Conflict error.
  107 
  108 * POSTs will produce a 307 TemporaryRedirect error.
  109 
  110 ----------
  111 Deployment
  112 ----------
  113 
  114 Symlinks are enabled by adding the `symlink` middleware to the proxy server
  115 WSGI pipeline and including a corresponding filter configuration section in the
  116 `proxy-server.conf` file. The `symlink` middleware should be placed after
  117 `slo`, `dlo` and `versioned_writes` middleware, but before `encryption`
  118 middleware in the pipeline. See the `proxy-server.conf-sample` file for further
  119 details. :ref:`Additional steps <symlink_container_sync_client_config>` are
  120 required if the container sync feature is being used.
  121 
  122 .. note::
  123 
  124     Once you have deployed `symlink` middleware in your pipeline, you should
  125     neither remove the `symlink` middleware nor downgrade swift to a version
  126     earlier than symlinks being supported. Doing so may result in unexpected
  127     container listing results in addition to symlink objects behaving like a
  128     normal object.
  129 
  130 .. _symlink_container_sync_client_config:
  131 
  132 Container sync configuration
  133 ----------------------------
  134 
  135 If container sync is being used then the `symlink` middleware
  136 must be added to the container sync internal client pipeline. The following
  137 configuration steps are required:
  138 
  139 #. Create a custom internal client configuration file for container sync (if
  140    one is not already in use) based on the sample file
  141    `internal-client.conf-sample`. For example, copy
  142    `internal-client.conf-sample` to `/etc/swift/container-sync-client.conf`.
  143 #. Modify this file to include the `symlink` middleware in the pipeline in
  144    the same way as described above for the proxy server.
  145 #. Modify the container-sync section of all container server config files to
  146    point to this internal client config file using the
  147    ``internal_client_conf_path`` option. For example::
  148 
  149      internal_client_conf_path = /etc/swift/container-sync-client.conf
  150 
  151 .. note::
  152 
  153     These container sync configuration steps will be necessary for container
  154     sync probe tests to pass if the `symlink` middleware is included in the
  155     proxy pipeline of a test cluster.
  156 """
  157 
  158 import json
  159 import os
  160 from cgi import parse_header
  161 from six.moves.urllib.parse import unquote
  162 
  163 from swift.common.utils import get_logger, register_swift_info, split_path, \
  164     MD5_OF_EMPTY_STRING, closing_if_possible
  165 from swift.common.constraints import check_account_format
  166 from swift.common.wsgi import WSGIContext, make_subrequest
  167 from swift.common.request_helpers import get_sys_meta_prefix, \
  168     check_path_header
  169 from swift.common.swob import Request, HTTPBadRequest, HTTPTemporaryRedirect, \
  170     HTTPException, HTTPConflict, HTTPPreconditionFailed
  171 from swift.common.http import is_success
  172 from swift.common.exceptions import LinkIterError
  173 from swift.common.header_key_dict import HeaderKeyDict
  174 
  175 DEFAULT_SYMLOOP_MAX = 2
  176 # Header values for symlink target path strings will be quoted values.
  177 TGT_OBJ_SYMLINK_HDR = 'x-symlink-target'
  178 TGT_ACCT_SYMLINK_HDR = 'x-symlink-target-account'
  179 TGT_OBJ_SYSMETA_SYMLINK_HDR = get_sys_meta_prefix('object') + 'symlink-target'
  180 TGT_ACCT_SYSMETA_SYMLINK_HDR = \
  181     get_sys_meta_prefix('object') + 'symlink-target-account'
  182 
  183 
  184 def _check_symlink_header(req):
  185     """
  186     Validate that the value from x-symlink-target header is
  187     well formatted. We assume the caller ensures that
  188     x-symlink-target header is present in req.headers.
  189 
  190     :param req: HTTP request object
  191     :raise: HTTPPreconditionFailed if x-symlink-target value
  192             is not well formatted.
  193     :raise: HTTPBadRequest if the x-symlink-target value points to the request
  194             path.
  195     """
  196     # N.B. check_path_header doesn't assert the leading slash and
  197     # copy middleware may accept the format. In the symlink, API
  198     # says apparently to use "container/object" format so add the
  199     # validation first, here.
  200     if unquote(req.headers[TGT_OBJ_SYMLINK_HDR]).startswith('/'):
  201         raise HTTPPreconditionFailed(
  202             body='X-Symlink-Target header must be of the '
  203                  'form <container name>/<object name>',
  204             request=req, content_type='text/plain')
  205 
  206     # check container and object format
  207     container, obj = check_path_header(
  208         req, TGT_OBJ_SYMLINK_HDR, 2,
  209         'X-Symlink-Target header must be of the '
  210         'form <container name>/<object name>')
  211 
  212     # Check account format if it exists
  213     account = check_account_format(
  214         req, unquote(req.headers[TGT_ACCT_SYMLINK_HDR])) \
  215         if TGT_ACCT_SYMLINK_HDR in req.headers else None
  216 
  217     # Extract request path
  218     _junk, req_acc, req_cont, req_obj = req.split_path(4, 4, True)
  219 
  220     if not account:
  221         account = req_acc
  222 
  223     # Check if symlink targets the symlink itself or not
  224     if (account, container, obj) == (req_acc, req_cont, req_obj):
  225         raise HTTPBadRequest(
  226             body='Symlink cannot target itself',
  227             request=req, content_type='text/plain')
  228 
  229 
  230 def symlink_usermeta_to_sysmeta(headers):
  231     """
  232     Helper function to translate from X-Symlink-Target and
  233     X-Symlink-Target-Account to X-Object-Sysmeta-Symlink-Target
  234     and X-Object-Sysmeta-Symlink-Target-Account.
  235 
  236     :param headers: request headers dict. Note that the headers dict
  237         will be updated directly.
  238     """
  239     # To preseve url-encoded value in the symlink header, use raw value
  240     if TGT_OBJ_SYMLINK_HDR in headers:
  241         headers[TGT_OBJ_SYSMETA_SYMLINK_HDR] = headers.pop(
  242             TGT_OBJ_SYMLINK_HDR)
  243 
  244     if TGT_ACCT_SYMLINK_HDR in headers:
  245         headers[TGT_ACCT_SYSMETA_SYMLINK_HDR] = headers.pop(
  246             TGT_ACCT_SYMLINK_HDR)
  247 
  248 
  249 def symlink_sysmeta_to_usermeta(headers):
  250     """
  251     Helper function to translate from X-Object-Sysmeta-Symlink-Target and
  252     X-Object-Sysmeta-Symlink-Target-Account to X-Symlink-Target and
  253     X-Sysmeta-Symlink-Target-Account
  254 
  255     :param headers: request headers dict. Note that the headers dict
  256         will be updated directly.
  257     """
  258     if TGT_OBJ_SYSMETA_SYMLINK_HDR in headers:
  259         headers[TGT_OBJ_SYMLINK_HDR] = headers.pop(
  260             TGT_OBJ_SYSMETA_SYMLINK_HDR)
  261 
  262     if TGT_ACCT_SYSMETA_SYMLINK_HDR in headers:
  263         headers[TGT_ACCT_SYMLINK_HDR] = headers.pop(
  264             TGT_ACCT_SYSMETA_SYMLINK_HDR)
  265 
  266 
  267 class SymlinkContainerContext(WSGIContext):
  268     def __init__(self, wsgi_app, logger):
  269         super(SymlinkContainerContext, self).__init__(wsgi_app)
  270         self.logger = logger
  271 
  272     def handle_container(self, req, start_response):
  273         """
  274         Handle container requests.
  275 
  276         :param req: a :class:`~swift.common.swob.Request`
  277         :param start_response: start_response function
  278 
  279         :return: Response Iterator after start_response called.
  280         """
  281         app_resp = self._app_call(req.environ)
  282 
  283         if req.method == 'GET' and is_success(self._get_status_int()):
  284             app_resp = self._process_json_resp(app_resp, req)
  285 
  286         start_response(self._response_status, self._response_headers,
  287                        self._response_exc_info)
  288 
  289         return app_resp
  290 
  291     def _process_json_resp(self, resp_iter, req):
  292         """
  293         Iterate through json body looking for symlinks and modify its content
  294         :return: modified json body
  295         """
  296         with closing_if_possible(resp_iter):
  297             resp_body = b''.join(resp_iter)
  298         body_json = json.loads(resp_body)
  299         swift_version, account, _junk = split_path(req.path, 2, 3, True)
  300         new_body = json.dumps(
  301             [self._extract_symlink_path_json(obj_dict, swift_version, account)
  302              for obj_dict in body_json]).encode('ascii')
  303         self.update_content_length(len(new_body))
  304         return [new_body]
  305 
  306     def _extract_symlink_path_json(self, obj_dict, swift_version, account):
  307         """
  308         Extract the symlink path from the hash value
  309         :return: object dictionary with additional key:value pair if object
  310                  is a symlink. The new key is symlink_path.
  311         """
  312         if 'hash' in obj_dict:
  313             hash_value, meta = parse_header(obj_dict['hash'])
  314             obj_dict['hash'] = hash_value
  315             target = None
  316             for key in meta:
  317                 if key == 'symlink_target':
  318                     target = meta[key]
  319                 elif key == 'symlink_target_account':
  320                     account = meta[key]
  321                 else:
  322                     # make sure to add all other (key, values) back in place
  323                     obj_dict['hash'] += '; %s=%s' % (key, meta[key])
  324             else:
  325                 if target:
  326                     obj_dict['symlink_path'] = os.path.join(
  327                         '/', swift_version, account, target)
  328 
  329         return obj_dict
  330 
  331 
  332 class SymlinkObjectContext(WSGIContext):
  333 
  334     def __init__(self, wsgi_app, logger, symloop_max):
  335         super(SymlinkObjectContext, self).__init__(wsgi_app)
  336         self.symloop_max = symloop_max
  337         self.logger = logger
  338         # N.B. _loop_count and _last_target_path are used to keep
  339         # the statement in the _recursive_get. Hence they should not be touched
  340         # from other resources.
  341         self._loop_count = 0
  342         self._last_target_path = None
  343 
  344     def handle_get_head_symlink(self, req):
  345         """
  346         Handle get/head request when client sent parameter ?symlink=get
  347 
  348         :param req: HTTP GET or HEAD object request with param ?symlink=get
  349         :returns: Response Iterator
  350         """
  351         resp = self._app_call(req.environ)
  352         response_header_dict = HeaderKeyDict(self._response_headers)
  353         symlink_sysmeta_to_usermeta(response_header_dict)
  354         self._response_headers = response_header_dict.items()
  355         return resp
  356 
  357     def handle_get_head(self, req):
  358         """
  359         Handle get/head request and in case the response is a symlink,
  360         redirect request to target object.
  361 
  362         :param req: HTTP GET or HEAD object request
  363         :returns: Response Iterator
  364         """
  365         try:
  366             return self._recursive_get_head(req)
  367         except LinkIterError:
  368             errmsg = 'Too many levels of symbolic links, ' \
  369                      'maximum allowed is %d' % self.symloop_max
  370             raise HTTPConflict(
  371                 body=errmsg, request=req, content_type='text/plain')
  372 
  373     def _recursive_get_head(self, req):
  374         resp = self._app_call(req.environ)
  375 
  376         def build_traversal_req(symlink_target):
  377             """
  378             :returns: new request for target path if it's symlink otherwise
  379                       None
  380             """
  381             version, account, _junk = split_path(req.path, 2, 3, True)
  382             account = self._response_header_value(
  383                 TGT_ACCT_SYSMETA_SYMLINK_HDR) or account
  384             target_path = os.path.join(
  385                 '/', version, account,
  386                 symlink_target.lstrip('/'))
  387             self._last_target_path = target_path
  388             new_req = make_subrequest(
  389                 req.environ, path=target_path, method=req.method,
  390                 headers=req.headers, swift_source='SYM')
  391             new_req.headers.pop('X-Backend-Storage-Policy-Index', None)
  392             return new_req
  393 
  394         symlink_target = self._response_header_value(
  395             TGT_OBJ_SYSMETA_SYMLINK_HDR)
  396         if symlink_target:
  397             if self._loop_count >= self.symloop_max:
  398                 raise LinkIterError()
  399             # format: /<account name>/<container name>/<object name>
  400             new_req = build_traversal_req(symlink_target)
  401             self._loop_count += 1
  402             return self._recursive_get_head(new_req)
  403         else:
  404             if self._last_target_path:
  405                 # Content-Location will be applied only when one or more
  406                 # symlink recursion occurred.
  407                 # In this case, Content-Location is applied to show which
  408                 # object path caused the error response.
  409                 # To preserve '%2F'(= quote('/')) in X-Symlink-Target
  410                 # header value as it is, Content-Location value comes from
  411                 # TGT_OBJ_SYMLINK_HDR, not req.path
  412                 self._response_headers.extend(
  413                     [('Content-Location', self._last_target_path)])
  414 
  415             return resp
  416 
  417     def handle_put(self, req):
  418         """
  419         Handle put request when it contains X-Symlink-Target header.
  420 
  421         Symlink headers are validated and moved to sysmeta namespace.
  422         :param req: HTTP PUT object request
  423         :returns: Response Iterator
  424         """
  425         if req.content_length != 0:
  426             raise HTTPBadRequest(
  427                 body='Symlink requests require a zero byte body',
  428                 request=req,
  429                 content_type='text/plain')
  430 
  431         _check_symlink_header(req)
  432         symlink_usermeta_to_sysmeta(req.headers)
  433         # Store info in container update that this object is a symlink.
  434         # We have a design decision to use etag space to store symlink info for
  435         # object listing because it's immutable unless the object is
  436         # overwritten. This may impact the downgrade scenario that the symlink
  437         # info can appear as the suffix in the hash value of object
  438         # listing result for clients.
  439         # To create override etag easily, we have a constraint that the symlink
  440         # must be 0 byte so we can add etag of the empty string + symlink info
  441         # here, simply. Note that this override etag may be encrypted in the
  442         # container db by encryption middleware.
  443         etag_override = [
  444             MD5_OF_EMPTY_STRING,
  445             'symlink_target=%s' % req.headers[TGT_OBJ_SYSMETA_SYMLINK_HDR]
  446         ]
  447         if TGT_ACCT_SYSMETA_SYMLINK_HDR in req.headers:
  448             etag_override.append(
  449                 'symlink_target_account=%s' %
  450                 req.headers[TGT_OBJ_SYSMETA_SYMLINK_HDR])
  451         req.headers['X-Object-Sysmeta-Container-Update-Override-Etag'] = \
  452             '; '.join(etag_override)
  453 
  454         return self._app_call(req.environ)
  455 
  456     def handle_post(self, req):
  457         """
  458         Handle post request. If POSTing to a symlink, a HTTPTemporaryRedirect
  459         error message is returned to client.
  460 
  461         Clients that POST to symlinks should understand that the POST is not
  462         redirected to the target object like in a HEAD/GET request. POSTs to a
  463         symlink will be handled just like a normal object by the object server.
  464         It cannot reject it because it may not have symlink state when the POST
  465         lands.  The object server has no knowledge of what is a symlink object
  466         is. On the other hand, on POST requests, the object server returns all
  467         sysmeta of the object. This method uses that sysmeta to determine if
  468         the stored object is a symlink or not.
  469 
  470         :param req: HTTP POST object request
  471         :raises: HTTPTemporaryRedirect if POSTing to a symlink.
  472         :returns: Response Iterator
  473         """
  474         if TGT_OBJ_SYMLINK_HDR in req.headers:
  475             raise HTTPBadRequest(
  476                 body='A PUT request is required to set a symlink target',
  477                 request=req,
  478                 content_type='text/plain')
  479 
  480         resp = self._app_call(req.environ)
  481         if not is_success(self._get_status_int()):
  482             return resp
  483 
  484         tgt_co = self._response_header_value(TGT_OBJ_SYSMETA_SYMLINK_HDR)
  485         if tgt_co:
  486             version, account, _junk = req.split_path(2, 3, True)
  487             target_acc = self._response_header_value(
  488                 TGT_ACCT_SYSMETA_SYMLINK_HDR) or account
  489             location_hdr = os.path.join(
  490                 '/', version, target_acc, tgt_co)
  491             req.environ['swift.leave_relative_location'] = True
  492             errmsg = 'The requested POST was applied to a symlink. POST ' +\
  493                      'directly to the target to apply requested metadata.'
  494             raise HTTPTemporaryRedirect(
  495                 body=errmsg, headers={'location': location_hdr})
  496         else:
  497             return resp
  498 
  499     def handle_object(self, req, start_response):
  500         """
  501         Handle object requests.
  502 
  503         :param req: a :class:`~swift.common.swob.Request`
  504         :param start_response: start_response function
  505         :returns: Response Iterator after start_response has been called
  506         """
  507         if req.method in ('GET', 'HEAD'):
  508             # if GET request came from versioned writes, then it should get
  509             # the symlink only, not the referenced target
  510             if req.params.get('symlink') == 'get' or \
  511                     req.environ.get('swift.source') == 'VW':
  512                 resp = self.handle_get_head_symlink(req)
  513             else:
  514                 resp = self.handle_get_head(req)
  515         elif req.method == 'PUT' and (TGT_OBJ_SYMLINK_HDR in req.headers):
  516             resp = self.handle_put(req)
  517         elif req.method == 'POST':
  518             resp = self.handle_post(req)
  519         else:
  520             # DELETE and OPTIONS reqs for a symlink and
  521             # PUT reqs without X-Symlink-Target behave like any other object
  522             resp = self._app_call(req.environ)
  523 
  524         start_response(self._response_status, self._response_headers,
  525                        self._response_exc_info)
  526 
  527         return resp
  528 
  529 
  530 class SymlinkMiddleware(object):
  531     """
  532     Middleware that implements symlinks.
  533 
  534     Symlinks are objects stored in Swift that contain a reference to another
  535     object (i.e., the target object). An important use case is to use a path in
  536     one container to access an object in a different container, with a
  537     different policy. This allows policy cost/performance trade-offs to be made
  538     on individual objects.
  539     """
  540 
  541     def __init__(self, app, conf, symloop_max):
  542         self.app = app
  543         self.conf = conf
  544         self.logger = get_logger(self.conf, log_route='symlink')
  545         self.symloop_max = symloop_max
  546 
  547     def __call__(self, env, start_response):
  548         req = Request(env)
  549         try:
  550             version, acc, cont, obj = req.split_path(3, 4, True)
  551         except ValueError:
  552             return self.app(env, start_response)
  553 
  554         try:
  555             if obj:
  556                 # object context
  557                 context = SymlinkObjectContext(self.app, self.logger,
  558                                                self.symloop_max)
  559                 return context.handle_object(req, start_response)
  560             else:
  561                 # container context
  562                 context = SymlinkContainerContext(self.app, self.logger)
  563                 return context.handle_container(req, start_response)
  564         except HTTPException as err_resp:
  565             return err_resp(env, start_response)
  566 
  567 
  568 def filter_factory(global_conf, **local_conf):
  569     conf = global_conf.copy()
  570     conf.update(local_conf)
  571 
  572     symloop_max = int(conf.get('symloop_max', DEFAULT_SYMLOOP_MAX))
  573     if symloop_max < 1:
  574         symloop_max = int(DEFAULT_SYMLOOP_MAX)
  575     register_swift_info('symlink', symloop_max=symloop_max)
  576 
  577     def symlink_mw(app):
  578         return SymlinkMiddleware(app, conf, symloop_max)
  579     return symlink_mw