Skip to content

Commit a16d165

Browse files
authored
Merge pull request #439 from atollk/master
Parsing compatibility of the FTP `LIST` command for Windows servers
2 parents c5d193b + 1fa1f27 commit a16d165

File tree

4 files changed

+77
-31
lines changed

4 files changed

+77
-31
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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
Many thanks to the following developers for contributing to this project:
44

5+
- [Andreas Tollkötter](https://github.com/atollk)
56
- [C. W.](https://github.com/chfw)
67
- [Diego Argueta](https://github.com/dargueta)
78
- [Geoff Jukes](https://github.com/geoffjukes)
89
- [Giampaolo](https://github.com/gpcimino)
910
- [Justin Charlong](https://github.com/jcharlong)
1011
- [Louis Sautier](https://github.com/sbraz)
1112
- [Martin Larralde](https://github.com/althonos)
13+
- [Morten Engelhardt Olsen](https://github.com/xoriath)
1214
- [Nick Henderson](https://github.com/nwh)
1315
- [Will McGugan](https://github.com/willmcgugan)
1416
- [Zmej Serow](https://github.com/zmej-serow)
15-
- [Morten Engelhardt Olsen](https://github.com/xoriath)

fs/_ftp_parse.py

Lines changed: 25 additions & 12 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():
@@ -82,15 +85,13 @@ def parse_line(line):
8285

8386

8487
def _parse_time(t, formats):
85-
t = " ".join(token.strip() for token in t.lower().split(" "))
86-
87-
_t = None
8888
for frmt in formats:
8989
try:
9090
_t = time.strptime(t, frmt)
91+
break
9192
except ValueError:
9293
continue
93-
if not _t:
94+
else:
9495
return None
9596

9697
year = _t.tm_year if _t.tm_year != 1900 else time.localtime().tm_year
@@ -104,6 +105,10 @@ def _parse_time(t, formats):
104105
return epoch_time
105106

106107

108+
def _decode_linux_time(mtime):
109+
return _parse_time(mtime, formats=["%b %d %Y", "%b %d %H:%M"])
110+
111+
107112
def decode_linux(line, match):
108113
perms, links, uid, gid, size, mtime, name = match.groups()
109114
is_link = perms.startswith("l")
@@ -114,7 +119,7 @@ def decode_linux(line, match):
114119
_link_name = _link_name.strip()
115120
permissions = Permissions.parse(perms[1:])
116121

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

119124
name = unicodedata.normalize("NFC", name)
120125

@@ -138,12 +143,18 @@ def decode_linux(line, match):
138143
return raw_info
139144

140145

146+
def _decode_windowsnt_time(mtime):
147+
return _parse_time(mtime, formats=["%d-%m-%y %I:%M%p", "%d-%m-%y %H:%M"])
148+
149+
141150
def decode_windowsnt(line, match):
142151
"""
143-
Decodes a Windows NT FTP LIST line like these two:
152+
Decodes a Windows NT FTP LIST line like one of these:
144153
145154
`11-02-18 02:12PM <DIR> images`
146155
`11-02-18 03:33PM 9276 logo.gif`
156+
157+
Alternatively, the time (02:12PM) might also be present in 24-hour format (14:12).
147158
"""
148159
is_dir = match.group("size") == "<DIR>"
149160

@@ -161,7 +172,9 @@ def decode_windowsnt(line, match):
161172
if not is_dir:
162173
raw_info["details"]["size"] = int(match.group("size"))
163174

164-
modified = _parse_time(match.group("modified"), formats=["%d-%m-%y %I:%M%p"])
175+
modified = _decode_windowsnt_time(
176+
match.group("modified_date") + " " + match.group("modified_time")
177+
)
165178
if modified is not None:
166179
raw_info["details"]["modified"] = modified
167180

tests/test_ftp_parse.py

Lines changed: 48 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,68 @@ 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
176+
11-02-17 4:54AM 0 icon.gif
177+
11-02-17 4:54PM 0 icon.png
178+
11-02-17 16:54 0 icon.jpg
171179
"""
172180
expected = [
173181
{
174182
"basic": {"is_dir": True, "name": "docs"},
175183
"details": {"modified": 1486778400.0, "type": 1},
176-
"ftp": {
177-
"ls": "11-02-17 02:00AM <DIR> docs"
178-
},
184+
"ftp": {"ls": "11-02-17 02:00AM <DIR> docs"},
179185
},
180186
{
181187
"basic": {"is_dir": True, "name": "images"},
182188
"details": {"modified": 1486822320.0, "type": 1},
183-
"ftp": {
184-
"ls": "11-02-17 02:12PM <DIR> images"
185-
},
189+
"ftp": {"ls": "11-02-17 02:12PM <DIR> images"},
186190
},
187191
{
188192
"basic": {"is_dir": True, "name": "AM to PM"},
189193
"details": {"modified": 1486822320.0, "type": 1},
190-
"ftp": {
191-
"ls": "11-02-17 02:12PM <DIR> AM to PM"
192-
},
194+
"ftp": {"ls": "11-02-17 02:12PM <DIR> AM to PM"},
193195
},
194196
{
195197
"basic": {"is_dir": False, "name": "logo.gif"},
196198
"details": {"modified": 1486827180.0, "size": 9276, "type": 2},
197-
"ftp": {
198-
"ls": "11-02-17 03:33PM 9276 logo.gif"
199-
},
199+
"ftp": {"ls": "11-02-17 03:33PM 9276 logo.gif"},
200+
},
201+
{
202+
"basic": {"is_dir": True, "name": "src"},
203+
"details": {"modified": 1604614260.0, "type": 1},
204+
"ftp": {"ls": "05-11-20 22:11 <DIR> src"},
205+
},
206+
{
207+
"basic": {"is_dir": False, "name": "12"},
208+
"details": {"modified": 1486776180.0, "size": 1, "type": 2},
209+
"ftp": {"ls": "11-02-17 01:23 1 12"},
210+
},
211+
{
212+
"basic": {"is_dir": False, "name": "icon.bmp"},
213+
"details": {"modified": 1486788840.0, "size": 0, "type": 2},
214+
"ftp": {"ls": "11-02-17 4:54 0 icon.bmp"},
215+
},
216+
{
217+
"basic": {"is_dir": False, "name": "icon.gif"},
218+
"details": {"modified": 1486788840.0, "size": 0, "type": 2},
219+
"ftp": {"ls": "11-02-17 4:54AM 0 icon.gif"},
220+
},
221+
{
222+
"basic": {"is_dir": False, "name": "icon.png"},
223+
"details": {"modified": 1486832040.0, "size": 0, "type": 2},
224+
"ftp": {"ls": "11-02-17 4:54PM 0 icon.png"},
225+
},
226+
{
227+
"basic": {"is_dir": False, "name": "icon.jpg"},
228+
"details": {"modified": 1486832040.0, "size": 0, "type": 2},
229+
"ftp": {"ls": "11-02-17 16:54 0 icon.jpg"},
200230
},
201231
]
202232

0 commit comments

Comments
 (0)