Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added `fs.copy.copy_file_if`, `fs.copy.copy_dir_if`, and `fs.copy.copy_fs_if`.
Closes [#458](https://github.com/PyFilesystem/pyfilesystem2/issues/458).

### Changed

- FTP servers that do not support the MLST command now try to use the MDTM command to
retrieve the last modification timestamp of a resource.
Closes [#456](https://github.com/PyFilesystem/pyfilesystem2/pull/456).

### Fixed

- Fixed performance bugs in `fs.copy.copy_dir_if_newer`. Test cases were adapted to catch those bugs in the future.
Expand Down
21 changes: 21 additions & 0 deletions fs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,25 @@ def readtext(

gettext = _new_name(readtext, "gettext")

def getmodified(self, path):
# type: (Text) -> Optional[datetime]
"""Get the timestamp of the last modifying access of a resource.

Arguments:
path (str): A path to a resource.

Returns:
datetime: The timestamp of the last modification.

The *modified timestamp* of a file is the point in time
that the file was last changed. Depending on the file system,
it might only have limited accuracy.

"""
if self.getmeta().get("supports_mtime", False):
return self.getinfo(path, namespaces=["modified"]).modified
return self.getinfo(path, namespaces=["details"]).modified

def getmeta(self, namespace="standard"):
# type: (Text) -> Mapping[Text, object]
"""Get meta information regarding a filesystem.
Expand Down Expand Up @@ -734,6 +753,8 @@ def getmeta(self, namespace="standard"):
read_only `True` if this filesystem is read only.
supports_rename `True` if this filesystem supports an
`os.rename` operation.
supports_mtime `True` if this filesystem supports a native
operation to retreive the "last modified" time.
=================== ============================================

Most builtin filesystems will provide all these keys, and third-
Expand Down
10 changes: 4 additions & 6 deletions fs/copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,9 +463,8 @@ def _copy_is_necessary(

elif condition == "newer":
try:
namespace = ("details",)
src_modified = src_fs.getinfo(src_path, namespace).modified
dst_modified = dst_fs.getinfo(dst_path, namespace).modified
src_modified = src_fs.getmodified(src_path)
dst_modified = dst_fs.getmodified(dst_path)
except ResourceNotFound:
return True
else:
Expand All @@ -477,9 +476,8 @@ def _copy_is_necessary(

elif condition == "older":
try:
namespace = ("details",)
src_modified = src_fs.getinfo(src_path, namespace).modified
dst_modified = dst_fs.getinfo(dst_path, namespace).modified
src_modified = src_fs.getmodified(src_path)
dst_modified = dst_fs.getmodified(dst_path)
except ResourceNotFound:
return True
else:
Expand Down
14 changes: 14 additions & 0 deletions fs/ftpfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from contextlib import contextmanager
from ftplib import FTP


try:
from ftplib import FTP_TLS
except ImportError as err:
Expand Down Expand Up @@ -667,6 +668,18 @@ def getinfo(self, path, namespaces=None):
}
)

if "modified" in namespaces:
if "basic" in namespaces or "details" in namespaces:
raise ValueError(
'Cannot use the "modified" namespace in combination with others.'
)
with self._lock:
with ftp_errors(self, path=path):
cmd = "MDTM " + _encode(self.validatepath(path), self.ftp.encoding)
response = self.ftp.sendcmd(cmd)
modified_info = {"modified": self._parse_ftp_time(response.split()[1])}
return Info({"modified": modified_info})

if self.supports_mlst:
with self._lock:
with ftp_errors(self, path=path):
Expand All @@ -692,6 +705,7 @@ def getmeta(self, namespace="standard"):
if namespace == "standard":
_meta = self._meta.copy()
_meta["unicode_paths"] = "UTF8" in self.features
_meta["supports_mtime"] = "MDTM" in self.features
return _meta

def listdir(self, path):
Expand Down
9 changes: 6 additions & 3 deletions fs/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,9 +317,12 @@ def modified(self):
namespace is not in the Info.

"""
self._require_namespace("details")
_time = self._make_datetime(self.get("details", "modified"))
return _time
try:
self._require_namespace("details")
return self._make_datetime(self.get("details", "modified"))
except MissingInfoNamespace:
self._require_namespace("modified")
return self._make_datetime(self.get("modified", "modified"))

@property
def created(self):
Expand Down
1 change: 1 addition & 0 deletions fs/osfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ def __init__(
"network": False,
"read_only": False,
"supports_rename": True,
"supports_mtime": False,
"thread_safe": True,
"unicode_paths": os.path.supports_unicode_filenames,
"virtual": False,
Expand Down
17 changes: 17 additions & 0 deletions tests/test_ftpfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,23 @@ def test_getmeta_unicode_path(self):
del self.fs.features["UTF8"]
self.assertFalse(self.fs.getmeta().get("unicode_paths"))

def test_getinfo_modified(self):
self.assertIn("MDTM", self.fs.features)
self.fs.create("bar")
mtime_detail = self.fs.getinfo("bar", ("basic", "details")).modified
mtime_modified = self.fs.getinfo("bar", ("modified",)).modified
# Microsecond and seconds might not actually be supported by all
# FTP commands, so we strip them before comparing if it looks
# like at least one of the two values does not contain them.
replacement = {}
if mtime_detail.microsecond == 0 or mtime_modified.microsecond == 0:
replacement["microsecond"] = 0
if mtime_detail.second == 0 or mtime_modified.second == 0:
replacement["second"] = 0
self.assertEqual(
mtime_detail.replace(**replacement), mtime_modified.replace(**replacement)
)

def test_opener_path(self):
self.fs.makedir("foo")
self.fs.writetext("foo/bar", "baz")
Expand Down
7 changes: 3 additions & 4 deletions tests/test_memoryfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,13 @@ def test_copy_preserve_time(self):
self.fs.makedir("bar")
self.fs.touch("foo/file.txt")

namespaces = ("details", "modified")
src_info = self.fs.getinfo("foo/file.txt", namespaces)
src_info = self.fs.getmodified("foo/file.txt")

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)
dst_info = self.fs.getmodified("bar/file.txt")
self.assertEqual(dst_info, src_info)


class TestMemoryFile(unittest.TestCase):
Expand Down