1414from collections import OrderedDict
1515from contextlib import contextmanager
1616from ftplib import FTP
17+
18+ try :
19+ from ftplib import FTP_TLS
20+ except ImportError :
21+ FTP_TLS = None
1722from ftplib import error_perm
1823from ftplib import error_temp
1924from typing import cast
@@ -346,7 +351,30 @@ def seek(self, pos, whence=Seek.set):
346351
347352
348353class 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