Uses the ``zipfile`` Python module to create zip files

    .. versionchanged:: 2015.5.0
        This function was rewritten to use Python's native zip file support.
        The old functionality has been preserved in the new function
        :mod:`archive.cmd_zip <salt.modules.archive.cmd_zip>`. For versions
        2014.7.x and earlier, see the :mod:`archive.cmd_zip
        <salt.modules.archive.cmd_zip>` documentation.

    zip_file
        Path of zip file to be created

    sources
        Comma-separated list of sources to include in the zip file. Sources can
        also be passed in a Python list.

        .. versionchanged:: 2017.7.0
            Globbing is now supported for this argument

    template : None
        Can be set to 'jinja' or another supported template engine to render
        the command arguments before execution:

        .. code-block:: bash

            salt '*' archive.zip template=jinja /tmp/zipfile.zip /tmp/sourcefile1,/tmp/{{grains.id}}.txt

    cwd : None
        Use this argument along with relative paths in ``sources`` to create
        zip files which do not contain the leading directories. If not
        specified, the zip file will be created as if the cwd was ``/``, and
        creating a zip file of ``/foo/bar/baz.txt`` will contain the parent
        directories ``foo`` and ``bar``. To create a zip file containing just
        ``baz.txt``, the following command would be used:

        .. code-block:: bash

            salt '*' archive.zip /tmp/baz.zip baz.txt cwd=/foo/bar

    runas : None
        Create the zip file as the specified user. Defaults to the user under
        which the minion is running.

    zip64 : False
        Used to enable ZIP64 support, necessary to create archives larger than
        4 GByte in size.
        If true, will create ZIP file with the ZIPp64 extension when the zipfile
        is larger than 2 GB.
        ZIP64 extension is disabled by default in the Python native zip support
        because the default zip and unzip commands on Unix (the InfoZIP utilities)
        don't support these extensions.

    CLI Example:

    .. code-block:: bash

        salt '*' archive.zip /tmp/zipfile.zip /tmp/sourcefile1,/tmp/sourcefile2
        # Globbing for sources (2017.7.0 and later)
        salt '*' archive.zip /tmp/zipfile.zip '/tmp/sourcefile*'

def zip_(zip_file, sources, template=None, cwd=None, runas=None, zip64=False):
    '''
    Uses the ``zipfile`` Python module to create zip files

    .. versionchanged:: 2015.5.0
        This function was rewritten to use Python's native zip file support.
        The old functionality has been preserved in the new function
        :mod:`archive.cmd_zip <salt.modules.archive.cmd_zip>`. For versions
        2014.7.x and earlier, see the :mod:`archive.cmd_zip
        <salt.modules.archive.cmd_zip>` documentation.

    zip_file
        Path of zip file to be created

    sources
        Comma-separated list of sources to include in the zip file. Sources can
        also be passed in a Python list.

        .. versionchanged:: 2017.7.0
            Globbing is now supported for this argument

    template : None
        Can be set to 'jinja' or another supported template engine to render
        the command arguments before execution:

        .. code-block:: bash

            salt '*' archive.zip template=jinja /tmp/zipfile.zip /tmp/sourcefile1,/tmp/{{grains.id}}.txt

    cwd : None
        Use this argument along with relative paths in ``sources`` to create
        zip files which do not contain the leading directories. If not
        specified, the zip file will be created as if the cwd was ``/``, and
        creating a zip file of ``/foo/bar/baz.txt`` will contain the parent
        directories ``foo`` and ``bar``. To create a zip file containing just
        ``baz.txt``, the following command would be used:

        .. code-block:: bash

            salt '*' archive.zip /tmp/baz.zip baz.txt cwd=/foo/bar

    runas : None
        Create the zip file as the specified user. Defaults to the user under
        which the minion is running.

    zip64 : False
        Used to enable ZIP64 support, necessary to create archives larger than
        4 GByte in size.
        If true, will create ZIP file with the ZIPp64 extension when the zipfile
        is larger than 2 GB.
        ZIP64 extension is disabled by default in the Python native zip support
        because the default zip and unzip commands on Unix (the InfoZIP utilities)
        don't support these extensions.

    CLI Example:

    .. code-block:: bash

        salt '*' archive.zip /tmp/zipfile.zip /tmp/sourcefile1,/tmp/sourcefile2
        # Globbing for sources (2017.7.0 and later)
        salt '*' archive.zip /tmp/zipfile.zip '/tmp/sourcefile*'
    '''
    if runas:
        euid = os.geteuid()
        egid = os.getegid()
        uinfo = __salt__['user.info'](runas)
        if not uinfo:
            raise SaltInvocationError(
                'User \'{0}\' does not exist'.format(runas)
            )

    zip_file, sources = _render_filenames(zip_file, sources, None, template)
    sources = _expand_sources(sources)

    if not cwd:
        for src in sources:
            if not os.path.isabs(src):
                raise SaltInvocationError(
                    'Relative paths require the \'cwd\' parameter'
                )
    else:
        err_msg = 'cwd must be absolute'
        try:
            if not os.path.isabs(cwd):
                raise SaltInvocationError(err_msg)
        except AttributeError:
            raise SaltInvocationError(err_msg)

    if runas and (euid != uinfo['uid'] or egid != uinfo['gid']):
        # Change the egid first, as changing it after the euid will fail
        # if the runas user is non-privileged.
        os.setegid(uinfo['gid'])
        os.seteuid(uinfo['uid'])

    try:
        exc = None
        archived_files = []
        with contextlib.closing(zipfile.ZipFile(zip_file, 'w', zipfile.ZIP_DEFLATED, zip64)) as zfile:
            for src in sources:
                if cwd:
                    src = os.path.join(cwd, src)
                if os.path.exists(src):
                    if os.path.isabs(src):
                        rel_root = '/'
                    else:
                        rel_root = cwd if cwd is not None else '/'
                    if os.path.isdir(src):
                        for dir_name, sub_dirs, files in salt.utils.path.os_walk(src):
                            if cwd and dir_name.startswith(cwd):
                                arc_dir = os.path.relpath(dir_name, cwd)
                            else:
                                arc_dir = os.path.relpath(dir_name, rel_root)
                            if arc_dir:
                                archived_files.append(arc_dir + '/')
                                zfile.write(dir_name, arc_dir)
                            for filename in files:
                                abs_name = os.path.join(dir_name, filename)
                                arc_name = os.path.join(arc_dir, filename)
                                archived_files.append(arc_name)
                                zfile.write(abs_name, arc_name)
                    else:
                        if cwd and src.startswith(cwd):
                            arc_name = os.path.relpath(src, cwd)
                        else:
                            arc_name = os.path.relpath(src, rel_root)
                        archived_files.append(arc_name)
                        zfile.write(src, arc_name)
    except Exception as exc:
        pass
    finally:
        # Restore the euid/egid
        if runas:
            os.seteuid(euid)
            os.setegid(egid)
        if exc is not None:
            # Wait to raise the exception until euid/egid are restored to avoid
            # permission errors in writing to minion log.
            if exc == zipfile.LargeZipFile:
                raise CommandExecutionError(
                    'Resulting zip file too large, would require ZIP64 support'
                    'which has not been enabled. Rerun command with zip64=True'
                )
            else:
                raise CommandExecutionError(
                    'Exception encountered creating zipfile: {0}'.format(exc)
                )

    return archived_files