Skip to content

Commit

Permalink
Merge pull request #463 from atollk/issue_450
Browse files Browse the repository at this point in the history
Added preserve_time parameter to move, copy, and mirror
  • Loading branch information
althonos committed Apr 12, 2021
2 parents 5f73778 + ebcd1a9 commit ac8a91a
Show file tree
Hide file tree
Showing 20 changed files with 459 additions and 122 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Better documentation of the `writable` parameter of `fs.open_fs`, and
hint about using `fs.wrap.read_only` when a read-only filesystem is
required. Closes [#441](https://github.com/PyFilesystem/pyfilesystem2/issues/441).
- Copy and move operations now provide a parameter `preserve_time` that, when
passed as `True`, makes sure the "mtime" of the destination file will be
the same as that of the source file.

### Changed

Expand Down
27 changes: 20 additions & 7 deletions fs/_bulk.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@

from six.moves.queue import Queue

from .copy import copy_file_internal
from .copy import copy_file_internal, copy_modified_time
from .errors import BulkCopyFailed
from .tools import copy_file_data

if typing.TYPE_CHECKING:
from .base import FS
from types import TracebackType
from typing import IO, List, Optional, Text, Type
from typing import List, Optional, Text, Type, IO, Tuple


class _Worker(threading.Thread):
Expand Down Expand Up @@ -75,11 +75,13 @@ def __call__(self):
class Copier(object):
"""Copy files in worker threads."""

def __init__(self, num_workers=4):
# type: (int) -> None
def __init__(self, num_workers=4, preserve_time=False):
# type: (int, bool) -> None
if num_workers < 0:
raise ValueError("num_workers must be >= 0")
self.num_workers = num_workers
self.preserve_time = preserve_time
self.all_tasks = [] # type: List[Tuple[FS, Text, FS, Text]]
self.queue = None # type: Optional[Queue[_Task]]
self.workers = [] # type: List[_Worker]
self.errors = [] # type: List[Exception]
Expand All @@ -97,10 +99,18 @@ def start(self):
def stop(self):
"""Stop the workers (will block until they are finished)."""
if self.running and self.num_workers:
# Notify the workers that all tasks have arrived
# and wait for them to finish.
for _worker in self.workers:
self.queue.put(None)
for worker in self.workers:
worker.join()

# If the "last modified" time is to be preserved, do it now.
if self.preserve_time:
for args in self.all_tasks:
copy_modified_time(*args)

# Free up references held by workers
del self.workers[:]
self.queue.join()
Expand All @@ -124,13 +134,16 @@ def __exit__(
if traceback is None and self.errors:
raise BulkCopyFailed(self.errors)

def copy(self, src_fs, src_path, dst_fs, dst_path):
# type: (FS, Text, FS, Text) -> None
def copy(self, src_fs, src_path, dst_fs, dst_path, preserve_time=False):
# type: (FS, Text, FS, Text, bool) -> None
"""Copy a file from one fs to another."""
if self.queue is None:
# This should be the most performant for a single-thread
copy_file_internal(src_fs, src_path, dst_fs, dst_path)
copy_file_internal(
src_fs, src_path, dst_fs, dst_path, preserve_time=self.preserve_time
)
else:
self.all_tasks.append((src_fs, src_path, dst_fs, dst_path))
src_file = src_fs.openbin(src_path, "r")
try:
dst_file = dst_fs.openbin(dst_path, "w")
Expand Down
47 changes: 37 additions & 10 deletions fs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import six

from . import copy, errors, fsencode, iotools, move, tools, walk, wildcard
from .copy import copy_modified_time
from .glob import BoundGlobber
from .mode import validate_open_mode
from .path import abspath, join, normpath
Expand Down Expand Up @@ -393,15 +394,23 @@ def close(self):
"""
self._closed = True

def copy(self, src_path, dst_path, overwrite=False):
# type: (Text, Text, bool) -> None
def copy(
self,
src_path, # type: Text
dst_path, # type: Text
overwrite=False, # type: bool
preserve_time=False, # type: bool
):
# type: (...) -> None
"""Copy file contents from ``src_path`` to ``dst_path``.
Arguments:
src_path (str): Path of source file.
dst_path (str): Path to destination file.
overwrite (bool): If `True`, overwrite the destination file
if it exists (defaults to `False`).
preserve_time (bool): If `True`, try to preserve mtime of the
resource (defaults to `False`).
Raises:
fs.errors.DestinationExists: If ``dst_path`` exists,
Expand All @@ -417,16 +426,26 @@ def copy(self, src_path, dst_path, overwrite=False):
with closing(self.open(src_path, "rb")) as read_file:
# FIXME(@althonos): typing complains because open return IO
self.upload(dst_path, read_file) # type: ignore
if preserve_time:
copy_modified_time(self, src_path, self, dst_path)

def copydir(self, src_path, dst_path, create=False):
# type: (Text, Text, bool) -> None
def copydir(
self,
src_path, # type: Text
dst_path, # type: Text
create=False, # type: bool
preserve_time=False, # type: bool
):
# type: (...) -> None
"""Copy the contents of ``src_path`` to ``dst_path``.
Arguments:
src_path (str): Path of source directory.
dst_path (str): Path to destination directory.
create (bool): If `True`, then ``dst_path`` will be created
if it doesn't exist already (defaults to `False`).
preserve_time (bool): If `True`, try to preserve mtime of the
resource (defaults to `False`).
Raises:
fs.errors.ResourceNotFound: If the ``dst_path``
Expand All @@ -440,7 +459,7 @@ def copydir(self, src_path, dst_path, create=False):
raise errors.ResourceNotFound(dst_path)
if not self.getinfo(src_path).is_dir:
raise errors.DirectoryExpected(src_path)
copy.copy_dir(self, src_path, self, dst_path)
copy.copy_dir(self, src_path, self, dst_path, preserve_time=preserve_time)

def create(self, path, wipe=False):
# type: (Text, bool) -> bool
Expand Down Expand Up @@ -1027,15 +1046,17 @@ def lock(self):
"""
return self._lock

def movedir(self, src_path, dst_path, create=False):
# type: (Text, Text, bool) -> None
def movedir(self, src_path, dst_path, create=False, preserve_time=False):
# type: (Text, Text, bool, bool) -> None
"""Move directory ``src_path`` to ``dst_path``.
Arguments:
src_path (str): Path of source directory on the filesystem.
dst_path (str): Path to destination directory.
create (bool): If `True`, then ``dst_path`` will be created
if it doesn't exist already (defaults to `False`).
preserve_time (bool): If `True`, try to preserve mtime of the
resources (defaults to `False`).
Raises:
fs.errors.ResourceNotFound: if ``dst_path`` does not exist,
Expand All @@ -1047,7 +1068,7 @@ def movedir(self, src_path, dst_path, create=False):
with self._lock:
if not create and not self.exists(dst_path):
raise errors.ResourceNotFound(dst_path)
move.move_dir(self, src_path, self, dst_path)
move.move_dir(self, src_path, self, dst_path, preserve_time=preserve_time)

def makedirs(
self,
Expand Down Expand Up @@ -1092,8 +1113,8 @@ def makedirs(
raise
return self.opendir(path)

def move(self, src_path, dst_path, overwrite=False):
# type: (Text, Text, bool) -> None
def move(self, src_path, dst_path, overwrite=False, preserve_time=False):
# type: (Text, Text, bool, bool) -> None
"""Move a file from ``src_path`` to ``dst_path``.
Arguments:
Expand All @@ -1102,6 +1123,8 @@ def move(self, src_path, dst_path, overwrite=False):
file will be written to.
overwrite (bool): If `True`, destination path will be
overwritten if it exists.
preserve_time (bool): If `True`, try to preserve mtime of the
resources (defaults to `False`).
Raises:
fs.errors.FileExpected: If ``src_path`` maps to a
Expand All @@ -1128,11 +1151,15 @@ def move(self, src_path, dst_path, overwrite=False):
except OSError:
pass
else:
if preserve_time:
copy_modified_time(self, src_path, self, dst_path)
return
with self._lock:
with self.open(src_path, "rb") as read_file:
# FIXME(@althonos): typing complains because open return IO
self.upload(dst_path, read_file) # type: ignore
if preserve_time:
copy_modified_time(self, src_path, self, dst_path)
self.remove(src_path)

def open(
Expand Down
Loading

0 comments on commit ac8a91a

Please sign in to comment.