Esempio n. 1
0
    def _construct_path(self, obj, base_dir=None, dir_only=None, extra_dir=None, extra_dir_at_root=False, alt_name=None, obj_dir=False, **kwargs):
        # extra_dir should never be constructed from provided data but just
        # make sure there are no shenannigans afoot
        if extra_dir and extra_dir != os.path.normpath(extra_dir):
            log.warning('extra_dir is not normalized: %s', extra_dir)
            raise ObjectInvalid("The requested object is invalid")
        # ensure that any parent directory references in alt_name would not
        # result in a path not contained in the directory path constructed here
        if alt_name:
            if not safe_relpath(alt_name):
                log.warning('alt_name would locate path outside dir: %s', alt_name)
                raise ObjectInvalid("The requested object is invalid")
            # alt_name can contain parent directory references, but S3 will not
            # follow them, so if they are valid we normalize them out
            alt_name = os.path.normpath(alt_name)
        rel_path = os.path.join(*directory_hash_id(obj.id))
        if extra_dir is not None:
            if extra_dir_at_root:
                rel_path = os.path.join(extra_dir, rel_path)
            else:
                rel_path = os.path.join(rel_path, extra_dir)

        # for JOB_WORK directory
        if obj_dir:
            rel_path = os.path.join(rel_path, str(obj.id))
        if base_dir:
            base = self.extra_dirs.get(base_dir)
            return os.path.join(base, rel_path)

        # S3 folders are marked by having trailing '/' so add it now
        rel_path = '%s/' % rel_path

        if not dir_only:
            rel_path = os.path.join(rel_path, alt_name if alt_name else "dataset_%s.dat" % obj.id)
        return rel_path
Esempio n. 2
0
    def __get_rods_path(self, obj, base_dir=None, dir_only=False, extra_dir=None, extra_dir_at_root=False, alt_name=None, strip_dat=True, **kwargs):
        # extra_dir should never be constructed from provided data but just
        # make sure there are no shenannigans afoot
        if extra_dir and extra_dir != os.path.normpath(extra_dir):
            log.warning('extra_dir is not normalized: %s', extra_dir)
            raise ObjectInvalid("The requested object is invalid")
        # ensure that any parent directory references in alt_name would not
        # result in a path not contained in the directory path constructed here
        if alt_name:
            if not safe_relpath(alt_name):
                log.warning('alt_name would locate path outside dir: %s', alt_name)
                raise ObjectInvalid("The requested object is invalid")
            # alt_name can contain parent directory references, but iRODS will
            # not follow them, so if they are valid we normalize them out
            alt_name = os.path.normpath(alt_name)
        path = ""
        if extra_dir is not None:
            path = extra_dir

        # extra_dir_at_root is ignored - since the iRODS plugin does not use
        # the directory hash, there is only one level of subdirectory.

        if not dir_only:
            # the .dat extension is stripped when stored in iRODS
            # TODO: is the strip_dat kwarg the best way to implement this?
            if strip_dat and alt_name and alt_name.endswith('.dat'):
                alt_name = os.path.splitext(alt_name)[0]
            default_name = 'dataset_%s' % obj.id
            if not strip_dat:
                default_name += '.dat'
            path = path_join(path, alt_name if alt_name else default_name)

        path = path_join(self.root_collection_path, path)
        return path
Esempio n. 3
0
    def __get_rods_path( self, obj, base_dir=None, dir_only=False, extra_dir=None, extra_dir_at_root=False, alt_name=None, strip_dat=True, **kwargs ):
        # extra_dir should never be constructed from provided data but just
        # make sure there are no shenannigans afoot
        if extra_dir and extra_dir != os.path.normpath(extra_dir):
            log.warning('extra_dir is not normalized: %s', extra_dir)
            raise ObjectInvalid("The requested object is invalid")
        # ensure that any parent directory references in alt_name would not
        # result in a path not contained in the directory path constructed here
        if alt_name:
            if not safe_relpath(alt_name):
                log.warning('alt_name would locate path outside dir: %s', alt_name)
                raise ObjectInvalid("The requested object is invalid")
            # alt_name can contain parent directory references, but iRODS will
            # not follow them, so if they are valid we normalize them out
            alt_name = os.path.normpath(alt_name)
        path = ""
        if extra_dir is not None:
            path = extra_dir

        # extra_dir_at_root is ignored - since the iRODS plugin does not use
        # the directory hash, there is only one level of subdirectory.

        if not dir_only:
            # the .dat extension is stripped when stored in iRODS
            # TODO: is the strip_dat kwarg the best way to implement this?
            if strip_dat and alt_name and alt_name.endswith( '.dat' ):
                alt_name = os.path.splitext( alt_name )[0]
            default_name = 'dataset_%s' % obj.id
            if not strip_dat:
                default_name += '.dat'
            path = path_join( path, alt_name if alt_name else default_name )

        path = path_join( self.root_collection_path, path )
        return path
Esempio n. 4
0
    def _construct_path(self, obj, base_dir=None, dir_only=None, extra_dir=None, extra_dir_at_root=False, alt_name=None, obj_dir=False, **kwargs):
        # extra_dir should never be constructed from provided data but just
        # make sure there are no shenannigans afoot
        if extra_dir and extra_dir != os.path.normpath(extra_dir):
            log.warning('extra_dir is not normalized: %s', extra_dir)
            raise ObjectInvalid("The requested object is invalid")
        # ensure that any parent directory references in alt_name would not
        # result in a path not contained in the directory path constructed here
        if alt_name:
            if not safe_relpath(alt_name):
                log.warning('alt_name would locate path outside dir: %s', alt_name)
                raise ObjectInvalid("The requested object is invalid")
            # alt_name can contain parent directory references, but S3 will not
            # follow them, so if they are valid we normalize them out
            alt_name = os.path.normpath(alt_name)
        rel_path = os.path.join(*directory_hash_id(obj.id))
        if extra_dir is not None:
            if extra_dir_at_root:
                rel_path = os.path.join(extra_dir, rel_path)
            else:
                rel_path = os.path.join(rel_path, extra_dir)

        # for JOB_WORK directory
        if obj_dir:
            rel_path = os.path.join(rel_path, str(obj.id))
        if base_dir:
            base = self.extra_dirs.get(base_dir)
            return os.path.join(base, rel_path)

        # S3 folders are marked by having trailing '/' so add it now
        rel_path = '%s/' % rel_path

        if not dir_only:
            rel_path = os.path.join(rel_path, alt_name if alt_name else "dataset_%s.dat" % obj.id)
        return rel_path
Esempio n. 5
0
def check_archive(repository, archive):
    valid = []
    invalid = []
    errors = []
    undesirable_files = []
    undesirable_dirs = []
    for member in archive.getmembers():
        # Allow regular files and directories only
        if not (member.isdir() or member.isfile() or member.islnk()):
            errors.append(
                "Uploaded archives can only include regular directories and files (no symbolic links, devices, etc)."
            )
            invalid.append(member)
            continue
        if not safe_relpath(member.name):
            errors.append(
                "Uploaded archives cannot contain files that would extract outside of the archive."
            )
            invalid.append(member)
            continue
        if os.path.basename(member.name) in UNDESIRABLE_FILES:
            undesirable_files.append(member)
            continue
        head = tail = member.name
        try:
            while tail:
                head, tail = os.path.split(head)
                if tail in UNDESIRABLE_DIRS:
                    undesirable_dirs.append(member)
                    assert False
        except AssertionError:
            continue
        if repository.type == rt_util.REPOSITORY_SUITE_DEFINITION and member.name != rt_util.REPOSITORY_DEPENDENCY_DEFINITION_FILENAME:
            errors.append(
                'Repositories of type <b>Repository suite definition</b> can contain only a single file named <b>repository_dependencies.xml</b>.'
            )
            invalid.append(member)
            continue
        if repository.type == rt_util.TOOL_DEPENDENCY_DEFINITION and member.name != rt_util.TOOL_DEPENDENCY_DEFINITION_FILENAME:
            errors.append(
                'Repositories of type <b>Tool dependency definition</b> can contain only a single file named <b>tool_dependencies.xml</b>.'
            )
            invalid.append(member)
            continue
        valid.append(member)
    ArchiveCheckResults = namedtuple('ArchiveCheckResults', [
        'valid', 'invalid', 'undesirable_files', 'undesirable_dirs', 'errors'
    ])
    return ArchiveCheckResults(valid, invalid, undesirable_files,
                               undesirable_dirs, errors)
Esempio n. 6
0
def check_archive(repository, archive):
    valid = []
    invalid = []
    errors = []
    undesirable_files = []
    undesirable_dirs = []
    for member in archive.getmembers():
        # Allow regular files and directories only
        if not (member.isdir() or member.isfile() or member.islnk()):
            errors.append("Uploaded archives can only include regular directories and files (no symbolic links, devices, etc).")
            invalid.append(member)
            continue
        if not safe_relpath(member.name):
            errors.append("Uploaded archives cannot contain files that would extract outside of the archive.")
            invalid.append(member)
            continue
        if os.path.basename(member.name) in UNDESIRABLE_FILES:
            undesirable_files.append(member)
            continue
        head = tail = member.name
        try:
            while tail:
                head, tail = os.path.split(head)
                if tail in UNDESIRABLE_DIRS:
                    undesirable_dirs.append(member)
                    assert False
        except AssertionError:
            continue
        if repository.type == rt_util.REPOSITORY_SUITE_DEFINITION and member.name != rt_util.REPOSITORY_DEPENDENCY_DEFINITION_FILENAME:
            errors.append('Repositories of type <b>Repository suite definition</b> can contain only a single file named <b>repository_dependencies.xml</b>.')
            invalid.append(member)
            continue
        if repository.type == rt_util.TOOL_DEPENDENCY_DEFINITION and member.name != rt_util.TOOL_DEPENDENCY_DEFINITION_FILENAME:
            errors.append('Repositories of type <b>Tool dependency definition</b> can contain only a single file named <b>tool_dependencies.xml</b>.')
            invalid.append(member)
            continue
        valid.append(member)
    ArchiveCheckResults = namedtuple('ArchiveCheckResults', ['valid', 'invalid', 'undesirable_files', 'undesirable_dirs', 'errors'])
    return ArchiveCheckResults(valid, invalid, undesirable_files, undesirable_dirs, errors)
Esempio n. 7
0
    def _construct_path(self, obj, old_style=False, base_dir=None, dir_only=False, extra_dir=None, extra_dir_at_root=False, alt_name=None, obj_dir=False, **kwargs):
        """
        Construct the absolute path for accessing the object identified by `obj.id`.

        :type base_dir: string
        :param base_dir: A key in self.extra_dirs corresponding to the base
                         directory in which this object should be created, or
                         None to specify the default directory.

        :type dir_only: boolean
        :param dir_only: If True, check only the path where the file
                         identified by `obj` should be located, not the
                         dataset itself. This option applies to `extra_dir`
                         argument as well.

        :type extra_dir: string
        :param extra_dir: Append the value of this parameter to the expected
            path used to access the object identified by `obj` (e.g.,
            /files/000/<extra_dir>/dataset_10.dat).

        :type alt_name: string
        :param alt_name: Use this name as the alternative name for the returned
                         dataset rather than the default.

        :type old_style: boolean
        param old_style: This option is used for backward compatibility. If
            `True` then the composed directory structure does not include a
            hash id (e.g., /files/dataset_10.dat (old) vs.
            /files/000/dataset_10.dat (new))
        """
        base = os.path.abspath(self.extra_dirs.get(base_dir, self.file_path))
        # extra_dir should never be constructed from provided data but just
        # make sure there are no shenannigans afoot
        if extra_dir and extra_dir != os.path.normpath(extra_dir):
            log.warning('extra_dir is not normalized: %s', extra_dir)
            raise ObjectInvalid("The requested object is invalid")
        # ensure that any parent directory references in alt_name would not
        # result in a path not contained in the directory path constructed here
        if alt_name and not safe_relpath(alt_name):
            log.warning('alt_name would locate path outside dir: %s', alt_name)
            raise ObjectInvalid("The requested object is invalid")
        if old_style:
            if extra_dir is not None:
                path = os.path.join(base, extra_dir)
            else:
                path = base
        else:
            # Construct hashed path
            rel_path = os.path.join(*directory_hash_id(obj.id))
            # Create a subdirectory for the object ID
            if obj_dir:
                rel_path = os.path.join(rel_path, str(obj.id))
            # Optionally append extra_dir
            if extra_dir is not None:
                if extra_dir_at_root:
                    rel_path = os.path.join(extra_dir, rel_path)
                else:
                    rel_path = os.path.join(rel_path, extra_dir)
            path = os.path.join(base, rel_path)
        if not dir_only:
            path = os.path.join(path, alt_name if alt_name else "dataset_%s.dat" % obj.id)
        return os.path.abspath(path)
Esempio n. 8
0
 def __call__(self, environ, start_response):
     if 'PATH_INFO' in environ:
         path_info = environ['PATH_INFO'].lstrip('/')
         if path_info == 'repository/reset_all_metadata':
             self.setting_repository_metadata = True
     cmd = self.__get_hg_command(**environ)
     # The 'getbundle' command indicates that a mercurial client is getting a bundle of one or more changesets, indicating
     # a clone or a pull.  However, we do not want to increment the times_downloaded count if we're only setting repository
     # metadata.
     if cmd == 'getbundle' and not self.setting_repository_metadata:
         hg_args = urlparse.parse_qs(environ['HTTP_X_HGARG_1'])
         # The 'common' parameter indicates the full sha-1 hash of the changeset the client currently has checked out. If
         # this is 0000000000000000000000000000000000000000, then the client is performing a fresh checkout. If it has any
         # other value, the client is getting updates to an existing checkout.
         if 'common' in hg_args and hg_args['common'][
                 -1] == '0000000000000000000000000000000000000000':
             # Increment the value of the times_downloaded column in the repository table for the cloned repository.
             if 'PATH_INFO' in environ:
                 # Instantiate a database connection
                 engine = sqlalchemy.create_engine(self.db_url)
                 connection = engine.connect()
                 path_info = environ['PATH_INFO'].lstrip('/')
                 user_id, repository_name = self.__get_user_id_repository_name_from_path_info(
                     connection, path_info)
                 sql_cmd = "SELECT times_downloaded FROM repository WHERE user_id = %d AND name = '%s'" % \
                     ( user_id, repository_name.lower() )
                 result_set = connection.execute(sql_cmd)
                 for row in result_set:
                     # Should only be 1 row...
                     times_downloaded = row['times_downloaded']
                 times_downloaded += 1
                 sql_cmd = "UPDATE repository SET times_downloaded = %d WHERE user_id = %d AND name = '%s'" % \
                     ( times_downloaded, user_id, repository_name.lower() )
                 connection.execute(sql_cmd)
                 connection.close()
     elif cmd in ['unbundle', 'pushkey']:
         # This is an hg push from the command line.  When doing this, the following commands, in order,
         # will be retrieved from environ (see the docs at http://mercurial.selenic.com/wiki/WireProtocol):
         # # If mercurial version >= '2.2.3': capabilities -> batch -> branchmap -> unbundle -> listkeys -> pushkey -> listkeys
         #
         # The mercurial API unbundle() ( i.e., hg push ) and pushkey() methods ultimately require authorization.
         # We'll force password entry every time a change set is pushed.
         #
         # When a user executes hg commit, it is not guaranteed to succeed.  Mercurial records your name
         # and address with each change that you commit, so that you and others will later be able to
         # tell who made each change. Mercurial tries to automatically figure out a sensible username
         # to commit the change with. It will attempt each of the following methods, in order:
         #
         # 1) If you specify a -u option to the hg commit command on the command line, followed by a username,
         # this is always given the highest precedence.
         # 2) If you have set the HGUSER environment variable, this is checked next.
         # 3) If you create a file in your home directory called .hgrc with a username entry, that
         # will be used next.
         # 4) If you have set the EMAIL environment variable, this will be used next.
         # 5) Mercurial will query your system to find out your local user name and host name, and construct
         # a username from these components. Since this often results in a username that is not very useful,
         # it will print a warning if it has to do this.
         #
         # If all of these mechanisms fail, Mercurial will fail, printing an error message. In this case, it
         # will not let you commit until you set up a username.
         result = self.authentication(environ)
         if not isinstance(
                 result,
                 str) and cmd == 'unbundle' and 'wsgi.input' in environ:
             bundle_data_stream = environ['wsgi.input']
             # Convert the incoming mercurial bundle into a json object and persit it to a temporary file for inspection.
             fh = tempfile.NamedTemporaryFile('wb', prefix="tmp-hg-bundle")
             tmp_filename = fh.name
             fh.close()
             fh = open(tmp_filename, 'wb')
             while 1:
                 chunk = bundle_data_stream.read(CHUNK_SIZE)
                 if not chunk:
                     break
                 fh.write(chunk)
             fh.close()
             fh = open(tmp_filename, 'rb')
             try:
                 changeset_groups = json.loads(hg_util.bundle_to_json(fh))
             except AttributeError:
                 msg = 'Your version of Mercurial is not supported. Please use a version < 3.5'
                 return self.__display_exception_remotely(
                     start_response, msg)
             fh.close()
             try:
                 os.unlink(tmp_filename)
             except:
                 pass
             if changeset_groups:
                 # Check the repository type to make sure inappropriate files are not being pushed.
                 if 'PATH_INFO' in environ:
                     # Ensure there are no symlinks with targets outside the repo
                     for entry in changeset_groups:
                         if len(entry) == 2:
                             filename, change_list = entry
                             if not isinstance(change_list, list):
                                 change_list = [change_list]
                             for change in change_list:
                                 for patch in change['data']:
                                     target = patch['block'].strip()
                                     if ((patch['end'] - patch['start']
                                          == 0)
                                             and not safe_relpath(target)):
                                         msg = "Changes include a symlink outside of the repository: %s -> %s" % (
                                             filename, target)
                                         log.warning(msg)
                                         return self.__display_exception_remotely(
                                             start_response, msg)
                     # Instantiate a database connection
                     engine = sqlalchemy.create_engine(self.db_url)
                     connection = engine.connect()
                     path_info = environ['PATH_INFO'].lstrip('/')
                     user_id, repository_name = self.__get_user_id_repository_name_from_path_info(
                         connection, path_info)
                     sql_cmd = "SELECT type FROM repository WHERE user_id = %d AND name = '%s'" % (
                         user_id, repository_name.lower())
                     result_set = connection.execute(sql_cmd)
                     for row in result_set:
                         # Should only be 1 row...
                         repository_type = str(row['type'])
                     if repository_type == rt_util.REPOSITORY_SUITE_DEFINITION:
                         # Handle repositories of type repository_suite_definition, which can only contain a single
                         # file named repository_dependencies.xml.
                         for entry in changeset_groups:
                             if len(entry) == 2:
                                 # We possibly found an altered file entry.
                                 filename, change_list = entry
                                 if filename and isinstance(filename, str):
                                     if filename == rt_util.REPOSITORY_DEPENDENCY_DEFINITION_FILENAME:
                                         # Make sure the any complex repository dependency definitions contain valid <repository> tags.
                                         is_valid, error_msg = self.repository_tags_are_valid(
                                             filename, change_list)
                                         if not is_valid:
                                             log.debug(error_msg)
                                             return self.__display_exception_remotely(
                                                 start_response, error_msg)
                                     else:
                                         msg = "Only a single file named repository_dependencies.xml can be pushed to a repository "
                                         msg += "of type 'Repository suite definition'."
                                         log.debug(msg)
                                         return self.__display_exception_remotely(
                                             start_response, msg)
                     elif repository_type == rt_util.TOOL_DEPENDENCY_DEFINITION:
                         # Handle repositories of type tool_dependency_definition, which can only contain a single
                         # file named tool_dependencies.xml.
                         for entry in changeset_groups:
                             if len(entry) == 2:
                                 # We possibly found an altered file entry.
                                 filename, change_list = entry
                                 if filename and isinstance(filename, str):
                                     if filename == rt_util.TOOL_DEPENDENCY_DEFINITION_FILENAME:
                                         # Make sure the any complex repository dependency definitions contain valid <repository> tags.
                                         is_valid, error_msg = self.repository_tags_are_valid(
                                             filename, change_list)
                                         if not is_valid:
                                             log.debug(error_msg)
                                             return self.__display_exception_remotely(
                                                 start_response, error_msg)
                                     else:
                                         msg = "Only a single file named tool_dependencies.xml can be pushed to a repository "
                                         msg += "of type 'Tool dependency definition'."
                                         log.debug(msg)
                                         return self.__display_exception_remotely(
                                             start_response, msg)
                     else:
                         # If the changeset includes changes to dependency definition files, make sure tag sets
                         # are not missing "toolshed" or "changeset_revision" attributes since automatically populating
                         # them is not supported when pushing from the command line.  These attributes are automatically
                         # populated only when using the tool shed upload utility.
                         for entry in changeset_groups:
                             if len(entry) == 2:
                                 # We possibly found an altered file entry.
                                 filename, change_list = entry
                                 if filename and isinstance(filename, str):
                                     if filename in [
                                             rt_util.
                                             REPOSITORY_DEPENDENCY_DEFINITION_FILENAME,
                                             rt_util.
                                             TOOL_DEPENDENCY_DEFINITION_FILENAME
                                     ]:
                                         # We check both files since tool dependency definitions files can contain complex
                                         # repository dependency definitions.
                                         is_valid, error_msg = self.repository_tags_are_valid(
                                             filename, change_list)
                                         if not is_valid:
                                             log.debug(error_msg)
                                             return self.__display_exception_remotely(
                                                 start_response, error_msg)
         if isinstance(result, str):
             # Authentication was successful
             AUTH_TYPE.update(environ, 'basic')
             REMOTE_USER.update(environ, result)
         else:
             return result.wsgi_application(environ, start_response)
     return self.app(environ, start_response)
Esempio n. 9
0
 def __call__( self, environ, start_response ):
     if 'PATH_INFO' in environ:
         path_info = environ[ 'PATH_INFO' ].lstrip( '/' )
         if path_info == 'repository/reset_all_metadata':
             self.setting_repository_metadata = True
     cmd = self.__get_hg_command( **environ )
     # The 'getbundle' command indicates that a mercurial client is getting a bundle of one or more changesets, indicating
     # a clone or a pull.  However, we do not want to increment the times_downloaded count if we're only setting repository
     # metadata.
     if cmd == 'getbundle' and not self.setting_repository_metadata:
         hg_args = urlparse.parse_qs( environ[ 'HTTP_X_HGARG_1' ] )
         # The 'common' parameter indicates the full sha-1 hash of the changeset the client currently has checked out. If
         # this is 0000000000000000000000000000000000000000, then the client is performing a fresh checkout. If it has any
         # other value, the client is getting updates to an existing checkout.
         if 'common' in hg_args and hg_args[ 'common' ][-1] == '0000000000000000000000000000000000000000':
             # Increment the value of the times_downloaded column in the repository table for the cloned repository.
             if 'PATH_INFO' in environ:
                 # Instantiate a database connection
                 engine = sqlalchemy.create_engine( self.db_url )
                 connection = engine.connect()
                 path_info = environ[ 'PATH_INFO' ].lstrip( '/' )
                 user_id, repository_name = self.__get_user_id_repository_name_from_path_info( connection, path_info )
                 sql_cmd = "SELECT times_downloaded FROM repository WHERE user_id = %d AND name = '%s'" % \
                     ( user_id, repository_name.lower() )
                 result_set = connection.execute( sql_cmd )
                 for row in result_set:
                     # Should only be 1 row...
                     times_downloaded = row[ 'times_downloaded' ]
                 times_downloaded += 1
                 sql_cmd = "UPDATE repository SET times_downloaded = %d WHERE user_id = %d AND name = '%s'" % \
                     ( times_downloaded, user_id, repository_name.lower() )
                 connection.execute( sql_cmd )
                 connection.close()
     elif cmd in [ 'unbundle', 'pushkey' ]:
         if self.config.get('disable_push', True):
             msg = 'Pushing to Tool Shed is disabled. Please use Galaxy Planemo to upload your changes.'
             return self.__display_exception_remotely( start_response, msg )
         # This is an hg push from the command line.  When doing this, the following commands, in order,
         # will be retrieved from environ (see the docs at http://mercurial.selenic.com/wiki/WireProtocol):
         # # If mercurial version >= '2.2.3': capabilities -> batch -> branchmap -> unbundle -> listkeys -> pushkey -> listkeys
         #
         # The mercurial API unbundle() ( i.e., hg push ) and pushkey() methods ultimately require authorization.
         # We'll force password entry every time a change set is pushed.
         #
         # When a user executes hg commit, it is not guaranteed to succeed.  Mercurial records your name
         # and address with each change that you commit, so that you and others will later be able to
         # tell who made each change. Mercurial tries to automatically figure out a sensible username
         # to commit the change with. It will attempt each of the following methods, in order:
         #
         # 1) If you specify a -u option to the hg commit command on the command line, followed by a username,
         # this is always given the highest precedence.
         # 2) If you have set the HGUSER environment variable, this is checked next.
         # 3) If you create a file in your home directory called .hgrc with a username entry, that
         # will be used next.
         # 4) If you have set the EMAIL environment variable, this will be used next.
         # 5) Mercurial will query your system to find out your local user name and host name, and construct
         # a username from these components. Since this often results in a username that is not very useful,
         # it will print a warning if it has to do this.
         #
         # If all of these mechanisms fail, Mercurial will fail, printing an error message. In this case, it
         # will not let you commit until you set up a username.
         result = self.authentication( environ )
         if not isinstance( result, str ) and cmd == 'unbundle' and 'wsgi.input' in environ:
             bundle_data_stream = environ[ 'wsgi.input' ]
             # Convert the incoming mercurial bundle into a json object and persit it to a temporary file for inspection.
             fh = tempfile.NamedTemporaryFile( 'wb', prefix="tmp-hg-bundle"  )
             tmp_filename = fh.name
             fh.close()
             fh = open( tmp_filename, 'wb' )
             while 1:
                 chunk = bundle_data_stream.read( CHUNK_SIZE )
                 if not chunk:
                     break
                 fh.write( chunk )
             fh.close()
             fh = open( tmp_filename, 'rb' )
             try:
                 changeset_groups = json.loads( hg_util.bundle_to_json( fh ) )
             except AttributeError:
                 msg = 'Your version of Mercurial is not supported. Please use a version < 3.5'
                 return self.__display_exception_remotely( start_response, msg )
             fh.close()
             try:
                 os.unlink( tmp_filename )
             except:
                 pass
             if changeset_groups:
                 # Check the repository type to make sure inappropriate files are not being pushed.
                 if 'PATH_INFO' in environ:
                     # Ensure there are no symlinks with targets outside the repo
                     for entry in changeset_groups:
                         if len( entry ) == 2:
                             filename, change_list = entry
                             if not isinstance(change_list, list):
                                 change_list = [change_list]
                             for change in change_list:
                                 for patch in change['data']:
                                     target = patch['block'].strip()
                                     if ( ( patch['end'] - patch['start'] == 0 ) and not safe_relpath( target ) ):
                                         msg = "Changes include a symlink outside of the repository: %s -> %s" % ( filename, target )
                                         log.warning( msg )
                                         return self.__display_exception_remotely( start_response, msg )
                     # Instantiate a database connection
                     engine = sqlalchemy.create_engine( self.db_url )
                     connection = engine.connect()
                     path_info = environ[ 'PATH_INFO' ].lstrip( '/' )
                     user_id, repository_name = self.__get_user_id_repository_name_from_path_info( connection, path_info )
                     sql_cmd = "SELECT type FROM repository WHERE user_id = %d AND name = '%s'" % ( user_id, repository_name.lower() )
                     result_set = connection.execute( sql_cmd )
                     for row in result_set:
                         # Should only be 1 row...
                         repository_type = str( row[ 'type' ] )
                     if repository_type == rt_util.REPOSITORY_SUITE_DEFINITION:
                         # Handle repositories of type repository_suite_definition, which can only contain a single
                         # file named repository_dependencies.xml.
                         for entry in changeset_groups:
                             if len( entry ) == 2:
                                 # We possibly found an altered file entry.
                                 filename, change_list = entry
                                 if filename and isinstance( filename, str ):
                                     if filename == rt_util.REPOSITORY_DEPENDENCY_DEFINITION_FILENAME:
                                         # Make sure the any complex repository dependency definitions contain valid <repository> tags.
                                         is_valid, error_msg = self.repository_tags_are_valid( filename, change_list )
                                         if not is_valid:
                                             log.debug( error_msg )
                                             return self.__display_exception_remotely( start_response, error_msg )
                                     else:
                                         msg = "Only a single file named repository_dependencies.xml can be pushed to a repository "
                                         msg += "of type 'Repository suite definition'."
                                         log.debug( msg )
                                         return self.__display_exception_remotely( start_response, msg )
                     elif repository_type == rt_util.TOOL_DEPENDENCY_DEFINITION:
                         # Handle repositories of type tool_dependency_definition, which can only contain a single
                         # file named tool_dependencies.xml.
                         for entry in changeset_groups:
                             if len( entry ) == 2:
                                 # We possibly found an altered file entry.
                                 filename, change_list = entry
                                 if filename and isinstance( filename, str ):
                                     if filename == rt_util.TOOL_DEPENDENCY_DEFINITION_FILENAME:
                                         # Make sure the any complex repository dependency definitions contain valid <repository> tags.
                                         is_valid, error_msg = self.repository_tags_are_valid( filename, change_list )
                                         if not is_valid:
                                             log.debug( error_msg )
                                             return self.__display_exception_remotely( start_response, error_msg )
                                     else:
                                         msg = "Only a single file named tool_dependencies.xml can be pushed to a repository "
                                         msg += "of type 'Tool dependency definition'."
                                         log.debug( msg )
                                         return self.__display_exception_remotely( start_response, msg )
                     else:
                         # If the changeset includes changes to dependency definition files, make sure tag sets
                         # are not missing "toolshed" or "changeset_revision" attributes since automatically populating
                         # them is not supported when pushing from the command line.  These attributes are automatically
                         # populated only when using the tool shed upload utility.
                         for entry in changeset_groups:
                             if len( entry ) == 2:
                                 # We possibly found an altered file entry.
                                 filename, change_list = entry
                                 if filename and isinstance( filename, str ):
                                     if filename in [ rt_util.REPOSITORY_DEPENDENCY_DEFINITION_FILENAME,
                                                      rt_util.TOOL_DEPENDENCY_DEFINITION_FILENAME ]:
                                         # We check both files since tool dependency definitions files can contain complex
                                         # repository dependency definitions.
                                         is_valid, error_msg = self.repository_tags_are_valid( filename, change_list )
                                         if not is_valid:
                                             log.debug( error_msg )
                                             return self.__display_exception_remotely( start_response, error_msg )
         if isinstance( result, str ):
             # Authentication was successful
             AUTH_TYPE.update( environ, 'basic' )
             REMOTE_USER.update( environ, result )
         else:
             return result.wsgi_application( environ, start_response )
     return self.app( environ, start_response )