Skip to content

Commit fb0b35a

Browse files
committed
add optimization
1 parent 05fc659 commit fb0b35a

File tree

3 files changed

+50
-11
lines changed

3 files changed

+50
-11
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
88

99
## Unreleased
1010

11+
- Optimized moving folders between filesystems with syspaths
12+
([#550](https://github.com/PyFilesystem/pyfilesystem2/pull/550))
1113

1214
### Added
1315

fs/move.py

+31-8
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55

66
import typing
77

8+
import os
9+
810
from ._pathcompat import commonpath
911
from .copy import copy_dir, copy_file
10-
from .errors import FSError
12+
from .error_tools import convert_os_errors
13+
from .errors import DirectoryExpected, FSError, IllegalDestination, ResourceNotFound
1114
from .opener import manage_fs
1215
from .osfs import OSFS
13-
from .path import frombase
16+
from .path import frombase, isbase
1417

1518
if typing.TYPE_CHECKING:
1619
from typing import Text, Union
@@ -26,15 +29,13 @@ def move_fs(
2629
):
2730
# type: (...) -> None
2831
"""Move the contents of a filesystem to another filesystem.
29-
3032
Arguments:
3133
src_fs (FS or str): Source filesystem (instance or URL).
3234
dst_fs (FS or str): Destination filesystem (instance or URL).
3335
workers (int): Use `worker` threads to copy data, or ``0`` (default) for
3436
a single-threaded copy.
3537
preserve_time (bool): If `True`, try to preserve mtime of the
3638
resources (defaults to `False`).
37-
3839
"""
3940
move_dir(src_fs, "/", dst_fs, "/", workers=workers, preserve_time=preserve_time)
4041

@@ -49,7 +50,6 @@ def move_file(
4950
):
5051
# type: (...) -> None
5152
"""Move a file from one filesystem to another.
52-
5353
Arguments:
5454
src_fs (FS or str): Source filesystem (instance or URL).
5555
src_path (str): Path to a file on ``src_fs``.
@@ -59,7 +59,6 @@ def move_file(
5959
resources (defaults to `False`).
6060
cleanup_dst_on_error (bool): If `True`, tries to delete the file copied to
6161
``dst_fs`` if deleting the file from ``src_fs`` fails (defaults to `True`).
62-
6362
"""
6463
with manage_fs(src_fs, writeable=True) as _src_fs:
6564
with manage_fs(dst_fs, writeable=True, create=True) as _dst_fs:
@@ -123,7 +122,6 @@ def move_dir(
123122
):
124123
# type: (...) -> None
125124
"""Move a directory from one filesystem to another.
126-
127125
Arguments:
128126
src_fs (FS or str): Source filesystem (instance or URL).
129127
src_path (str): Path to a directory on ``src_fs``
@@ -133,10 +131,35 @@ def move_dir(
133131
(default) for a single-threaded copy.
134132
preserve_time (bool): If `True`, try to preserve mtime of the
135133
resources (defaults to `False`).
136-
134+
Raises:
135+
fs.errors.ResourceNotFound: if ``src_path`` does not exist on `src_fs`
136+
fs.errors.DirectoryExpected: if ``src_path`` or one of its
137+
ancestors is not a directory.
138+
fs.errors.IllegalDestination: when moving a folder into itself
137139
"""
138140
with manage_fs(src_fs, writeable=True) as _src_fs:
139141
with manage_fs(dst_fs, writeable=True, create=True) as _dst_fs:
142+
if not _src_fs.exists(src_path):
143+
raise ResourceNotFound(src_path)
144+
if not _src_fs.isdir(src_path):
145+
raise DirectoryExpected(src_path)
146+
147+
# if both filesystems have a syspath we use `os.rename` to move the folder
148+
if _src_fs.hassyspath(src_path) and _dst_fs.hassyspath(dst_path):
149+
src_syspath = _src_fs.getsyspath(src_path)
150+
dst_syspath = _dst_fs.getsyspath(dst_path)
151+
# recheck if the move operation is legal using the syspaths
152+
if isbase(src_syspath, dst_syspath):
153+
raise IllegalDestination(dst_path)
154+
with _src_fs.lock(), _dst_fs.lock():
155+
with convert_os_errors("move_dir", src_path, directory=True):
156+
os.rename(src_syspath, dst_syspath)
157+
# recreate the root dir if it has been renamed
158+
if src_path == "/":
159+
_src_fs.makedir("/")
160+
return # optimization worked, exit early
161+
162+
# standard copy then delete
140163
with _src_fs.lock(), _dst_fs.lock():
141164
_dst_fs.makedir(dst_path, recreate=True)
142165
copy_dir(

tests/test_move.py

+17-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
except ImportError:
88
import mock
99

10+
import os
1011
from parameterized import parameterized, parameterized_class
1112

1213
import fs.move
@@ -167,7 +168,7 @@ def test_move_file_overwrite(self, _, fs_url):
167168
self.assertFalse(src.exists("target.txt"))
168169
self.assertFalse(dst.exists("file.txt"))
169170
self.assertTrue(dst.exists("target.txt"))
170-
self.assertEquals(dst.readtext("target.txt"), "source content")
171+
self.assertEqual(dst.readtext("target.txt"), "source content")
171172

172173
@parameterized.expand([("temp", "temp://"), ("mem", "mem://")])
173174
def test_move_file_overwrite_itself(self, _, fs_url):
@@ -177,7 +178,7 @@ def test_move_file_overwrite_itself(self, _, fs_url):
177178
tmp.writetext("file.txt", "content")
178179
fs.move.move_file(tmp, "file.txt", tmp, "file.txt")
179180
self.assertTrue(tmp.exists("file.txt"))
180-
self.assertEquals(tmp.readtext("file.txt"), "content")
181+
self.assertEqual(tmp.readtext("file.txt"), "content")
181182

182183
@parameterized.expand([("temp", "temp://"), ("mem", "mem://")])
183184
def test_move_file_overwrite_itself_relpath(self, _, fs_url):
@@ -188,7 +189,7 @@ def test_move_file_overwrite_itself_relpath(self, _, fs_url):
188189
new_dir.writetext("file.txt", "content")
189190
fs.move.move_file(tmp, "dir/../dir/file.txt", tmp, "dir/file.txt")
190191
self.assertTrue(tmp.exists("dir/file.txt"))
191-
self.assertEquals(tmp.readtext("dir/file.txt"), "content")
192+
self.assertEqual(tmp.readtext("dir/file.txt"), "content")
192193

193194
@parameterized.expand([(True,), (False,)])
194195
def test_move_file_cleanup_on_error(self, cleanup):
@@ -206,3 +207,16 @@ def test_move_file_cleanup_on_error(self, cleanup):
206207
)
207208
self.assertTrue(src.exists("file.txt"))
208209
self.assertEqual(not dst.exists("target.txt"), cleanup)
210+
211+
@parameterized.expand([("temp", "temp://", True), ("mem", "mem://", False)])
212+
def test_move_dir_optimized(self, _, fs_url, mv_called):
213+
with open_fs(fs_url) as tmp:
214+
with mock.patch.object(os, "rename", wraps=os.rename) as mv:
215+
dir_ = tmp.makedir("dir")
216+
dir_.writetext("file.txt", "content")
217+
sub = dir_.makedir("sub")
218+
sub.writetext("file.txt", "sub content")
219+
fs.move.move_dir(tmp, "dir", tmp, "newdir")
220+
self.assertEqual(tmp.readtext("newdir/file.txt"), "content")
221+
self.assertEqual(tmp.readtext("newdir/sub/file.txt"), "sub content")
222+
self.assertEqual(mv.called, mv_called)

0 commit comments

Comments
 (0)