Skip to content

Commit 11ad1ec

Browse files
authored
Merge pull request #547 from tfeldmann/fix/file-deleted-when-moved-onto-itself
Bugfix: File deleted / emptied when moved / copied onto itself (#546)
2 parents f169482 + 420998d commit 11ad1ec

File tree

7 files changed

+165
-29
lines changed

7 files changed

+165
-29
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1616
### Fixed
1717
- Elaborated documentation of `filter_dirs` and `exclude_dirs` in `fs.walk.Walker`.
1818
Closes [#371](https://github.com/PyFilesystem/pyfilesystem2/issues/371).
19-
19+
- Fixed a bug where files could be truncated or deleted when moved / copied onto itself.
20+
Closes [#546](https://github.com/PyFilesystem/pyfilesystem2/issues/546)
2021

2122
## [2.4.16] - 2022-05-02
2223

fs/base.py

+37-18
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121
from contextlib import closing
2222
from functools import partial, wraps
2323

24-
from . import copy, errors, fsencode, iotools, tools, walk, wildcard, glob
24+
from . import copy, errors, fsencode, glob, iotools, tools, walk, wildcard
2525
from .copy import copy_modified_time
2626
from .glob import BoundGlobber
2727
from .mode import validate_open_mode
28-
from .path import abspath, join, normpath
28+
from .path import abspath, isbase, join, normpath
2929
from .time import datetime_to_epoch
3030
from .walk import Walker
3131

@@ -423,13 +423,17 @@ def copy(
423423
424424
"""
425425
with self._lock:
426-
if not overwrite and self.exists(dst_path):
426+
_src_path = self.validatepath(src_path)
427+
_dst_path = self.validatepath(dst_path)
428+
if not overwrite and self.exists(_dst_path):
427429
raise errors.DestinationExists(dst_path)
428-
with closing(self.open(src_path, "rb")) as read_file:
430+
if _src_path == _dst_path:
431+
raise errors.IllegalDestination(dst_path)
432+
with closing(self.open(_src_path, "rb")) as read_file:
429433
# FIXME(@althonos): typing complains because open return IO
430-
self.upload(dst_path, read_file) # type: ignore
434+
self.upload(_dst_path, read_file) # type: ignore
431435
if preserve_time:
432-
copy_modified_time(self, src_path, self, dst_path)
436+
copy_modified_time(self, _src_path, self, _dst_path)
433437

434438
def copydir(
435439
self,
@@ -457,11 +461,15 @@ def copydir(
457461
458462
"""
459463
with self._lock:
460-
if not create and not self.exists(dst_path):
464+
_src_path = self.validatepath(src_path)
465+
_dst_path = self.validatepath(dst_path)
466+
if isbase(_src_path, _dst_path):
467+
raise errors.IllegalDestination(dst_path)
468+
if not create and not self.exists(_dst_path):
461469
raise errors.ResourceNotFound(dst_path)
462-
if not self.getinfo(src_path).is_dir:
470+
if not self.getinfo(_src_path).is_dir:
463471
raise errors.DirectoryExpected(src_path)
464-
copy.copy_dir(self, src_path, self, dst_path, preserve_time=preserve_time)
472+
copy.copy_dir(self, _src_path, self, _dst_path, preserve_time=preserve_time)
465473

466474
def create(self, path, wipe=False):
467475
# type: (Text, bool) -> bool
@@ -1088,6 +1096,12 @@ def movedir(self, src_path, dst_path, create=False, preserve_time=False):
10881096
from .move import move_dir
10891097

10901098
with self._lock:
1099+
_src_path = self.validatepath(src_path)
1100+
_dst_path = self.validatepath(dst_path)
1101+
if _src_path == _dst_path:
1102+
return
1103+
if isbase(_src_path, _dst_path):
1104+
raise errors.IllegalDestination(dst_path)
10911105
if not create and not self.exists(dst_path):
10921106
raise errors.ResourceNotFound(dst_path)
10931107
move_dir(self, src_path, self, dst_path, preserve_time=preserve_time)
@@ -1157,14 +1171,19 @@ def move(self, src_path, dst_path, overwrite=False, preserve_time=False):
11571171
``dst_path`` does not exist.
11581172
11591173
"""
1160-
if not overwrite and self.exists(dst_path):
1174+
_src_path = self.validatepath(src_path)
1175+
_dst_path = self.validatepath(dst_path)
1176+
if not overwrite and self.exists(_dst_path):
11611177
raise errors.DestinationExists(dst_path)
1162-
if self.getinfo(src_path).is_dir:
1178+
if self.getinfo(_src_path).is_dir:
11631179
raise errors.FileExpected(src_path)
1180+
if _src_path == _dst_path:
1181+
# early exit when moving a file onto itself
1182+
return
11641183
if self.getmeta().get("supports_rename", False):
11651184
try:
1166-
src_sys_path = self.getsyspath(src_path)
1167-
dst_sys_path = self.getsyspath(dst_path)
1185+
src_sys_path = self.getsyspath(_src_path)
1186+
dst_sys_path = self.getsyspath(_dst_path)
11681187
except errors.NoSysPath: # pragma: no cover
11691188
pass
11701189
else:
@@ -1174,15 +1193,15 @@ def move(self, src_path, dst_path, overwrite=False, preserve_time=False):
11741193
pass
11751194
else:
11761195
if preserve_time:
1177-
copy_modified_time(self, src_path, self, dst_path)
1196+
copy_modified_time(self, _src_path, self, _dst_path)
11781197
return
11791198
with self._lock:
1180-
with self.open(src_path, "rb") as read_file:
1199+
with self.open(_src_path, "rb") as read_file:
11811200
# FIXME(@althonos): typing complains because open return IO
1182-
self.upload(dst_path, read_file) # type: ignore
1201+
self.upload(_dst_path, read_file) # type: ignore
11831202
if preserve_time:
1184-
copy_modified_time(self, src_path, self, dst_path)
1185-
self.remove(src_path)
1203+
copy_modified_time(self, _src_path, self, _dst_path)
1204+
self.remove(_src_path)
11861205

11871206
def open(
11881207
self,

fs/copy.py

+18-7
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
import warnings
99

10-
from .errors import ResourceNotFound
10+
from .errors import IllegalDestination, ResourceNotFound
1111
from .opener import manage_fs
12-
from .path import abspath, combine, frombase, normpath
12+
from .path import abspath, combine, frombase, isbase, normpath
1313
from .tools import is_thread_safe
1414
from .walk import Walker
1515

@@ -257,9 +257,13 @@ def copy_file_internal(
257257
lock (bool): Lock both filesystems before copying.
258258
259259
"""
260+
_src_path = src_fs.validatepath(src_path)
261+
_dst_path = dst_fs.validatepath(dst_path)
260262
if src_fs is dst_fs:
261-
# Same filesystem, so we can do a potentially optimized
262-
# copy
263+
# It's not allowed to copy a file onto itself
264+
if _src_path == _dst_path:
265+
raise IllegalDestination(dst_path)
266+
# Same filesystem, so we can do a potentially optimized copy
263267
src_fs.copy(src_path, dst_path, overwrite=True, preserve_time=preserve_time)
264268
return
265269

@@ -305,11 +309,18 @@ def copy_structure(
305309
walker = walker or Walker()
306310
with manage_fs(src_fs) as _src_fs:
307311
with manage_fs(dst_fs, create=True) as _dst_fs:
312+
_src_root = _src_fs.validatepath(src_root)
313+
_dst_root = _dst_fs.validatepath(dst_root)
314+
315+
# It's not allowed to copy a structure into itself
316+
if _src_fs == _dst_fs and isbase(_src_root, _dst_root):
317+
raise IllegalDestination(dst_root)
318+
308319
with _src_fs.lock(), _dst_fs.lock():
309-
_dst_fs.makedirs(dst_root, recreate=True)
310-
for dir_path in walker.dirs(_src_fs, src_root):
320+
_dst_fs.makedirs(_dst_root, recreate=True)
321+
for dir_path in walker.dirs(_src_fs, _src_root):
311322
_dst_fs.makedir(
312-
combine(dst_root, frombase(src_root, dir_path)), recreate=True
323+
combine(_dst_root, frombase(_src_root, dir_path)), recreate=True
313324
)
314325

315326

fs/errors.py

+11
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"FilesystemClosed",
3333
"FSError",
3434
"IllegalBackReference",
35+
"IllegalDestination",
3536
"InsufficientStorage",
3637
"InvalidCharsInPath",
3738
"InvalidPath",
@@ -240,6 +241,16 @@ class RemoveRootError(OperationFailed):
240241
default_message = "root directory may not be removed"
241242

242243

244+
class IllegalDestination(OperationFailed):
245+
"""The given destination cannot be used for the operation.
246+
247+
This error will occur when attempting to move / copy a folder into itself or copying
248+
a file onto itself.
249+
"""
250+
251+
default_message = "'{path}' is not a legal destination"
252+
253+
243254
class ResourceError(FSError):
244255
"""Base exception class for error associated with a specific resource."""
245256

fs/memoryfs.py

+18-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from .enums import ResourceType, Seek
2020
from .info import Info
2121
from .mode import Mode
22-
from .path import iteratepath, normpath, split
22+
from .path import isbase, iteratepath, normpath, split
2323

2424
if typing.TYPE_CHECKING:
2525
from typing import (
@@ -462,6 +462,12 @@ def move(self, src_path, dst_path, overwrite=False, preserve_time=False):
462462
elif not overwrite and dst_name in dst_dir_entry:
463463
raise errors.DestinationExists(dst_path)
464464

465+
# handle moving a file onto itself
466+
if src_dir == dst_dir and src_name == dst_name:
467+
if overwrite:
468+
return
469+
raise errors.DestinationExists(dst_path)
470+
465471
# move the entry from the src folder to the dst folder
466472
dst_dir_entry.set_entry(dst_name, src_entry)
467473
src_dir_entry.remove_entry(src_name)
@@ -472,8 +478,17 @@ def move(self, src_path, dst_path, overwrite=False, preserve_time=False):
472478
copy_modified_time(self, src_path, self, dst_path)
473479

474480
def movedir(self, src_path, dst_path, create=False, preserve_time=False):
475-
src_dir, src_name = split(self.validatepath(src_path))
476-
dst_dir, dst_name = split(self.validatepath(dst_path))
481+
_src_path = self.validatepath(src_path)
482+
_dst_path = self.validatepath(dst_path)
483+
dst_dir, dst_name = split(_dst_path)
484+
src_dir, src_name = split(_src_path)
485+
486+
# move a dir onto itself
487+
if _src_path == _dst_path:
488+
return
489+
# move a dir into itself
490+
if isbase(_src_path, _dst_path):
491+
raise errors.IllegalDestination(dst_path)
477492

478493
with self._lock:
479494
src_dir_entry = self._get_dir_entry(src_dir)

fs/osfs.py

+3
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,9 @@ def _check_copy(self, src_path, dst_path, overwrite=False):
411411
# check dst_path does not exist if we are not overwriting
412412
if not overwrite and self.exists(_dst_path):
413413
raise errors.DestinationExists(dst_path)
414+
# it's not allowed to copy a file onto itself
415+
if _src_path == _dst_path:
416+
raise errors.IllegalDestination(dst_path)
414417
# check parent dir of _dst_path exists and is a directory
415418
if self.gettype(dirname(dst_path)) is not ResourceType.directory:
416419
raise errors.DirectoryExpected(dirname(dst_path))

fs/test.py

+76
Original file line numberDiff line numberDiff line change
@@ -1811,6 +1811,40 @@ def test_move_file_mem(self):
18111811
def test_move_file_temp(self):
18121812
self._test_move_file("temp://")
18131813

1814+
def test_move_file_onto_itself(self):
1815+
self.fs.writetext("file.txt", "Hello")
1816+
self.fs.move("file.txt", "file.txt", overwrite=True)
1817+
self.assert_text("file.txt", "Hello")
1818+
1819+
with self.assertRaises(errors.DestinationExists):
1820+
self.fs.move("file.txt", "file.txt", overwrite=False)
1821+
1822+
def test_move_file_onto_itself_relpath(self):
1823+
subdir = self.fs.makedir("sub")
1824+
subdir.writetext("file.txt", "Hello")
1825+
self.fs.move("sub/file.txt", "sub/../sub/file.txt", overwrite=True)
1826+
self.assert_text("sub/file.txt", "Hello")
1827+
1828+
with self.assertRaises(errors.DestinationExists):
1829+
self.fs.move("sub/file.txt", "sub/../sub/file.txt", overwrite=False)
1830+
1831+
def test_copy_file_onto_itself(self):
1832+
self.fs.writetext("file.txt", "Hello")
1833+
with self.assertRaises(errors.IllegalDestination):
1834+
self.fs.copy("file.txt", "file.txt", overwrite=True)
1835+
with self.assertRaises(errors.DestinationExists):
1836+
self.fs.copy("file.txt", "file.txt", overwrite=False)
1837+
self.assert_text("file.txt", "Hello")
1838+
1839+
def test_copy_file_onto_itself_relpath(self):
1840+
subdir = self.fs.makedir("sub")
1841+
subdir.writetext("file.txt", "Hello")
1842+
with self.assertRaises(errors.IllegalDestination):
1843+
self.fs.copy("sub/file.txt", "sub/../sub/file.txt", overwrite=True)
1844+
with self.assertRaises(errors.DestinationExists):
1845+
self.fs.copy("sub/file.txt", "sub/../sub/file.txt", overwrite=False)
1846+
self.assert_text("sub/file.txt", "Hello")
1847+
18141848
def test_copydir(self):
18151849
self.fs.makedirs("foo/bar/baz/egg")
18161850
self.fs.writetext("foo/bar/foofoo.txt", "Hello")
@@ -1828,6 +1862,27 @@ def test_copydir(self):
18281862
with self.assertRaises(errors.DirectoryExpected):
18291863
self.fs.copydir("foo2/foofoo.txt", "foofoo.txt", create=True)
18301864

1865+
def test_copydir_onto_itself(self):
1866+
folder = self.fs.makedir("folder")
1867+
folder.writetext("file1.txt", "Hello1")
1868+
sub = folder.makedir("sub")
1869+
sub.writetext("file2.txt", "Hello2")
1870+
1871+
with self.assertRaises(errors.IllegalDestination):
1872+
self.fs.copydir("folder", "folder")
1873+
self.assert_text("folder/file1.txt", "Hello1")
1874+
self.assert_text("folder/sub/file2.txt", "Hello2")
1875+
1876+
def test_copydir_into_its_own_subfolder(self):
1877+
folder = self.fs.makedir("folder")
1878+
folder.writetext("file1.txt", "Hello1")
1879+
sub = folder.makedir("sub")
1880+
sub.writetext("file2.txt", "Hello2")
1881+
with self.assertRaises(errors.IllegalDestination):
1882+
self.fs.copydir("folder", "folder/sub/")
1883+
self.assert_text("folder/file1.txt", "Hello1")
1884+
self.assert_text("folder/sub/file2.txt", "Hello2")
1885+
18311886
def test_movedir(self):
18321887
self.fs.makedirs("foo/bar/baz/egg")
18331888
self.fs.writetext("foo/bar/foofoo.txt", "Hello")
@@ -1851,6 +1906,27 @@ def test_movedir(self):
18511906
with self.assertRaises(errors.DirectoryExpected):
18521907
self.fs.movedir("foo2/foofoo.txt", "foo2/baz/egg")
18531908

1909+
def test_movedir_onto_itself(self):
1910+
folder = self.fs.makedir("folder")
1911+
folder.writetext("file1.txt", "Hello1")
1912+
sub = folder.makedir("sub")
1913+
sub.writetext("file2.txt", "Hello2")
1914+
1915+
self.fs.movedir("folder", "folder")
1916+
self.assert_text("folder/file1.txt", "Hello1")
1917+
self.assert_text("folder/sub/file2.txt", "Hello2")
1918+
1919+
def test_movedir_into_its_own_subfolder(self):
1920+
folder = self.fs.makedir("folder")
1921+
folder.writetext("file1.txt", "Hello1")
1922+
sub = folder.makedir("sub")
1923+
sub.writetext("file2.txt", "Hello2")
1924+
1925+
with self.assertRaises(errors.IllegalDestination):
1926+
self.fs.movedir("folder", "folder/sub/")
1927+
self.assert_text("folder/file1.txt", "Hello1")
1928+
self.assert_text("folder/sub/file2.txt", "Hello2")
1929+
18541930
def test_match(self):
18551931
self.assertTrue(self.fs.match(["*.py"], "foo.py"))
18561932
self.assertEqual(

0 commit comments

Comments
 (0)