Skip to content

Commit 5f8f0e8

Browse files
committed
Make sure nothing is changed if a rename failed
- rename is implemented via remove/add, a failed add shall revert the remove - see #643
1 parent c81445c commit 5f8f0e8

File tree

3 files changed

+40
-5
lines changed

3 files changed

+40
-5
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ The released versions correspond to PyPi releases.
1111
* fixed handling of alternative path separator in `os.path.split`,
1212
`os.path.splitdrive` and `glob.glob`
1313
(see [#632](../../issues/632))
14+
* fixed handling of failed rename due to permission error
15+
(see [#643](../../issues/643))
16+
1417

1518
## [Version 4.5.1](https://pypi.python.org/pypi/pyfakefs/4.5.1) (2021-08-29)
1619
This is a bugfix release.

pyfakefs/fake_filesystem.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2257,14 +2257,27 @@ def rename(self, old_file_path: AnyPath,
22572257
if new_dir_object.has_parent_object(old_object):
22582258
self.raise_os_error(errno.EINVAL, new_path)
22592259

2260+
self._do_rename(old_dir_object, old_name, new_dir_object, new_name)
2261+
2262+
def _do_rename(self, old_dir_object, old_name, new_dir_object, new_name):
22602263
object_to_rename = old_dir_object.get_entry(old_name)
22612264
old_dir_object.remove_entry(old_name, recursive=False)
22622265
object_to_rename.name = new_name
22632266
new_name = new_dir_object._normalized_entryname(new_name)
2264-
if new_name in new_dir_object.entries:
2265-
# in case of overwriting remove the old entry first
2266-
new_dir_object.remove_entry(new_name)
2267-
new_dir_object.add_entry(object_to_rename)
2267+
old_entry = (new_dir_object.get_entry(new_name)
2268+
if new_name in new_dir_object.entries else None)
2269+
try:
2270+
if old_entry:
2271+
# in case of overwriting remove the old entry first
2272+
new_dir_object.remove_entry(new_name)
2273+
new_dir_object.add_entry(object_to_rename)
2274+
except OSError:
2275+
# adding failed, roll back the changes before re-raising
2276+
if old_entry and new_name not in new_dir_object.entries:
2277+
new_dir_object.add_entry(old_entry)
2278+
object_to_rename.name = old_name
2279+
old_dir_object.add_entry(object_to_rename)
2280+
raise
22682281

22692282
def _handle_broken_link_with_trailing_sep(self, path: AnyStr) -> None:
22702283
# note that the check for trailing sep has to be done earlier

pyfakefs/tests/fake_filesystem_shutil_test.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import unittest
2525

2626
from pyfakefs import fake_filesystem_unittest
27-
from pyfakefs.fake_filesystem import is_root
27+
from pyfakefs.fake_filesystem import is_root, set_uid, USER_ID
2828
from pyfakefs.tests.test_utils import RealFsTestMixin
2929

3030
is_windows = sys.platform == 'win32'
@@ -38,6 +38,8 @@ def __init__(self, methodName='runTest'):
3838
def setUp(self):
3939
RealFsTestMixin.setUp(self)
4040
self.cwd = os.getcwd()
41+
self.uid = USER_ID
42+
set_uid(1000)
4143
if not self.use_real_fs():
4244
self.setUpPyfakefs()
4345
self.filesystem = self.fs
@@ -47,6 +49,7 @@ def setUp(self):
4749
self.fs.set_disk_usage(1000, self.base_path)
4850

4951
def tearDown(self):
52+
set_uid(self.uid)
5053
RealFsTestMixin.tearDown(self)
5154

5255
@property
@@ -57,6 +60,22 @@ def is_windows_fs(self):
5760

5861

5962
class FakeShutilModuleTest(RealFsTestCase):
63+
@unittest.skipIf(is_windows, 'Posix specific behavior')
64+
def test_catch_permission_error(self):
65+
root_path = self.make_path('rootpath')
66+
self.create_dir(root_path)
67+
dir1_path = self.os.path.join(root_path, 'dir1')
68+
dir2_path = self.os.path.join(root_path, 'dir2')
69+
self.create_dir(dir1_path)
70+
self.os.chmod(dir1_path, 0o555) # remove write permissions
71+
self.create_dir(dir2_path)
72+
old_file_path = self.os.path.join(dir2_path, 'f1.txt')
73+
new_file_path = self.os.path.join(dir1_path, 'f1.txt')
74+
self.create_file(old_file_path)
75+
76+
with self.assertRaises(PermissionError):
77+
shutil.move(old_file_path, new_file_path)
78+
6079
def test_rmtree(self):
6180
directory = self.make_path('xyzzy')
6281
dir_path = os.path.join(directory, 'subdir')

0 commit comments

Comments
 (0)