Skip to content

Commit 53f91b2

Browse files
committed
gh-42948: Fix shutil.move() on permission-restricted filesystems
1 parent 630cd37 commit 53f91b2

File tree

3 files changed

+56
-2
lines changed

3 files changed

+56
-2
lines changed

Lib/shutil.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,8 +443,13 @@ def lookup(name):
443443
else:
444444
st = lookup("stat")(src, follow_symlinks=follow)
445445
mode = stat.S_IMODE(st.st_mode)
446-
lookup("utime")(dst, ns=(st.st_atime_ns, st.st_mtime_ns),
447-
follow_symlinks=follow)
446+
try:
447+
lookup("utime")(dst, ns=(st.st_atime_ns, st.st_mtime_ns),
448+
follow_symlinks=follow)
449+
except OSError as e:
450+
# Ignore permission errors when setting file times (gh-42948).
451+
if e.errno not in (errno.EPERM, errno.EACCES):
452+
raise
448453
# We must copy extended attributes before the file is (potentially)
449454
# chmod()'ed read-only, otherwise setxattr() will error with -EACCES.
450455
_copyxattr(src, dst, follow_symlinks=follow)

Lib/test/test_shutil.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,6 +1251,52 @@ def _chflags_raiser(path, flags, *, follow_symlinks=True):
12511251
finally:
12521252
os.chflags = old_chflags
12531253

1254+
def test_copystat_handles_utime_errors(self):
1255+
# gh-42948: copystat should ignore permission errors when setting times
1256+
tmpdir = self.mkdtemp()
1257+
file1 = os.path.join(tmpdir, 'file1')
1258+
file2 = os.path.join(tmpdir, 'file2')
1259+
create_file(file1, 'xxx')
1260+
create_file(file2, 'xxx')
1261+
1262+
def make_utime_raiser(err):
1263+
def _utime_raiser(path, times=None, *, ns=None, dir_fd=None,
1264+
follow_symlinks=True):
1265+
ex = OSError()
1266+
ex.errno = err
1267+
raise ex
1268+
return _utime_raiser
1269+
1270+
for err in errno.EPERM, errno.EACCES:
1271+
with unittest.mock.patch('os.utime', side_effect=make_utime_raiser(err)):
1272+
shutil.copystat(file1, file2)
1273+
1274+
with unittest.mock.patch('os.utime', side_effect=make_utime_raiser(errno.EINVAL)):
1275+
self.assertRaises(OSError, shutil.copystat, file1, file2)
1276+
1277+
@mock_rename
1278+
def test_move_handles_utime_errors(self):
1279+
# gh-42948: move should succeed despite utime permission errors
1280+
src_dir = self.mkdtemp()
1281+
dst_dir = self.mkdtemp()
1282+
src_file = os.path.join(src_dir, 'file')
1283+
dst_file = os.path.join(dst_dir, 'file')
1284+
create_file(src_file, 'content')
1285+
1286+
def _utime_raiser(path, times=None, *, ns=None, dir_fd=None,
1287+
follow_symlinks=True):
1288+
ex = OSError()
1289+
ex.errno = errno.EPERM
1290+
raise ex
1291+
1292+
with unittest.mock.patch('os.utime', side_effect=_utime_raiser):
1293+
result = shutil.move(src_file, dst_file)
1294+
self.assertEqual(result, dst_file)
1295+
self.assertTrue(os.path.exists(dst_file))
1296+
self.assertFalse(os.path.exists(src_file))
1297+
with open(dst_file) as f:
1298+
self.assertEqual(f.read(), 'content')
1299+
12541300
### shutil.copyxattr
12551301

12561302
@os_helper.skip_unless_xattr
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:func:`shutil.copystat` now ignores permission errors when setting file
2+
times. This fixes :func:`shutil.move` failures on filesystems where the user
3+
has write access but is not the owner. Patch by Shamil Abdulaev.

0 commit comments

Comments
 (0)