diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b965c9c..9e0a7680 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Missing `mode` attribute to `_MemoryFile` objects returned by `MemoryFS.openbin`. - Missing `readinto` method for `MemoryFS` and `FTPFS` file objects. Closes [#380](https://github.com/PyFilesystem/pyfilesystem2/issues/380). +- Added compatibility if a Windows FTP server returns file information to the + `LIST` command with 24-hour times. Closes [#438](https://github.com/PyFilesystem/pyfilesystem2/issues/438). ### Changed diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ba15fd73..5df27157 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -2,6 +2,7 @@ Many thanks to the following developers for contributing to this project: +- [Andreas Tollkötter](https://github.com/atollk) - [C. W.](https://github.com/chfw) - [Diego Argueta](https://github.com/dargueta) - [Geoff Jukes](https://github.com/geoffjukes) @@ -9,7 +10,7 @@ Many thanks to the following developers for contributing to this project: - [Justin Charlong](https://github.com/jcharlong) - [Louis Sautier](https://github.com/sbraz) - [Martin Larralde](https://github.com/althonos) +- [Morten Engelhardt Olsen](https://github.com/xoriath) - [Nick Henderson](https://github.com/nwh) - [Will McGugan](https://github.com/willmcgugan) - [Zmej Serow](https://github.com/zmej-serow) -- [Morten Engelhardt Olsen](https://github.com/xoriath) diff --git a/fs/_ftp_parse.py b/fs/_ftp_parse.py index b503d737..9e15a265 100644 --- a/fs/_ftp_parse.py +++ b/fs/_ftp_parse.py @@ -41,14 +41,17 @@ RE_WINDOWSNT = re.compile( r""" ^ - (?P.*?(AM|PM)) - \s* - (?P(|\d*)) - \s* + (?P\S+) + \s+ + (?P\S+(AM|PM)?) + \s+ + (?P(|\d+)) + \s+ (?P.*) $ """, - re.VERBOSE) + re.VERBOSE, +) def get_decoders(): @@ -82,15 +85,13 @@ def parse_line(line): def _parse_time(t, formats): - t = " ".join(token.strip() for token in t.lower().split(" ")) - - _t = None for frmt in formats: try: _t = time.strptime(t, frmt) + break except ValueError: continue - if not _t: + else: return None year = _t.tm_year if _t.tm_year != 1900 else time.localtime().tm_year @@ -104,6 +105,10 @@ def _parse_time(t, formats): return epoch_time +def _decode_linux_time(mtime): + return _parse_time(mtime, formats=["%b %d %Y", "%b %d %H:%M"]) + + def decode_linux(line, match): perms, links, uid, gid, size, mtime, name = match.groups() is_link = perms.startswith("l") @@ -114,7 +119,7 @@ def decode_linux(line, match): _link_name = _link_name.strip() permissions = Permissions.parse(perms[1:]) - mtime_epoch = _parse_time(mtime, formats=["%b %d %Y", "%b %d %H:%M"]) + mtime_epoch = _decode_linux_time(mtime) name = unicodedata.normalize("NFC", name) @@ -138,12 +143,18 @@ def decode_linux(line, match): return raw_info +def _decode_windowsnt_time(mtime): + return _parse_time(mtime, formats=["%d-%m-%y %I:%M%p", "%d-%m-%y %H:%M"]) + + def decode_windowsnt(line, match): """ - Decodes a Windows NT FTP LIST line like these two: + Decodes a Windows NT FTP LIST line like one of these: `11-02-18 02:12PM images` `11-02-18 03:33PM 9276 logo.gif` + + Alternatively, the time (02:12PM) might also be present in 24-hour format (14:12). """ is_dir = match.group("size") == "" @@ -161,7 +172,9 @@ def decode_windowsnt(line, match): if not is_dir: raw_info["details"]["size"] = int(match.group("size")) - modified = _parse_time(match.group("modified"), formats=["%d-%m-%y %I:%M%p"]) + modified = _decode_windowsnt_time( + match.group("modified_date") + " " + match.group("modified_time") + ) if modified is not None: raw_info["details"]["modified"] = modified diff --git a/tests/test_ftp_parse.py b/tests/test_ftp_parse.py index d0abc05a..b9a69cf1 100644 --- a/tests/test_ftp_parse.py +++ b/tests/test_ftp_parse.py @@ -17,17 +17,18 @@ class TestFTPParse(unittest.TestCase): @mock.patch("time.localtime") def test_parse_time(self, mock_localtime): self.assertEqual( - ftp_parse._parse_time("JUL 05 1974", formats=["%b %d %Y"]), - 142214400.0) + ftp_parse._parse_time("JUL 05 1974", formats=["%b %d %Y"]), 142214400.0 + ) mock_localtime.return_value = time2017 self.assertEqual( - ftp_parse._parse_time("JUL 05 02:00", formats=["%b %d %H:%M"]), - 1499220000.0) + ftp_parse._parse_time("JUL 05 02:00", formats=["%b %d %H:%M"]), 1499220000.0 + ) self.assertEqual( ftp_parse._parse_time("05-07-17 02:00AM", formats=["%d-%m-%y %I:%M%p"]), - 1499220000.0) + 1499220000.0, + ) self.assertEqual(ftp_parse._parse_time("notadate", formats=["%b %d %Y"]), None) @@ -164,39 +165,68 @@ def test_decode_linux(self, mock_localtime): def test_decode_windowsnt(self, mock_localtime): mock_localtime.return_value = time2017 directory = """\ +unparsable line 11-02-17 02:00AM docs 11-02-17 02:12PM images -11-02-17 02:12PM AM to PM +11-02-17 02:12PM AM to PM 11-02-17 03:33PM 9276 logo.gif +05-11-20 22:11 src +11-02-17 01:23 1 12 +11-02-17 4:54 0 icon.bmp +11-02-17 4:54AM 0 icon.gif +11-02-17 4:54PM 0 icon.png +11-02-17 16:54 0 icon.jpg """ expected = [ { "basic": {"is_dir": True, "name": "docs"}, "details": {"modified": 1486778400.0, "type": 1}, - "ftp": { - "ls": "11-02-17 02:00AM docs" - }, + "ftp": {"ls": "11-02-17 02:00AM docs"}, }, { "basic": {"is_dir": True, "name": "images"}, "details": {"modified": 1486822320.0, "type": 1}, - "ftp": { - "ls": "11-02-17 02:12PM images" - }, + "ftp": {"ls": "11-02-17 02:12PM images"}, }, { "basic": {"is_dir": True, "name": "AM to PM"}, "details": {"modified": 1486822320.0, "type": 1}, - "ftp": { - "ls": "11-02-17 02:12PM AM to PM" - }, + "ftp": {"ls": "11-02-17 02:12PM AM to PM"}, }, { "basic": {"is_dir": False, "name": "logo.gif"}, "details": {"modified": 1486827180.0, "size": 9276, "type": 2}, - "ftp": { - "ls": "11-02-17 03:33PM 9276 logo.gif" - }, + "ftp": {"ls": "11-02-17 03:33PM 9276 logo.gif"}, + }, + { + "basic": {"is_dir": True, "name": "src"}, + "details": {"modified": 1604614260.0, "type": 1}, + "ftp": {"ls": "05-11-20 22:11 src"}, + }, + { + "basic": {"is_dir": False, "name": "12"}, + "details": {"modified": 1486776180.0, "size": 1, "type": 2}, + "ftp": {"ls": "11-02-17 01:23 1 12"}, + }, + { + "basic": {"is_dir": False, "name": "icon.bmp"}, + "details": {"modified": 1486788840.0, "size": 0, "type": 2}, + "ftp": {"ls": "11-02-17 4:54 0 icon.bmp"}, + }, + { + "basic": {"is_dir": False, "name": "icon.gif"}, + "details": {"modified": 1486788840.0, "size": 0, "type": 2}, + "ftp": {"ls": "11-02-17 4:54AM 0 icon.gif"}, + }, + { + "basic": {"is_dir": False, "name": "icon.png"}, + "details": {"modified": 1486832040.0, "size": 0, "type": 2}, + "ftp": {"ls": "11-02-17 4:54PM 0 icon.png"}, + }, + { + "basic": {"is_dir": False, "name": "icon.jpg"}, + "details": {"modified": 1486832040.0, "size": 0, "type": 2}, + "ftp": {"ls": "11-02-17 16:54 0 icon.jpg"}, }, ]