Skip to content

Commit 19e4387

Browse files
author
atollk
committed
Issue #438: Extended parsing compatibility of the FTP LIST command for Windows servers.
The parsing process now properly supports 24-hour time format, both with and without leading zeros.
1 parent c5d193b commit 19e4387

File tree

4 files changed

+60
-26
lines changed

4 files changed

+60
-26
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1212
- Missing `mode` attribute to `_MemoryFile` objects returned by `MemoryFS.openbin`.
1313
- Missing `readinto` method for `MemoryFS` and `FTPFS` file objects. Closes
1414
[#380](https://github.com/PyFilesystem/pyfilesystem2/issues/380).
15+
- Added compatibility if a Windows FTP server returns file information to the
16+
`LIST` command with 24-hour times. Closes [#438](https://github.com/PyFilesystem/pyfilesystem2/issues/438).
1517

1618
### Changed
1719

CONTRIBUTORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ Many thanks to the following developers for contributing to this project:
1313
- [Will McGugan](https://github.com/willmcgugan)
1414
- [Zmej Serow](https://github.com/zmej-serow)
1515
- [Morten Engelhardt Olsen](https://github.com/xoriath)
16+
- [Andreas Tollkötter](https://github.com/atollk)

fs/_ftp_parse.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,17 @@
4141
RE_WINDOWSNT = re.compile(
4242
r"""
4343
^
44-
(?P<modified>.*?(AM|PM))
45-
\s*
46-
(?P<size>(<DIR>|\d*))
47-
\s*
44+
(?P<modified_date>\S+)
45+
\s+
46+
(?P<modified_time>\S+(AM|PM)?)
47+
\s+
48+
(?P<size>(<DIR>|\d+))
49+
\s+
4850
(?P<name>.*)
4951
$
5052
""",
51-
re.VERBOSE)
53+
re.VERBOSE,
54+
)
5255

5356

5457
def get_decoders():
@@ -104,6 +107,10 @@ def _parse_time(t, formats):
104107
return epoch_time
105108

106109

110+
def _decode_linux_time(mtime):
111+
return _parse_time(mtime, formats=["%b %d %Y", "%b %d %H:%M"])
112+
113+
107114
def decode_linux(line, match):
108115
perms, links, uid, gid, size, mtime, name = match.groups()
109116
is_link = perms.startswith("l")
@@ -114,7 +121,7 @@ def decode_linux(line, match):
114121
_link_name = _link_name.strip()
115122
permissions = Permissions.parse(perms[1:])
116123

117-
mtime_epoch = _parse_time(mtime, formats=["%b %d %Y", "%b %d %H:%M"])
124+
mtime_epoch = _decode_linux_time(mtime)
118125

119126
name = unicodedata.normalize("NFC", name)
120127

@@ -138,12 +145,22 @@ def decode_linux(line, match):
138145
return raw_info
139146

140147

148+
def _decode_windowsnt_time(date, time):
149+
while len(time.split(":")[0]) < 2:
150+
time = "0" + time
151+
return _parse_time(
152+
date + " " + time, formats=["%d-%m-%y %I:%M%p", "%d-%m-%y %H:%M"]
153+
)
154+
155+
141156
def decode_windowsnt(line, match):
142157
"""
143-
Decodes a Windows NT FTP LIST line like these two:
158+
Decodes a Windows NT FTP LIST line like one of these:
144159
145160
`11-02-18 02:12PM <DIR> images`
146161
`11-02-18 03:33PM 9276 logo.gif`
162+
163+
Alternatively, the time (02:12PM) might also be present in 24-hour format (14:12).
147164
"""
148165
is_dir = match.group("size") == "<DIR>"
149166

@@ -161,7 +178,9 @@ def decode_windowsnt(line, match):
161178
if not is_dir:
162179
raw_info["details"]["size"] = int(match.group("size"))
163180

164-
modified = _parse_time(match.group("modified"), formats=["%d-%m-%y %I:%M%p"])
181+
modified = _decode_windowsnt_time(
182+
match.group("modified_date"), match.group("modified_time")
183+
)
165184
if modified is not None:
166185
raw_info["details"]["modified"] = modified
167186

tests/test_ftp_parse.py

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,18 @@ class TestFTPParse(unittest.TestCase):
1717
@mock.patch("time.localtime")
1818
def test_parse_time(self, mock_localtime):
1919
self.assertEqual(
20-
ftp_parse._parse_time("JUL 05 1974", formats=["%b %d %Y"]),
21-
142214400.0)
20+
ftp_parse._parse_time("JUL 05 1974", formats=["%b %d %Y"]), 142214400.0
21+
)
2222

2323
mock_localtime.return_value = time2017
2424
self.assertEqual(
25-
ftp_parse._parse_time("JUL 05 02:00", formats=["%b %d %H:%M"]),
26-
1499220000.0)
25+
ftp_parse._parse_time("JUL 05 02:00", formats=["%b %d %H:%M"]), 1499220000.0
26+
)
2727

2828
self.assertEqual(
2929
ftp_parse._parse_time("05-07-17 02:00AM", formats=["%d-%m-%y %I:%M%p"]),
30-
1499220000.0)
30+
1499220000.0,
31+
)
3132

3233
self.assertEqual(ftp_parse._parse_time("notadate", formats=["%b %d %Y"]), None)
3334

@@ -164,39 +165,50 @@ def test_decode_linux(self, mock_localtime):
164165
def test_decode_windowsnt(self, mock_localtime):
165166
mock_localtime.return_value = time2017
166167
directory = """\
168+
unparsable line
167169
11-02-17 02:00AM <DIR> docs
168170
11-02-17 02:12PM <DIR> images
169-
11-02-17 02:12PM <DIR> AM to PM
171+
11-02-17 02:12PM <DIR> AM to PM
170172
11-02-17 03:33PM 9276 logo.gif
173+
05-11-20 22:11 <DIR> src
174+
11-02-17 01:23 1 12
175+
11-02-17 4:54 0 icon.bmp
171176
"""
172177
expected = [
173178
{
174179
"basic": {"is_dir": True, "name": "docs"},
175180
"details": {"modified": 1486778400.0, "type": 1},
176-
"ftp": {
177-
"ls": "11-02-17 02:00AM <DIR> docs"
178-
},
181+
"ftp": {"ls": "11-02-17 02:00AM <DIR> docs"},
179182
},
180183
{
181184
"basic": {"is_dir": True, "name": "images"},
182185
"details": {"modified": 1486822320.0, "type": 1},
183-
"ftp": {
184-
"ls": "11-02-17 02:12PM <DIR> images"
185-
},
186+
"ftp": {"ls": "11-02-17 02:12PM <DIR> images"},
186187
},
187188
{
188189
"basic": {"is_dir": True, "name": "AM to PM"},
189190
"details": {"modified": 1486822320.0, "type": 1},
190-
"ftp": {
191-
"ls": "11-02-17 02:12PM <DIR> AM to PM"
192-
},
191+
"ftp": {"ls": "11-02-17 02:12PM <DIR> AM to PM"},
193192
},
194193
{
195194
"basic": {"is_dir": False, "name": "logo.gif"},
196195
"details": {"modified": 1486827180.0, "size": 9276, "type": 2},
197-
"ftp": {
198-
"ls": "11-02-17 03:33PM 9276 logo.gif"
199-
},
196+
"ftp": {"ls": "11-02-17 03:33PM 9276 logo.gif"},
197+
},
198+
{
199+
"basic": {"is_dir": True, "name": "src"},
200+
"details": {"modified": 1604614260.0, "type": 1},
201+
"ftp": {"ls": "05-11-20 22:11 <DIR> src"},
202+
},
203+
{
204+
"basic": {"is_dir": False, "name": "12"},
205+
"details": {"modified": 1486776180.0, "size": 1, "type": 2},
206+
"ftp": {"ls": "11-02-17 01:23 1 12"},
207+
},
208+
{
209+
"basic": {"is_dir": False, "name": "icon.bmp"},
210+
"details": {"modified": 1486788840.0, "size": 0, "type": 2},
211+
"ftp": {"ls": "11-02-17 4:54 0 icon.bmp"},
200212
},
201213
]
202214

0 commit comments

Comments
 (0)