diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f933fb1..c24b0e66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/fs/_bulk.py b/fs/_bulk.py index 9b0b8b79..12eef85e 100644 --- a/fs/_bulk.py +++ b/fs/_bulk.py @@ -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): @@ -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] @@ -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() @@ -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") diff --git a/fs/base.py b/fs/base.py index 4966d0ca..8d8ee4c8 100644 --- a/fs/base.py +++ b/fs/base.py @@ -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 @@ -393,8 +394,14 @@ 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: @@ -402,6 +409,8 @@ def copy(self, src_path, dst_path, overwrite=False): 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, @@ -417,9 +426,17 @@ 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: @@ -427,6 +444,8 @@ def copydir(self, src_path, dst_path, create=False): 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`` @@ -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 @@ -1027,8 +1046,8 @@ 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: @@ -1036,6 +1055,8 @@ def movedir(self, src_path, dst_path, create=False): 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, @@ -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, @@ -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: @@ -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 @@ -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( diff --git a/fs/copy.py b/fs/copy.py index 03108c00..95650b94 100644 --- a/fs/copy.py +++ b/fs/copy.py @@ -25,6 +25,7 @@ def copy_fs( walker=None, # type: Optional[Walker] on_copy=None, # type: Optional[_OnCopy] workers=0, # type: int + preserve_time=False, # type: bool ): # type: (...) -> None """Copy the contents of one filesystem to another. @@ -40,9 +41,13 @@ def copy_fs( dst_path)``. workers (int): Use `worker` threads to copy data, or ``0`` (default) for a single-threaded copy. + preserve_time (bool): If `True`, try to preserve mtime of the + resources (defaults to `False`). """ - return copy_fs_if(src_fs, dst_fs, "always", walker, on_copy, workers) + return copy_fs_if( + src_fs, dst_fs, "always", walker, on_copy, workers, preserve_time=preserve_time + ) def copy_fs_if_newer( @@ -51,6 +56,7 @@ def copy_fs_if_newer( walker=None, # type: Optional[Walker] on_copy=None, # type: Optional[_OnCopy] workers=0, # type: int + preserve_time=False, # type: bool ): # type: (...) -> None """Copy the contents of one filesystem to another, checking times. @@ -62,7 +68,9 @@ def copy_fs_if_newer( warnings.warn( "copy_fs_if_newer is deprecated. Use copy_fs_if instead.", DeprecationWarning ) - return copy_fs_if(src_fs, dst_fs, "newer", walker, on_copy, workers) + return copy_fs_if( + src_fs, dst_fs, "newer", walker, on_copy, workers, preserve_time=preserve_time + ) def copy_fs_if( @@ -72,6 +80,7 @@ def copy_fs_if( walker=None, # type: Optional[Walker] on_copy=None, # type: Optional[_OnCopy] workers=0, # type: int + preserve_time=False, # type: bool ): # type: (...) -> None """Copy the contents of one filesystem to another, depending on a condition. @@ -88,6 +97,8 @@ def copy_fs_if( dst_path)``. workers (int): Use ``worker`` threads to copy data, or ``0`` (default) for a single-threaded copy. + preserve_time (bool): If `True`, try to preserve mtime of the + resources (defaults to `False`). See Also: `~fs.copy.copy_file_if` for the full list of supported values for the @@ -103,6 +114,7 @@ def copy_fs_if( walker=walker, on_copy=on_copy, workers=workers, + preserve_time=preserve_time, ) @@ -111,6 +123,7 @@ def copy_file( src_path, # type: Text dst_fs, # type: Union[FS, Text] dst_path, # type: Text + preserve_time=False, # type: bool ): # type: (...) -> None """Copy a file from one filesystem to another. @@ -122,9 +135,13 @@ def copy_file( src_path (str): Path to a file on the source filesystem. dst_fs (FS or str): Destination filesystem (instance or URL). dst_path (str): Path to a file on the destination filesystem. + preserve_time (bool): If `True`, try to preserve mtime of the + resource (defaults to `False`). """ - copy_file_if(src_fs, src_path, dst_fs, dst_path, "always") + copy_file_if( + src_fs, src_path, dst_fs, dst_path, "always", preserve_time=preserve_time + ) def copy_file_if_newer( @@ -132,6 +149,7 @@ def copy_file_if_newer( src_path, # type: Text dst_fs, # type: Union[FS, Text] dst_path, # type: Text + preserve_time=False, # type: bool ): # type: (...) -> bool """Copy a file from one filesystem to another, checking times. @@ -144,7 +162,9 @@ def copy_file_if_newer( "copy_file_if_newer is deprecated. Use copy_file_if instead.", DeprecationWarning, ) - return copy_file_if(src_fs, src_path, dst_fs, dst_path, "newer") + return copy_file_if( + src_fs, src_path, dst_fs, dst_path, "newer", preserve_time=preserve_time + ) def copy_file_if( @@ -153,6 +173,7 @@ def copy_file_if( dst_fs, # type: Union[FS, Text] dst_path, # type: Text condition, # type: Text + preserve_time=False, # type: bool ): # type: (...) -> bool """Copy a file from one filesystem to another, depending on a condition. @@ -184,6 +205,8 @@ def copy_file_if( dst_fs (FS or str): Destination filesystem (instance or URL). dst_path (str): Path to a file on the destination filesystem. condition (str): Name of the condition to check for each file. + preserve_time (bool): If `True`, try to preserve mtime of the + resource (defaults to `False`). Returns: bool: `True` if the file copy was executed, `False` otherwise. @@ -195,7 +218,14 @@ def copy_file_if( _src_fs, src_path, _dst_fs, dst_path, condition ) if do_copy: - copy_file_internal(_src_fs, src_path, _dst_fs, dst_path, True) + copy_file_internal( + _src_fs, + src_path, + _dst_fs, + dst_path, + preserve_time=preserve_time, + lock=True, + ) return do_copy @@ -204,6 +234,7 @@ def copy_file_internal( src_path, # type: Text dst_fs, # type: FS dst_path, # type: Text + preserve_time=False, # type: bool lock=False, # type: bool ): # type: (...) -> None @@ -219,13 +250,15 @@ def copy_file_internal( src_path (str): Path to a file on the source filesystem. dst_fs (FS): Destination filesystem. dst_path (str): Path to a file on the destination filesystem. + preserve_time (bool): If `True`, try to preserve mtime of the + resource (defaults to `False`). lock (bool): Lock both filesystems before copying. """ if src_fs is dst_fs: # Same filesystem, so we can do a potentially optimized # copy - src_fs.copy(src_path, dst_path, overwrite=True) + src_fs.copy(src_path, dst_path, overwrite=True, preserve_time=preserve_time) return def _copy_locked(): @@ -236,6 +269,9 @@ def _copy_locked(): with src_fs.openbin(src_path) as read_file: dst_fs.upload(dst_path, read_file) + if preserve_time: + copy_modified_time(src_fs, src_path, dst_fs, dst_path) + if lock: with src_fs.lock(), dst_fs.lock(): _copy_locked() @@ -283,6 +319,7 @@ def copy_dir( walker=None, # type: Optional[Walker] on_copy=None, # type: Optional[_OnCopy] workers=0, # type: int + preserve_time=False, # type: bool ): # type: (...) -> None """Copy a directory from one filesystem to another. @@ -300,9 +337,21 @@ def copy_dir( ``(src_fs, src_path, dst_fs, dst_path)``. workers (int): Use ``worker`` threads to copy data, or ``0`` (default) for a single-threaded copy. + preserve_time (bool): If `True`, try to preserve mtime of the + resources (defaults to `False`). """ - copy_dir_if(src_fs, src_path, dst_fs, dst_path, "always", walker, on_copy, workers) + copy_dir_if( + src_fs, + src_path, + dst_fs, + dst_path, + "always", + walker, + on_copy, + workers, + preserve_time=preserve_time, + ) def copy_dir_if_newer( @@ -313,6 +362,7 @@ def copy_dir_if_newer( walker=None, # type: Optional[Walker] on_copy=None, # type: Optional[_OnCopy] workers=0, # type: int + preserve_time=False, # type: bool ): # type: (...) -> None """Copy a directory from one filesystem to another, checking times. @@ -324,7 +374,17 @@ def copy_dir_if_newer( warnings.warn( "copy_dir_if_newer is deprecated. Use copy_dir_if instead.", DeprecationWarning ) - copy_dir_if(src_fs, src_path, dst_fs, dst_path, "newer", walker, on_copy, workers) + copy_dir_if( + src_fs, + src_path, + dst_fs, + dst_path, + "newer", + walker, + on_copy, + workers, + preserve_time=preserve_time, + ) def copy_dir_if( @@ -336,6 +396,7 @@ def copy_dir_if( walker=None, # type: Optional[Walker] on_copy=None, # type: Optional[_OnCopy] workers=0, # type: int + preserve_time=False, # type: bool ): # type: (...) -> None """Copy a directory from one filesystem to another, depending on a condition. @@ -354,6 +415,8 @@ def copy_dir_if( dst_path)``. workers (int): Use ``worker`` threads to copy data, or ``0`` (default) for a single-threaded copy. + preserve_time (bool): If `True`, try to preserve mtime of the + resources (defaults to `False`). See Also: `~fs.copy.copy_file_if` for the full list of supported values for the @@ -374,7 +437,9 @@ def copy_dir_if( ) as _dst_fs: with _src_fs.lock(), _dst_fs.lock(): _thread_safe = is_thread_safe(_src_fs, _dst_fs) - with Copier(num_workers=workers if _thread_safe else 0) as copier: + with Copier( + num_workers=workers if _thread_safe else 0, preserve_time=preserve_time + ) as copier: for dir_path in walker.files(_src_fs, _src_path): copy_path = combine(_dst_path, frombase(_src_path, dir_path)) if _copy_is_necessary( @@ -432,3 +497,31 @@ def _copy_is_necessary( else: raise ValueError("{} is not a valid copy condition.".format(condition)) + + +def copy_modified_time( + src_fs, # type: Union[FS, Text] + src_path, # type: Text + dst_fs, # type: Union[FS, Text] + dst_path, # type: Text +): + # type: (...) -> None + """Copy modified time metadata from one file to another. + + Arguments: + src_fs (FS or str): Source filesystem (instance or URL). + src_path (str): Path to a directory on the source filesystem. + dst_fs (FS or str): Destination filesystem (instance or URL). + dst_path (str): Path to a directory on the destination filesystem. + + """ + namespaces = ("details",) + with manage_fs(src_fs, writeable=False) as _src_fs: + with manage_fs(dst_fs, create=True) as _dst_fs: + src_meta = _src_fs.getinfo(src_path, namespaces) + src_details = src_meta.raw.get("details", {}) + dst_details = {} + for value in ("metadata_changed", "modified"): + if value in src_details: + dst_details[value] = src_details[value] + _dst_fs.setinfo(dst_path, {"details": dst_details}) diff --git a/fs/ftpfs.py b/fs/ftpfs.py index 515709df..86f04958 100644 --- a/fs/ftpfs.py +++ b/fs/ftpfs.py @@ -6,6 +6,7 @@ import array import calendar +import datetime import io import itertools import socket @@ -19,8 +20,7 @@ from ftplib import FTP_TLS except ImportError as err: FTP_TLS = err # type: ignore -from ftplib import error_perm -from ftplib import error_temp +from ftplib import error_perm, error_temp from typing import cast from six import PY2 @@ -836,8 +836,32 @@ def writebytes(self, path, contents): def setinfo(self, path, info): # type: (Text, RawInfo) -> None - if not self.exists(path): - raise errors.ResourceNotFound(path) + use_mfmt = False + if "MFMT" in self.features: + info_details = None + if "modified" in info: + info_details = info["modified"] + elif "details" in info: + info_details = info["details"] + if info_details and "modified" in info_details: + use_mfmt = True + mtime = cast(float, info_details["modified"]) + + if use_mfmt: + with ftp_errors(self, path): + cmd = ( + "MFMT " + + datetime.datetime.utcfromtimestamp(mtime).strftime("%Y%m%d%H%M%S") + + " " + + _encode(path, self.ftp.encoding) + ) + try: + self.ftp.sendcmd(cmd) + except error_perm: + pass + else: + if not self.exists(path): + raise errors.ResourceNotFound(path) def readbytes(self, path): # type: (Text) -> bytes diff --git a/fs/memoryfs.py b/fs/memoryfs.py index 7965a135..c34d60a7 100644 --- a/fs/memoryfs.py +++ b/fs/memoryfs.py @@ -15,6 +15,7 @@ from . import errors from .base import FS +from .copy import copy_modified_time from .enums import ResourceType, Seek from .info import Info from .mode import Mode @@ -444,7 +445,7 @@ def makedir( parent_dir.set_entry(dir_name, new_dir) return self.opendir(path) - def move(self, src_path, dst_path, overwrite=False): + def move(self, src_path, dst_path, overwrite=False, preserve_time=False): src_dir, src_name = split(self.validatepath(src_path)) dst_dir, dst_name = split(self.validatepath(dst_path)) @@ -465,7 +466,10 @@ def move(self, src_path, dst_path, overwrite=False): dst_dir_entry.set_entry(dst_name, src_entry) src_dir_entry.remove_entry(src_name) - def movedir(self, src_path, dst_path, create=False): + if preserve_time: + copy_modified_time(self, src_path, self, dst_path) + + def movedir(self, src_path, dst_path, create=False, preserve_time=False): src_dir, src_name = split(self.validatepath(src_path)) dst_dir, dst_name = split(self.validatepath(dst_path)) @@ -484,6 +488,9 @@ def movedir(self, src_path, dst_path, create=False): dst_dir_entry.set_entry(dst_name, src_entry) src_dir_entry.remove_entry(src_name) + if preserve_time: + copy_modified_time(self, src_path, self, dst_path) + def openbin(self, path, mode="r", buffering=-1, **options): # type: (Text, Text, int, **Any) -> BinaryIO _mode = Mode(mode) diff --git a/fs/mirror.py b/fs/mirror.py index 6b989e63..dd00ff7b 100644 --- a/fs/mirror.py +++ b/fs/mirror.py @@ -57,6 +57,7 @@ def mirror( walker=None, # type: Optional[Walker] copy_if_newer=True, # type: bool workers=0, # type: int + preserve_time=False, # type: bool ): # type: (...) -> None """Mirror files / directories from one filesystem to another. @@ -73,6 +74,8 @@ def mirror( workers (int): Number of worker threads used (0 for single threaded). Set to a relatively low number for network filesystems, 4 would be a good start. + preserve_time (bool): If `True`, try to preserve mtime of the + resources (defaults to `False`). """ @@ -83,22 +86,30 @@ def dst(): return manage_fs(dst_fs, create=True) with src() as _src_fs, dst() as _dst_fs: - with _src_fs.lock(), _dst_fs.lock(): - _thread_safe = is_thread_safe(_src_fs, _dst_fs) - with Copier(num_workers=workers if _thread_safe else 0) as copier: + _thread_safe = is_thread_safe(_src_fs, _dst_fs) + with Copier( + num_workers=workers if _thread_safe else 0, preserve_time=preserve_time + ) as copier: + with _src_fs.lock(), _dst_fs.lock(): _mirror( _src_fs, _dst_fs, walker=walker, copy_if_newer=copy_if_newer, copy_file=copier.copy, + preserve_time=preserve_time, ) def _mirror( - src_fs, dst_fs, walker=None, copy_if_newer=True, copy_file=copy_file_internal + src_fs, # type: FS + dst_fs, # type: FS + walker=None, # type: Optional[Walker] + copy_if_newer=True, # type: bool + copy_file=copy_file_internal, # type: Callable[[FS, str, FS, str, bool], None] + preserve_time=False, # type: bool ): - # type: (FS, FS, Optional[Walker], bool, Callable[[FS, str, FS, str], None]) -> None + # type: (...) -> None walker = walker or Walker() walk = walker.walk(src_fs, namespaces=["details"]) for path, dirs, files in walk: @@ -122,7 +133,7 @@ def _mirror( # Compare file info if copy_if_newer and not _compare(_file, dst_file): continue - copy_file(src_fs, _path, dst_fs, _path) + copy_file(src_fs, _path, dst_fs, _path, preserve_time) # Make directories for _dir in dirs: diff --git a/fs/move.py b/fs/move.py index 1d8e26c1..56e9e5ca 100644 --- a/fs/move.py +++ b/fs/move.py @@ -15,8 +15,13 @@ from typing import Text, Union -def move_fs(src_fs, dst_fs, workers=0): - # type: (Union[Text, FS], Union[Text, FS], int) -> None +def move_fs( + src_fs, # type: Union[Text, FS] + dst_fs, # type:Union[Text, FS] + workers=0, # type: int + preserve_time=False, # type: bool +): + # type: (...) -> None """Move the contents of a filesystem to another filesystem. Arguments: @@ -24,9 +29,11 @@ def move_fs(src_fs, dst_fs, workers=0): dst_fs (FS or str): Destination filesystem (instance or URL). workers (int): Use `worker` threads to copy data, or ``0`` (default) for a single-threaded copy. + preserve_time (bool): If `True`, try to preserve mtime of the + resources (defaults to `False`). """ - move_dir(src_fs, "/", dst_fs, "/", workers=workers) + move_dir(src_fs, "/", dst_fs, "/", workers=workers, preserve_time=preserve_time) def move_file( @@ -34,6 +41,7 @@ def move_file( src_path, # type: Text dst_fs, # type: Union[Text, FS] dst_path, # type: Text + preserve_time=False, # type: bool ): # type: (...) -> None """Move a file from one filesystem to another. @@ -43,17 +51,27 @@ def move_file( src_path (str): Path to a file on ``src_fs``. dst_fs (FS or str): Destination filesystem (instance or URL). dst_path (str): Path to a file on ``dst_fs``. + preserve_time (bool): If `True`, try to preserve mtime of the + resources (defaults to `False`). """ with manage_fs(src_fs) as _src_fs: with manage_fs(dst_fs, create=True) as _dst_fs: if _src_fs is _dst_fs: # Same filesystem, may be optimized - _src_fs.move(src_path, dst_path, overwrite=True) + _src_fs.move( + src_path, dst_path, overwrite=True, preserve_time=preserve_time + ) else: # Standard copy and delete with _src_fs.lock(), _dst_fs.lock(): - copy_file(_src_fs, src_path, _dst_fs, dst_path) + copy_file( + _src_fs, + src_path, + _dst_fs, + dst_path, + preserve_time=preserve_time, + ) _src_fs.remove(src_path) @@ -63,6 +81,7 @@ def move_dir( dst_fs, # type: Union[Text, FS] dst_path, # type: Text workers=0, # type: int + preserve_time=False, # type: bool ): # type: (...) -> None """Move a directory from one filesystem to another. @@ -74,6 +93,8 @@ def move_dir( dst_path (str): Path to a directory on ``dst_fs``. workers (int): Use ``worker`` threads to copy data, or ``0`` (default) for a single-threaded copy. + preserve_time (bool): If `True`, try to preserve mtime of the + resources (defaults to `False`). """ @@ -86,5 +107,12 @@ def dst(): with src() as _src_fs, dst() as _dst_fs: with _src_fs.lock(), _dst_fs.lock(): _dst_fs.makedir(dst_path, recreate=True) - copy_dir(src_fs, src_path, dst_fs, dst_path, workers=workers) + copy_dir( + src_fs, + src_path, + dst_fs, + dst_path, + workers=workers, + preserve_time=preserve_time, + ) _src_fs.removetree(src_path) diff --git a/fs/osfs.py b/fs/osfs.py index 5beb16bf..ac43471a 100644 --- a/fs/osfs.py +++ b/fs/osfs.py @@ -49,6 +49,7 @@ from .mode import Mode, validate_open_mode from .errors import FileExpected, NoURL from ._url_tools import url_quote +from .copy import copy_modified_time if typing.TYPE_CHECKING: from typing import ( @@ -431,8 +432,8 @@ def _check_copy(self, src_path, dst_path, overwrite=False): if hasattr(errno, "ENOTSUP"): _sendfile_error_codes.add(errno.ENOTSUP) - def copy(self, src_path, dst_path, overwrite=False): - # type: (Text, Text, bool) -> None + def copy(self, src_path, dst_path, overwrite=False, preserve_time=False): + # type: (Text, Text, bool, bool) -> None with self._lock: # validate and canonicalise paths _src_path, _dst_path = self._check_copy(src_path, dst_path, overwrite) @@ -452,6 +453,8 @@ def copy(self, src_path, dst_path, overwrite=False): while sent > 0: sent = sendfile(fd_dst, fd_src, offset, maxsize) offset += sent + if preserve_time: + copy_modified_time(self, src_path, self, dst_path) except OSError as e: # the error is not a simple "sendfile not supported" error if e.errno not in self._sendfile_error_codes: @@ -461,8 +464,8 @@ def copy(self, src_path, dst_path, overwrite=False): else: - def copy(self, src_path, dst_path, overwrite=False): - # type: (Text, Text, bool) -> None + def copy(self, src_path, dst_path, overwrite=False, preserve_time=False): + # type: (Text, Text, bool, bool) -> None with self._lock: _src_path, _dst_path = self._check_copy(src_path, dst_path, overwrite) shutil.copy2(self.getsyspath(_src_path), self.getsyspath(_dst_path)) diff --git a/fs/wrap.py b/fs/wrap.py index e03ef805..21fc10d1 100644 --- a/fs/wrap.py +++ b/fs/wrap.py @@ -199,8 +199,8 @@ def makedir( self.check() raise ResourceReadOnly(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 self.check() raise ResourceReadOnly(dst_path) @@ -248,8 +248,8 @@ def settimes(self, path, accessed=None, modified=None): self.check() raise ResourceReadOnly(path) - def copy(self, src_path, dst_path, overwrite=False): - # type: (Text, Text, bool) -> None + def copy(self, src_path, dst_path, overwrite=False, preserve_time=False): + # type: (Text, Text, bool, bool) -> None self.check() raise ResourceReadOnly(dst_path) diff --git a/fs/wrapfs.py b/fs/wrapfs.py index 5944b111..00edd7af 100644 --- a/fs/wrapfs.py +++ b/fs/wrapfs.py @@ -167,18 +167,18 @@ def makedir( with unwrap_errors(path): return _fs.makedir(_path, permissions=permissions, recreate=recreate) - 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 # A custom move permits a potentially optimized code path src_fs, _src_path = self.delegate_path(src_path) dst_fs, _dst_path = self.delegate_path(dst_path) with unwrap_errors({_src_path: src_path, _dst_path: dst_path}): if not overwrite and dst_fs.exists(_dst_path): raise errors.DestinationExists(_dst_path) - move_file(src_fs, _src_path, dst_fs, _dst_path) + move_file(src_fs, _src_path, dst_fs, _dst_path, preserve_time=preserve_time) - 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 src_fs, _src_path = self.delegate_path(src_path) dst_fs, _dst_path = self.delegate_path(dst_path) with unwrap_errors({_src_path: src_path, _dst_path: dst_path}): @@ -186,7 +186,7 @@ def movedir(self, src_path, dst_path, create=False): raise errors.ResourceNotFound(dst_path) if not src_fs.getinfo(_src_path).is_dir: raise errors.DirectoryExpected(src_path) - move_dir(src_fs, _src_path, dst_fs, _dst_path) + move_dir(src_fs, _src_path, dst_fs, _dst_path, preserve_time=preserve_time) def openbin(self, path, mode="r", buffering=-1, **options): # type: (Text, Text, int, **Any) -> BinaryIO @@ -265,17 +265,17 @@ def touch(self, path): with unwrap_errors(path): _fs.touch(_path) - def copy(self, src_path, dst_path, overwrite=False): - # type: (Text, Text, bool) -> None + def copy(self, src_path, dst_path, overwrite=False, preserve_time=False): + # type: (Text, Text, bool, bool) -> None src_fs, _src_path = self.delegate_path(src_path) dst_fs, _dst_path = self.delegate_path(dst_path) with unwrap_errors({_src_path: src_path, _dst_path: dst_path}): if not overwrite and dst_fs.exists(_dst_path): raise errors.DestinationExists(_dst_path) - copy_file(src_fs, _src_path, dst_fs, _dst_path) + copy_file(src_fs, _src_path, dst_fs, _dst_path, preserve_time=preserve_time) - def copydir(self, src_path, dst_path, create=False): - # type: (Text, Text, bool) -> None + def copydir(self, src_path, dst_path, create=False, preserve_time=False): + # type: (Text, Text, bool, bool) -> None src_fs, _src_path = self.delegate_path(src_path) dst_fs, _dst_path = self.delegate_path(dst_path) with unwrap_errors({_src_path: src_path, _dst_path: dst_path}): @@ -283,7 +283,7 @@ def copydir(self, src_path, dst_path, create=False): raise errors.ResourceNotFound(dst_path) if not src_fs.getinfo(_src_path).is_dir: raise errors.DirectoryExpected(src_path) - copy_dir(src_fs, _src_path, dst_fs, _dst_path) + copy_dir(src_fs, _src_path, dst_fs, _dst_path, preserve_time=preserve_time) def create(self, path, wipe=False): # type: (Text, bool) -> bool diff --git a/tests/requirements.txt b/tests/requirements.txt index 9e7ece32..b7ff3ce4 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -14,3 +14,5 @@ pysendfile ~=2.0 ; python_version <= "3.3" # mock v4+ doesn't support Python 2.7 anymore mock ~=3.0 ; python_version < "3.3" +# parametrized to prevent code duplication in tests. +parameterized ~=0.8 \ No newline at end of file diff --git a/tests/test_copy.py b/tests/test_copy.py index 92d77c33..6441e812 100644 --- a/tests/test_copy.py +++ b/tests/test_copy.py @@ -8,6 +8,8 @@ import shutil import calendar +from parameterized import parameterized + import fs.copy from fs import open_fs @@ -61,20 +63,29 @@ def mkdirp(path): class TestCopySimple(unittest.TestCase): - def test_copy_fs(self): - for workers in (0, 1, 2, 4): - src_fs = open_fs("mem://") - src_fs.makedirs("foo/bar") - src_fs.makedirs("foo/empty") - src_fs.touch("test.txt") - src_fs.touch("foo/bar/baz.txt") + @parameterized.expand([(0,), (1,), (2,), (4,)]) + def test_copy_fs(self, workers): + namespaces = ("details", "modified") + + src_fs = open_fs("mem://") + src_fs.makedirs("foo/bar") + src_fs.makedirs("foo/empty") + src_fs.touch("test.txt") + src_fs.touch("foo/bar/baz.txt") + src_file1_info = src_fs.getinfo("test.txt", namespaces) + src_file2_info = src_fs.getinfo("foo/bar/baz.txt", namespaces) + + dst_fs = open_fs("mem://") + fs.copy.copy_fs(src_fs, dst_fs, workers=workers, preserve_time=True) - dst_fs = open_fs("mem://") - fs.copy.copy_fs(src_fs, dst_fs, workers=workers) + self.assertTrue(dst_fs.isdir("foo/empty")) + self.assertTrue(dst_fs.isdir("foo/bar")) + self.assertTrue(dst_fs.isfile("test.txt")) - self.assertTrue(dst_fs.isdir("foo/empty")) - self.assertTrue(dst_fs.isdir("foo/bar")) - self.assertTrue(dst_fs.isfile("test.txt")) + dst_file1_info = dst_fs.getinfo("test.txt", namespaces) + dst_file2_info = dst_fs.getinfo("foo/bar/baz.txt", namespaces) + self.assertEqual(dst_file1_info.modified, src_file1_info.modified) + self.assertEqual(dst_file2_info.modified, src_file2_info.modified) def test_copy_value_error(self): src_fs = open_fs("mem://") @@ -82,18 +93,46 @@ def test_copy_value_error(self): with self.assertRaises(ValueError): fs.copy.copy_fs(src_fs, dst_fs, workers=-1) - def test_copy_dir(self): + def test_copy_dir0(self): + namespaces = ("details", "modified") + + src_fs = open_fs("mem://") + src_fs.makedirs("foo/bar") + src_fs.makedirs("foo/empty") + src_fs.touch("test.txt") + src_fs.touch("foo/bar/baz.txt") + src_file2_info = src_fs.getinfo("foo/bar/baz.txt", namespaces) + + with open_fs("mem://") as dst_fs: + fs.copy.copy_dir(src_fs, "/foo", dst_fs, "/", workers=0, preserve_time=True) + self.assertTrue(dst_fs.isdir("bar")) + self.assertTrue(dst_fs.isdir("empty")) + self.assertTrue(dst_fs.isfile("bar/baz.txt")) + + dst_file2_info = dst_fs.getinfo("bar/baz.txt", namespaces) + self.assertEqual(dst_file2_info.modified, src_file2_info.modified) + + @parameterized.expand([(0,), (1,), (2,), (4,)]) + def test_copy_dir(self, workers): + namespaces = ("details", "modified") + src_fs = open_fs("mem://") src_fs.makedirs("foo/bar") src_fs.makedirs("foo/empty") src_fs.touch("test.txt") src_fs.touch("foo/bar/baz.txt") - for workers in (0, 1, 2, 4): - with open_fs("mem://") as dst_fs: - fs.copy.copy_dir(src_fs, "/foo", dst_fs, "/", workers=workers) - self.assertTrue(dst_fs.isdir("bar")) - self.assertTrue(dst_fs.isdir("empty")) - self.assertTrue(dst_fs.isfile("bar/baz.txt")) + src_file2_info = src_fs.getinfo("foo/bar/baz.txt", namespaces) + + with open_fs("mem://") as dst_fs: + fs.copy.copy_dir( + src_fs, "/foo", dst_fs, "/", workers=workers, preserve_time=True + ) + self.assertTrue(dst_fs.isdir("bar")) + self.assertTrue(dst_fs.isdir("empty")) + self.assertTrue(dst_fs.isfile("bar/baz.txt")) + + dst_file2_info = dst_fs.getinfo("bar/baz.txt", namespaces) + self.assertEqual(dst_file2_info.modified, src_file2_info.modified) def test_copy_large(self): data1 = b"foo" * 512 * 1024 diff --git a/tests/test_ftpfs.py b/tests/test_ftpfs.py index b0e0c1f0..43028da7 100644 --- a/tests/test_ftpfs.py +++ b/tests/test_ftpfs.py @@ -3,6 +3,7 @@ from __future__ import print_function from __future__ import unicode_literals +import calendar import os import platform import shutil @@ -11,6 +12,7 @@ import time import unittest import uuid +import datetime try: from unittest import mock @@ -144,7 +146,6 @@ def test_manager_with_host(self): @mark.slow @unittest.skipIf(platform.python_implementation() == "PyPy", "ftp unreliable with PyPy") class TestFTPFS(FSTestCases, unittest.TestCase): - user = "user" pasw = "1234" @@ -162,7 +163,7 @@ def setUpClass(cls): cls.server.shutdown_after = -1 cls.server.handler.authorizer = DummyAuthorizer() cls.server.handler.authorizer.add_user( - cls.user, cls.pasw, cls._temp_path, perm="elradfmw" + cls.user, cls.pasw, cls._temp_path, perm="elradfmwT" ) cls.server.handler.authorizer.add_anonymous(cls._temp_path) cls.server.start() @@ -223,6 +224,23 @@ def test_geturl(self): ), ) + def test_setinfo(self): + # TODO: temporary test, since FSTestCases.test_setinfo is broken. + self.fs.create("bar") + original_modified = self.fs.getinfo("bar", ("details",)).modified + new_modified = original_modified - datetime.timedelta(hours=1) + new_modified_stamp = calendar.timegm(new_modified.timetuple()) + self.fs.setinfo("bar", {"details": {"modified": new_modified_stamp}}) + new_modified_get = self.fs.getinfo("bar", ("details",)).modified + if original_modified.microsecond == 0 or new_modified_get.microsecond == 0: + original_modified = original_modified.replace(microsecond=0) + new_modified_get = new_modified_get.replace(microsecond=0) + if original_modified.second == 0 or new_modified_get.second == 0: + original_modified = original_modified.replace(second=0) + new_modified_get = new_modified_get.replace(second=0) + new_modified_get = new_modified_get + datetime.timedelta(hours=1) + self.assertEqual(original_modified, new_modified_get) + def test_host(self): self.assertEqual(self.fs.host, self.server.host) @@ -301,7 +319,6 @@ def test_features(self): @mark.slow @unittest.skipIf(platform.python_implementation() == "PyPy", "ftp unreliable with PyPy") class TestAnonFTPFS(FSTestCases, unittest.TestCase): - user = "anonymous" pasw = "" diff --git a/tests/test_memoryfs.py b/tests/test_memoryfs.py index 31909f0f..bb5096f9 100644 --- a/tests/test_memoryfs.py +++ b/tests/test_memoryfs.py @@ -67,9 +67,22 @@ def test_close_mem_free(self): % (diff_close.size_diff / 1024.0), ) + def test_copy_preserve_time(self): + self.fs.makedir("foo") + self.fs.makedir("bar") + self.fs.touch("foo/file.txt") -class TestMemoryFile(unittest.TestCase): + namespaces = ("details", "modified") + src_info = self.fs.getinfo("foo/file.txt", namespaces) + + self.fs.copy("foo/file.txt", "bar/file.txt", preserve_time=True) + self.assertTrue(self.fs.exists("bar/file.txt")) + + dst_info = self.fs.getinfo("bar/file.txt", namespaces) + self.assertEqual(dst_info.modified, src_info.modified) + +class TestMemoryFile(unittest.TestCase): def setUp(self): self.fs = memoryfs.MemoryFS() diff --git a/tests/test_mirror.py b/tests/test_mirror.py index a0e8ac53..1cce3d59 100644 --- a/tests/test_mirror.py +++ b/tests/test_mirror.py @@ -2,15 +2,17 @@ import unittest +from parameterized import parameterized_class + from fs.mirror import mirror from fs import open_fs +@parameterized_class(("WORKERS",), [(0,), (1,), (2,), (4,)]) class TestMirror(unittest.TestCase): - WORKERS = 0 # Single threaded - def _contents(self, fs): """Extract an FS in to a simple data structure.""" + namespaces = ("details", "metadata_changed", "modified") contents = [] for path, dirs, files in fs.walk(): for info in dirs: @@ -18,7 +20,17 @@ def _contents(self, fs): contents.append((_path, "dir", b"")) for info in files: _path = info.make_path(path) - contents.append((_path, "file", fs.readbytes(_path))) + _bytes = fs.readbytes(_path) + _info = fs.getinfo(_path, namespaces) + contents.append( + ( + _path, + "file", + _bytes, + _info.modified, + _info.metadata_changed, + ) + ) return sorted(contents) def assert_compare_fs(self, fs1, fs2): @@ -28,14 +40,14 @@ def assert_compare_fs(self, fs1, fs2): def test_empty_mirror(self): m1 = open_fs("mem://") m2 = open_fs("mem://") - mirror(m1, m2, workers=self.WORKERS) + mirror(m1, m2, workers=self.WORKERS, preserve_time=True) self.assert_compare_fs(m1, m2) def test_mirror_one_file(self): m1 = open_fs("mem://") m1.writetext("foo", "hello") m2 = open_fs("mem://") - mirror(m1, m2, workers=self.WORKERS) + mirror(m1, m2, workers=self.WORKERS, preserve_time=True) self.assert_compare_fs(m1, m2) def test_mirror_one_file_one_dir(self): @@ -43,7 +55,7 @@ def test_mirror_one_file_one_dir(self): m1.writetext("foo", "hello") m1.makedir("bar") m2 = open_fs("mem://") - mirror(m1, m2, workers=self.WORKERS) + mirror(m1, m2, workers=self.WORKERS, preserve_time=True) self.assert_compare_fs(m1, m2) def test_mirror_delete_replace(self): @@ -51,13 +63,13 @@ def test_mirror_delete_replace(self): m1.writetext("foo", "hello") m1.makedir("bar") m2 = open_fs("mem://") - mirror(m1, m2, workers=self.WORKERS) + mirror(m1, m2, workers=self.WORKERS, preserve_time=True) self.assert_compare_fs(m1, m2) m2.remove("foo") - mirror(m1, m2, workers=self.WORKERS) + mirror(m1, m2, workers=self.WORKERS, preserve_time=True) self.assert_compare_fs(m1, m2) m2.removedir("bar") - mirror(m1, m2, workers=self.WORKERS) + mirror(m1, m2, workers=self.WORKERS, preserve_time=True) self.assert_compare_fs(m1, m2) def test_mirror_extra_dir(self): @@ -66,7 +78,7 @@ def test_mirror_extra_dir(self): m1.makedir("bar") m2 = open_fs("mem://") m2.makedir("baz") - mirror(m1, m2, workers=self.WORKERS) + mirror(m1, m2, workers=self.WORKERS, preserve_time=True) self.assert_compare_fs(m1, m2) def test_mirror_extra_file(self): @@ -76,7 +88,7 @@ def test_mirror_extra_file(self): m2 = open_fs("mem://") m2.makedir("baz") m2.touch("egg") - mirror(m1, m2, workers=self.WORKERS) + mirror(m1, m2, workers=self.WORKERS, preserve_time=True) self.assert_compare_fs(m1, m2) def test_mirror_wrong_type(self): @@ -86,7 +98,7 @@ def test_mirror_wrong_type(self): m2 = open_fs("mem://") m2.makedir("foo") m2.touch("bar") - mirror(m1, m2, workers=self.WORKERS) + mirror(m1, m2, workers=self.WORKERS, preserve_time=True) self.assert_compare_fs(m1, m2) def test_mirror_update(self): @@ -94,20 +106,8 @@ def test_mirror_update(self): m1.writetext("foo", "hello") m1.makedir("bar") m2 = open_fs("mem://") - mirror(m1, m2, workers=self.WORKERS) + mirror(m1, m2, workers=self.WORKERS, preserve_time=True) self.assert_compare_fs(m1, m2) m2.appendtext("foo", " world!") - mirror(m1, m2, workers=self.WORKERS) + mirror(m1, m2, workers=self.WORKERS, preserve_time=True) self.assert_compare_fs(m1, m2) - - -class TestMirrorWorkers1(TestMirror): - WORKERS = 1 - - -class TestMirrorWorkers2(TestMirror): - WORKERS = 2 - - -class TestMirrorWorkers4(TestMirror): - WORKERS = 4 diff --git a/tests/test_move.py b/tests/test_move.py index d87d2bd6..6b12b2b6 100644 --- a/tests/test_move.py +++ b/tests/test_move.py @@ -2,33 +2,61 @@ import unittest +from parameterized import parameterized_class + import fs.move from fs import open_fs +@parameterized_class(("preserve_time",), [(True,), (False,)]) class TestMove(unittest.TestCase): def test_move_fs(self): + namespaces = ("details", "modified") + src_fs = open_fs("mem://") src_fs.makedirs("foo/bar") src_fs.touch("test.txt") src_fs.touch("foo/bar/baz.txt") + src_file1_info = src_fs.getinfo("test.txt", namespaces) + src_file2_info = src_fs.getinfo("foo/bar/baz.txt", namespaces) dst_fs = open_fs("mem://") - fs.move.move_fs(src_fs, dst_fs) + dst_fs.create("test.txt") + dst_fs.setinfo("test.txt", {"details": {"modified": 1000000}}) + + fs.move.move_fs(src_fs, dst_fs, preserve_time=self.preserve_time) self.assertTrue(dst_fs.isdir("foo/bar")) self.assertTrue(dst_fs.isfile("test.txt")) + self.assertTrue(dst_fs.isfile("foo/bar/baz.txt")) self.assertTrue(src_fs.isempty("/")) - def test_copy_dir(self): + if self.preserve_time: + dst_file1_info = dst_fs.getinfo("test.txt", namespaces) + dst_file2_info = dst_fs.getinfo("foo/bar/baz.txt", namespaces) + self.assertEqual(dst_file1_info.modified, src_file1_info.modified) + self.assertEqual(dst_file2_info.modified, src_file2_info.modified) + + def test_move_dir(self): + namespaces = ("details", "modified") + src_fs = open_fs("mem://") src_fs.makedirs("foo/bar") src_fs.touch("test.txt") src_fs.touch("foo/bar/baz.txt") + src_file2_info = src_fs.getinfo("foo/bar/baz.txt", namespaces) dst_fs = open_fs("mem://") - fs.move.move_dir(src_fs, "/foo", dst_fs, "/") + dst_fs.create("test.txt") + dst_fs.setinfo("test.txt", {"details": {"modified": 1000000}}) + + fs.move.move_dir(src_fs, "/foo", dst_fs, "/", preserve_time=self.preserve_time) self.assertTrue(dst_fs.isdir("bar")) self.assertTrue(dst_fs.isfile("bar/baz.txt")) self.assertFalse(src_fs.exists("foo")) + self.assertTrue(src_fs.isfile("test.txt")) + + if self.preserve_time: + dst_file2_info = dst_fs.getinfo("bar/baz.txt", namespaces) + self.assertEqual(dst_file2_info.modified, src_file2_info.modified) diff --git a/tests/test_opener.py b/tests/test_opener.py index e7fae983..bc2f5cd7 100644 --- a/tests/test_opener.py +++ b/tests/test_opener.py @@ -300,14 +300,26 @@ def test_user_data_opener(self, app_dir): def test_open_ftp(self, mock_FTPFS): open_fs("ftp://foo:bar@ftp.example.org") mock_FTPFS.assert_called_once_with( - "ftp.example.org", passwd="bar", port=21, user="foo", proxy=None, timeout=10, tls=False + "ftp.example.org", + passwd="bar", + port=21, + user="foo", + proxy=None, + timeout=10, + tls=False, ) @mock.patch("fs.ftpfs.FTPFS") def test_open_ftps(self, mock_FTPFS): open_fs("ftps://foo:bar@ftp.example.org") mock_FTPFS.assert_called_once_with( - "ftp.example.org", passwd="bar", port=21, user="foo", proxy=None, timeout=10, tls=True + "ftp.example.org", + passwd="bar", + port=21, + user="foo", + proxy=None, + timeout=10, + tls=True, ) @mock.patch("fs.ftpfs.FTPFS") diff --git a/tests/test_osfs.py b/tests/test_osfs.py index 88879ec9..ff3af9e5 100644 --- a/tests/test_osfs.py +++ b/tests/test_osfs.py @@ -7,6 +7,7 @@ import shutil import tempfile import sys +import time import unittest import warnings @@ -96,6 +97,22 @@ def test_expand_vars(self): self.assertIn("TYRIONLANISTER", fs1.getsyspath("/")) self.assertNotIn("TYRIONLANISTER", fs2.getsyspath("/")) + def test_copy_preserve_time(self): + self.fs.makedir("foo") + self.fs.makedir("bar") + self.fs.create("foo/file.txt") + raw_info = {"details": {"modified": time.time() - 10000}} + self.fs.setinfo("foo/file.txt", raw_info) + + namespaces = ("details", "modified") + src_info = self.fs.getinfo("foo/file.txt", namespaces) + + self.fs.copy("foo/file.txt", "bar/file.txt", preserve_time=True) + self.assertTrue(self.fs.exists("bar/file.txt")) + + dst_info = self.fs.getinfo("bar/file.txt", namespaces) + self.assertEqual(dst_info.modified, src_info.modified) + @unittest.skipUnless(osfs.sendfile, "sendfile not supported") @unittest.skipIf( sys.version_info >= (3, 8), diff --git a/tests/test_wrap.py b/tests/test_wrap.py index 89a91187..e11ded4a 100644 --- a/tests/test_wrap.py +++ b/tests/test_wrap.py @@ -177,7 +177,7 @@ def test_scandir(self): ] with mock.patch.object(self.fs, "scandir", wraps=self.fs.scandir) as scandir: self.assertEqual(sorted(self.cached.scandir("/"), key=key), expected) - scandir.assert_has_calls([mock.call('/', namespaces=None, page=None)]) + scandir.assert_has_calls([mock.call("/", namespaces=None, page=None)]) with mock.patch.object(self.fs, "scandir", wraps=self.fs.scandir) as scandir: self.assertEqual(sorted(self.cached.scandir("/"), key=key), expected) scandir.assert_not_called() @@ -187,7 +187,7 @@ def test_isdir(self): self.assertTrue(self.cached.isdir("foo")) self.assertFalse(self.cached.isdir("egg")) # is file self.assertFalse(self.cached.isdir("spam")) # doesn't exist - scandir.assert_has_calls([mock.call('/', namespaces=None, page=None)]) + scandir.assert_has_calls([mock.call("/", namespaces=None, page=None)]) with mock.patch.object(self.fs, "scandir", wraps=self.fs.scandir) as scandir: self.assertTrue(self.cached.isdir("foo")) self.assertFalse(self.cached.isdir("egg")) @@ -199,7 +199,7 @@ def test_isfile(self): self.assertTrue(self.cached.isfile("egg")) self.assertFalse(self.cached.isfile("foo")) # is dir self.assertFalse(self.cached.isfile("spam")) # doesn't exist - scandir.assert_has_calls([mock.call('/', namespaces=None, page=None)]) + scandir.assert_has_calls([mock.call("/", namespaces=None, page=None)]) with mock.patch.object(self.fs, "scandir", wraps=self.fs.scandir) as scandir: self.assertTrue(self.cached.isfile("egg")) self.assertFalse(self.cached.isfile("foo")) @@ -211,7 +211,7 @@ def test_getinfo(self): self.assertEqual(self.cached.getinfo("foo"), self.fs.getinfo("foo")) self.assertEqual(self.cached.getinfo("/"), self.fs.getinfo("/")) self.assertNotFound(self.cached.getinfo, "spam") - scandir.assert_has_calls([mock.call('/', namespaces=None, page=None)]) + scandir.assert_has_calls([mock.call("/", namespaces=None, page=None)]) with mock.patch.object(self.fs, "scandir", wraps=self.fs.scandir) as scandir: self.assertEqual(self.cached.getinfo("foo"), self.fs.getinfo("foo")) self.assertEqual(self.cached.getinfo("/"), self.fs.getinfo("/"))