Skip to content

Commit 515ca26

Browse files
committed
FTPFS: Add FTPS support.
Try to import FTP_TLS while still supporting older Python versions, add a 'tls' option to the FTPFS class, and show the 'ftps://' scheme in the repr when enabled.
1 parent 8784fd6 commit 515ca26

File tree

1 file changed

+42
-3
lines changed

1 file changed

+42
-3
lines changed

fs/ftpfs.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
from collections import OrderedDict
1515
from contextlib import contextmanager
1616
from ftplib import FTP
17+
18+
try:
19+
from ftplib import FTP_TLS
20+
except ImportError:
21+
FTP_TLS = None
1722
from ftplib import error_perm
1823
from ftplib import error_temp
1924
from typing import cast
@@ -346,7 +351,30 @@ def seek(self, pos, whence=Seek.set):
346351

347352

348353
class FTPFS(FS):
349-
"""A FTP (File Transport Protocol) Filesystem."""
354+
"""A FTP (File Transport Protocol) Filesystem.
355+
356+
Optionally, the connection can be made securely via TLS. This is known as
357+
FTPS, or FTP Secure. TLS will be enabled when using the ftps:// protocol,
358+
or when setting the `tls` argument to True in the constructor.
359+
360+
361+
Examples:
362+
Create with the constructor::
363+
364+
>>> from fs.ftpfs import FTPFS
365+
>>> ftp_fs = FTPFS()
366+
367+
Or via an FS URL::
368+
369+
>>> import fs
370+
>>> ftp_fs = fs.open_fs('ftp://')
371+
372+
Or via an FS URL, using TLS::
373+
374+
>>> import fs
375+
>>> ftp_fs = fs.open_fs('ftps://')
376+
377+
"""
350378

351379
_meta = {
352380
"invalid_path_chars": "\0",
@@ -366,6 +394,7 @@ def __init__(
366394
timeout=10, # type: int
367395
port=21, # type: int
368396
proxy=None, # type: Optional[Text]
397+
tls=False, # type: bool
369398
):
370399
# type: (...) -> None
371400
"""Create a new `FTPFS` instance.
@@ -380,6 +409,7 @@ def __init__(
380409
port (int): FTP port number (default 21).
381410
proxy (str, optional): An FTP proxy, or ``None`` (default)
382411
for no proxy.
412+
tls (bool): Attempt to use FTP over TLS (FTPS) (default: False)
383413
384414
"""
385415
super(FTPFS, self).__init__()
@@ -390,6 +420,7 @@ def __init__(
390420
self.timeout = timeout
391421
self.port = port
392422
self.proxy = proxy
423+
self.tls = tls
393424

394425
self.encoding = "latin-1"
395426
self._ftp = None # type: Optional[FTP]
@@ -432,11 +463,17 @@ def _parse_features(cls, feat_response):
432463
def _open_ftp(self):
433464
# type: () -> FTP
434465
"""Open a new ftp object."""
435-
_ftp = FTP()
466+
if self.tls and FTP_TLS:
467+
_ftp = FTP_TLS()
468+
else:
469+
self.tls = False
470+
_ftp = FTP()
436471
_ftp.set_debuglevel(0)
437472
with ftp_errors(self):
438473
_ftp.connect(self.host, self.port, self.timeout)
439474
_ftp.login(self.user, self.passwd, self.acct)
475+
if self.tls:
476+
_ftp.prot_p()
440477
self._features = {}
441478
try:
442479
feat_response = _decode(_ftp.sendcmd("FEAT"), "latin-1")
@@ -471,7 +508,9 @@ def ftp_url(self):
471508
_user_part = ""
472509
else:
473510
_user_part = "{}:{}@".format(self.user, self.passwd)
474-
url = "ftp://{}{}".format(_user_part, _host_part)
511+
512+
scheme = "ftps" if self.tls else "ftp"
513+
url = "{}://{}{}".format(scheme, _user_part, _host_part)
475514
return url
476515

477516
@property

0 commit comments

Comments
 (0)