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
5 changes: 4 additions & 1 deletion Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,11 @@ def copyfile(src, dst, *, follow_symlinks=True):
if _samefile(src, dst):
raise SameFileError("{!r} and {!r} are the same file".format(src, dst))

copy_symlink = not follow_symlinks and _islink(src)
file_size = 0
for i, fn in enumerate([src, dst]):
if copy_symlink and i == 0:
continue
try:
st = _stat(fn)
except OSError:
Expand All @@ -315,7 +318,7 @@ def copyfile(src, dst, *, follow_symlinks=True):
if _WINDOWS and i == 0:
file_size = st.st_size

if not follow_symlinks and _islink(src):
if copy_symlink:
os.symlink(os.readlink(src), dst)
else:
with open(src, 'rb') as fsrc:
Expand Down
46 changes: 46 additions & 0 deletions Lib/test/test_shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -1574,6 +1574,47 @@ def test_copyfile_socket(self):
self.assertRaisesRegex(shutil.SpecialFileError, 'is a socket',
shutil.copyfile, __file__, sock_path)

def _check_copyfile_symlink_to_special_file(self, target):
tmp_dir = self.mkdtemp()
src = os.path.join(tmp_dir, 'src')
dst = os.path.join(tmp_dir, 'dst')
os.symlink(target, src)

shutil.copyfile(src, dst, follow_symlinks=False)

self.assertTrue(os.path.islink(dst))
self.assertEqual(os.readlink(dst), target)

@os_helper.skip_unless_symlink
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
@unittest.skipIf(sys.platform == "vxworks",
"fifo requires special path on VxWorks")
def test_copyfile_symlink_to_named_pipe(self):
fifo_path = os.path.join(self.mkdtemp(), 'fifo')
try:
os.mkfifo(fifo_path)
except PermissionError as e:
self.skipTest('os.mkfifo(): %s' % e)
self._check_copyfile_symlink_to_special_file(fifo_path)

@os_helper.skip_unless_symlink
@socket_helper.skip_unless_bind_unix_socket
def test_copyfile_symlink_to_socket(self):
sock_path = os.path.join(self.mkdtemp(), 'sock')
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.addCleanup(sock.close)
try:
socket_helper.bind_unix_socket(sock, sock_path)
except OSError as e:
if str(e) == "AF_UNIX path too long":
self.skipTest(
"Pathname {0!a} is too long to serve as an AF_UNIX path"
.format(sock_path))
else:
raise
self.addCleanup(os_helper.unlink, sock_path)
self._check_copyfile_symlink_to_special_file(sock_path)

@unittest.skipUnless(os.path.exists('/dev/null'), 'requires /dev/null')
def test_copyfile_character_device(self):
self.assertRaisesRegex(shutil.SpecialFileError, 'is a character device',
Expand All @@ -1583,6 +1624,11 @@ def test_copyfile_character_device(self):
self.assertRaisesRegex(shutil.SpecialFileError, 'is a character device',
shutil.copyfile, src_file, '/dev/null')

@os_helper.skip_unless_symlink
@unittest.skipUnless(os.path.exists('/dev/null'), 'requires /dev/null')
def test_copyfile_symlink_to_character_device(self):
self._check_copyfile_symlink_to_special_file('/dev/null')

def test_copyfile_block_device(self):
block_dev = None
for dev in ['/dev/loop0', '/dev/sda', '/dev/vda', '/dev/disk0']:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :func:`shutil.copyfile` to copy a symbolic link to a special file when
``follow_symlinks=False`` instead of raising
:exc:`~shutil.SpecialFileError`.
Loading