Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -1063,10 +1063,28 @@ def _set_uid_gid(tarinfo):

def _make_zipfile(base_name, base_dir, verbose=0, dry_run=0,
logger=None, owner=None, group=None, root_dir=None):
"""Create a zip file from all the files under 'base_dir'.

The output zip file will be named 'base_name' + ".zip". Returns the
name of the output zip file.
"""Create a zip file. Returns the name of the zip file.

- `base_name` is used as the name for the zip file, with a suffix
of '.zip'.
- `base_dir` is the directory containing the files to be included
in the zip file. The zip file is created at the same level in the
directory structure as `base_dir`.

Symbolic links to files and directories are followed and the
targets of the links included in the zip file. This matches the
default behaviour of command-line zip utilities on
Linux/UNIX/Windows systems.

Hard links to files are followed and the targets of the links
included in the zip file. There is no de-duplication of multiple
hard links to the same file that is provided by other formats,
e.g. tar.

CAUTION: This function uses os.walk() to prepare the list of files to be
included in the zip file. os.walk() does not keep track of which
directories have already been visited. A symbolic link to a directory that
is a parent of itself will lead to infinite recursion.
"""
import zipfile # late import for breaking circular dependency

Expand Down Expand Up @@ -1094,7 +1112,7 @@ def _make_zipfile(base_name, base_dir, verbose=0, dry_run=0,
zf.write(base_dir, arcname)
if logger is not None:
logger.info("adding '%s'", base_dir)
for dirpath, dirnames, filenames in os.walk(base_dir):
for dirpath, dirnames, filenames in os.walk(base_dir, followlinks=True):
arcdirpath = dirpath
if root_dir is not None:
arcdirpath = os.path.relpath(arcdirpath, root_dir)
Expand Down Expand Up @@ -1183,16 +1201,16 @@ def unregister_archive_format(name):

def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0,
dry_run=0, owner=None, group=None, logger=None):
"""Create an archive file (eg. zip or tar).
"""Create an archive file (e.g. zip or tar).

'base_name' is the name of the file to create, minus any format-specific
extension; 'format' is the archive format: one of "zip", "tar", "gztar",
"bztar", "xztar", or "zstdtar". Or any other registered format.

'root_dir' is a directory that will be the root directory of the
archive; ie. we typically chdir into 'root_dir' before creating the
archive; i.e. we typically chdir into 'root_dir' before creating the
archive. 'base_dir' is the directory where we start archiving from;
ie. 'base_dir' will be the common prefix of all files and
i.e. 'base_dir' will be the common prefix of all files and
directories in the archive. 'root_dir' and 'base_dir' both default
to the current directory. Returns the name of the archive file.

Expand Down
72 changes: 72 additions & 0 deletions Lib/test/test_shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -1636,6 +1636,37 @@ def _create_files(self, base_dir='dist'):
create_file((root_dir, 'outer'), 'xxx')
return root_dir, base_dir

def _create_files_symlinks(self, base_dir="symlinks"):
# Create a test structure containing symbolic links to files and
# directories.
root_dir = self.mkdtemp()
wd = os.path.join(root_dir, base_dir)
os.mkdir(wd)
# Create a regular file.
create_file(
(wd, 'file1'),
'This is file1.'
)
# Create a symbolic link to the file.
os.symlink(
os.path.join(wd, 'file1'),
os.path.join(wd, 'link1'),
)
# Create a sub-directory.
os.mkdir(os.path.join(wd, 'sub'))
# Create a regular file in the sub-directory.
create_file(
(wd, 'sub', 'file2'),
'This is a file2.'
)
# Create a symbolic link to the sub-directory.
os.symlink(
os.path.join(wd, 'sub'),
os.path.join(wd, 'sub2'),
target_is_directory=True,
)
return root_dir, base_dir

@support.requires_zlib()
def test_make_tarfile(self):
root_dir, base_dir = self._create_files()
Expand Down Expand Up @@ -1882,6 +1913,47 @@ def test_zipfile_vs_zip(self):
names2 = zf.namelist()
self.assertEqual(sorted(names), sorted(names2))

@os_helper.skip_unless_symlink
def test_make_zipfile_symlink_behaviour(self):
# Test that symbolic links for both file and directories are resolved
# to their targets when shutil.make_archive() is used to make a zip
# file, to match default command-line zip behaviour in
# Linux/UNIX/Windows.
#
# If this test is being skipped, it is because either the operating
# environment does not support symbolic links, or you do not have the
# necessary permissions to create them. For Windows (10 and above) the
# test must be invoked from an elevated command prompt or with
# Developer Mode turned on.
root_dir, base_dir = self._create_files_symlinks()
name = os.path.join(root_dir, 'z')
archive = shutil.make_archive(
name, "zip", root_dir=root_dir, base_dir=base_dir
)
# Check we have a zip file.
self.assertTrue(zipfile.is_zipfile(archive))
with zipfile.ZipFile(archive) as zf:
extract_dir = os.path.join(root_dir, base_dir, 'extract')
zf.extractall(extract_dir)
try:
# If symbolic link sub2 that targets directory sub1 was
# preserved as a link then sub2/file2 will not exist.
self.assertTrue(
os.path.exists(
os.path.join(extract_dir, base_dir, 'sub2/file2')
)
)
# If symbolic link link1 that targets file1 was preserved then
# it will be a symbolic link.
self.assertFalse(
os.path.islink(
os.path.join(extract_dir, base_dir, 'link1')
)
)
finally:
# Clean up.
shutil.rmtree(root_dir)

@support.requires_zlib()
@unittest.skipUnless(shutil.which('unzip'),
'Need the unzip command to run')
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ Eugene Dvurechenski
Karmen Dykstra
Josip Dzolonga
Maxim Dzumanenko
Thomas Earp
Hans Eckardt
Rodolpho Eckhardt
Ulrich Eckhardt
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The default behaviour of shutil.make_archive(), in relation to handling of symbolic links when creating zip files, now matches the default behaviour of command-line zip utilities on Linux/UNIX/Windows: symbolic links to both files and directories are resolved to their targets and included in the zip file.
Loading