Skip to content

Added preserve_time parameter to move, copy, and mirror #463

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Apr 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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