Skip to content

Commit 29354ef

Browse files
author
atollk
committed
Added support for "last modified time" in getinfo.
A new namespace "modified" was introduced to the `getinfo` function to potentially use cheaper commands compared to the general "details" namespace. In particular, `FTPFS` now uses the MDTM command if supported by the server.
1 parent ac8a91a commit 29354ef

File tree

8 files changed

+72
-13
lines changed

8 files changed

+72
-13
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1313
- Added `fs.copy.copy_file_if`, `fs.copy.copy_dir_if`, and `fs.copy.copy_fs_if`.
1414
Closes [#458](https://github.com/PyFilesystem/pyfilesystem2/issues/458).
1515

16+
### Changed
17+
18+
- FTP servers that do not support the MLST command now try to use the MDTM command to
19+
retrieve the last modification timestamp of a resource.
20+
Closes [#456](https://github.com/PyFilesystem/pyfilesystem2/pull/456).
21+
1622
### Fixed
1723

1824
- Fixed performance bugs in `fs.copy.copy_dir_if_newer`. Test cases were adapted to catch those bugs in the future.

fs/base.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,25 @@ def readtext(
697697

698698
gettext = _new_name(readtext, "gettext")
699699

700+
def getmodified(self, path):
701+
# type: (Text) -> Optional[datetime]
702+
"""Get the timestamp of the last modifying access of a resource.
703+
704+
Arguments:
705+
path (str): A path to a resource.
706+
707+
Returns:
708+
datetime: The timestamp of the last modification.
709+
710+
The *modified timestamp* of a file is the point in time
711+
that the file was last changed. Depending on the file system,
712+
it might only have limited accuracy.
713+
714+
"""
715+
if self.getmeta().get("supports_mtime", False):
716+
return self.getinfo(path, namespaces=["modified"]).modified
717+
return self.getinfo(path, namespaces=["details"]).modified
718+
700719
def getmeta(self, namespace="standard"):
701720
# type: (Text) -> Mapping[Text, object]
702721
"""Get meta information regarding a filesystem.
@@ -734,6 +753,8 @@ def getmeta(self, namespace="standard"):
734753
read_only `True` if this filesystem is read only.
735754
supports_rename `True` if this filesystem supports an
736755
`os.rename` operation.
756+
supports_mtime `True` if this filesystem supports a native
757+
operation to retreive the "last modified" time.
737758
=================== ============================================
738759
739760
Most builtin filesystems will provide all these keys, and third-

fs/copy.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -463,9 +463,8 @@ def _copy_is_necessary(
463463

464464
elif condition == "newer":
465465
try:
466-
namespace = ("details",)
467-
src_modified = src_fs.getinfo(src_path, namespace).modified
468-
dst_modified = dst_fs.getinfo(dst_path, namespace).modified
466+
src_modified = src_fs.getmodified(src_path)
467+
dst_modified = dst_fs.getmodified(dst_path)
469468
except ResourceNotFound:
470469
return True
471470
else:
@@ -477,9 +476,8 @@ def _copy_is_necessary(
477476

478477
elif condition == "older":
479478
try:
480-
namespace = ("details",)
481-
src_modified = src_fs.getinfo(src_path, namespace).modified
482-
dst_modified = dst_fs.getinfo(dst_path, namespace).modified
479+
src_modified = src_fs.getmodified(src_path)
480+
dst_modified = dst_fs.getmodified(dst_path)
483481
except ResourceNotFound:
484482
return True
485483
else:

fs/ftpfs.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from contextlib import contextmanager
1717
from ftplib import FTP
1818

19+
1920
try:
2021
from ftplib import FTP_TLS
2122
except ImportError as err:
@@ -667,6 +668,18 @@ def getinfo(self, path, namespaces=None):
667668
}
668669
)
669670

671+
if "modified" in namespaces:
672+
if "basic" in namespaces or "details" in namespaces:
673+
raise ValueError(
674+
'Cannot use the "modified" namespace in combination with others.'
675+
)
676+
with self._lock:
677+
with ftp_errors(self, path=path):
678+
cmd = "MDTM " + _encode(self.validatepath(path), self.ftp.encoding)
679+
response = self.ftp.sendcmd(cmd)
680+
modified_info = {"modified": self._parse_ftp_time(response.split()[1])}
681+
return Info({"modified": modified_info})
682+
670683
if self.supports_mlst:
671684
with self._lock:
672685
with ftp_errors(self, path=path):
@@ -692,6 +705,7 @@ def getmeta(self, namespace="standard"):
692705
if namespace == "standard":
693706
_meta = self._meta.copy()
694707
_meta["unicode_paths"] = "UTF8" in self.features
708+
_meta["supports_mtime"] = "MDTM" in self.features
695709
return _meta
696710

697711
def listdir(self, path):

fs/info.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -317,9 +317,12 @@ def modified(self):
317317
namespace is not in the Info.
318318
319319
"""
320-
self._require_namespace("details")
321-
_time = self._make_datetime(self.get("details", "modified"))
322-
return _time
320+
try:
321+
self._require_namespace("details")
322+
return self._make_datetime(self.get("details", "modified"))
323+
except MissingInfoNamespace:
324+
self._require_namespace("modified")
325+
return self._make_datetime(self.get("modified", "modified"))
323326

324327
@property
325328
def created(self):

fs/osfs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ def __init__(
145145
"network": False,
146146
"read_only": False,
147147
"supports_rename": True,
148+
"supports_mtime": False,
148149
"thread_safe": True,
149150
"unicode_paths": os.path.supports_unicode_filenames,
150151
"virtual": False,

tests/test_ftpfs.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,23 @@ def test_getmeta_unicode_path(self):
261261
del self.fs.features["UTF8"]
262262
self.assertFalse(self.fs.getmeta().get("unicode_paths"))
263263

264+
def test_getinfo_modified(self):
265+
self.assertIn("MDTM", self.fs.features)
266+
self.fs.create("bar")
267+
mtime_detail = self.fs.getinfo("bar", ("basic", "details")).modified
268+
mtime_modified = self.fs.getinfo("bar", ("modified",)).modified
269+
# Microsecond and seconds might not actually be supported by all
270+
# FTP commands, so we strip them before comparing if it looks
271+
# like at least one of the two values does not contain them.
272+
replacement = {}
273+
if mtime_detail.microsecond == 0 or mtime_modified.microsecond == 0:
274+
replacement["microsecond"] = 0
275+
if mtime_detail.second == 0 or mtime_modified.second == 0:
276+
replacement["second"] = 0
277+
self.assertEqual(
278+
mtime_detail.replace(**replacement), mtime_modified.replace(**replacement)
279+
)
280+
264281
def test_opener_path(self):
265282
self.fs.makedir("foo")
266283
self.fs.writetext("foo/bar", "baz")

tests/test_memoryfs.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,13 @@ def test_copy_preserve_time(self):
7272
self.fs.makedir("bar")
7373
self.fs.touch("foo/file.txt")
7474

75-
namespaces = ("details", "modified")
76-
src_info = self.fs.getinfo("foo/file.txt", namespaces)
75+
src_info = self.fs.getmodified("foo/file.txt")
7776

7877
self.fs.copy("foo/file.txt", "bar/file.txt", preserve_time=True)
7978
self.assertTrue(self.fs.exists("bar/file.txt"))
8079

81-
dst_info = self.fs.getinfo("bar/file.txt", namespaces)
82-
self.assertEqual(dst_info.modified, src_info.modified)
80+
dst_info = self.fs.getmodified("bar/file.txt")
81+
self.assertEqual(dst_info, src_info)
8382

8483

8584
class TestMemoryFile(unittest.TestCase):

0 commit comments

Comments
 (0)