13
13
14
14
from contextlib import contextmanager
15
15
from ftplib import FTP
16
- from ftplib import error_reply
17
16
from ftplib import error_perm
18
17
from ftplib import error_temp
19
18
@@ -47,16 +46,18 @@ def ftp_errors(fs, path=None):
47
46
raise errors .RemoteConnectionError (
48
47
msg = 'unable to connect to {}' .format (fs .host )
49
48
)
50
- except error_temp as e :
49
+ except error_temp as error :
51
50
if path is not None :
52
51
raise errors .ResourceError (
53
52
path ,
54
- msg = "ftp error on resource '{}' ({})" .format (path , e )
53
+ msg = "ftp error on resource '{}' ({})" .format (path , error )
55
54
)
56
55
else :
57
- raise errors .OperationFailed (msg = 'ftp error ({})' .format (e ))
58
- except error_perm as e :
59
- code , message = parse_ftp_error (e )
56
+ raise errors .OperationFailed (
57
+ msg = 'ftp error ({})' .format (error )
58
+ )
59
+ except error_perm as error :
60
+ code , message = _parse_ftp_error (error )
60
61
if code == 552 :
61
62
raise errors .InsufficientStorage (
62
63
path = path ,
@@ -79,8 +80,10 @@ def manage_ftp(ftp):
79
80
except : # pragma: nocover
80
81
pass
81
82
82
- def parse_ftp_error (e ):
83
- code , _ , message = text_type (e ).partition (' ' )
83
+
84
+ def _parse_ftp_error (error ):
85
+ """Extract code and message from ftp error."""
86
+ code , _ , message = text_type (error ).partition (' ' )
84
87
if code .isdigit ():
85
88
code = int (code )
86
89
return code , message
@@ -101,6 +104,7 @@ def _decode(st, _):
101
104
class FTPFile (io .IOBase ):
102
105
103
106
def __init__ (self , ftpfs , path , mode ):
107
+ super (FTPFile , self ).__init__ ()
104
108
self .fs = ftpfs
105
109
self .path = path
106
110
self .mode = Mode (mode )
@@ -111,7 +115,8 @@ def __init__(self, ftpfs, path, mode):
111
115
self ._write_conn = None
112
116
113
117
def _open_ftp (self ):
114
- ftp = self .fs ._open_ftp (self .fs .ftp .encoding )
118
+ """Open an ftp object for the file."""
119
+ ftp = self .fs ._open_ftp ()
115
120
ftp .voidcmd (str ('TYPE I' ))
116
121
return ftp
117
122
@@ -306,6 +311,7 @@ def __init__(self,
306
311
self .timeout = timeout
307
312
self .port = port
308
313
314
+ self .encoding = 'latin-1'
309
315
self ._ftp = None
310
316
self ._welcome = None
311
317
self ._features = None
@@ -321,21 +327,52 @@ def __str__(self):
321
327
)
322
328
return _fmt .format (host = self .host , port = self .port )
323
329
324
- def _open_ftp (self , encoding = "utf-8" ):
330
+ @classmethod
331
+ def _parse_features (cls , feat_response ):
332
+ """Parse a dict of features from FTP feat response."""
333
+ features = {}
334
+ if feat_response .split ('-' )[0 ] == '211' :
335
+ for line in feat_response .splitlines ():
336
+ if line .startswith (' ' ):
337
+ key , _ , value = line [1 :].partition (' ' )
338
+ features [key ] = value
339
+ return features
340
+
341
+ def _open_ftp (self ):
342
+ """Open a new ftp object."""
325
343
_ftp = FTP ()
326
344
_ftp .set_debuglevel (0 )
327
345
with ftp_errors (self ):
328
- _ftp .encoding = encoding
329
346
_ftp .connect (self .host , self .port , self .timeout )
330
347
_ftp .login (self .user , self .passwd , self .acct )
348
+ self ._features = {}
349
+ try :
350
+ feat_response = _decode (_ftp .sendcmd ("FEAT" ), 'latin-1' )
351
+ except error_perm :
352
+ self .encoding = 'latin-1'
353
+ else :
354
+ self ._features = self ._parse_features (feat_response )
355
+ self .encoding = (
356
+ 'utf-8'
357
+ if 'UTF8' in self ._features
358
+ else 'latin-1'
359
+ )
360
+ if not PY2 :
361
+ _ftp .file = _ftp .sock .makefile (
362
+ 'r' ,
363
+ encoding = self .encoding
364
+ )
365
+ _ftp .encoding = self .encoding
366
+ self ._welcome = _ftp .welcome
331
367
return _ftp
332
368
333
369
def _manage_ftp (self ):
334
- ftp = self ._open_ftp (self . ftp . encoding )
370
+ ftp = self ._open_ftp ()
335
371
return manage_ftp (ftp )
336
372
337
373
@property
338
374
def ftp_url (self ):
375
+ """Get the FTP url this filesystem will open."""
339
376
url = (
340
377
"ftp://{}" .format (self .host )
341
378
if self .port == 21
@@ -347,40 +384,18 @@ def ftp_url(self):
347
384
def ftp (self ):
348
385
"""`~ftplib.FTP`: the underlying FTP client.
349
386
"""
387
+ return self ._get_ftp ()
388
+
389
+ def _get_ftp (self ):
350
390
if self ._ftp is None :
351
- _ftp = self ._open_ftp ('latin-1' )
352
- try :
353
- encoding = (
354
- 'utf-8'
355
- if 'UTF8' in _ftp .sendcmd ('FEAT' )
356
- else 'latin-1'
357
- )
358
- except error_perm :
359
- encoding = 'latin-1'
360
- self ._ftp = (
361
- _ftp
362
- if encoding == 'latin-1'
363
- else self ._open_ftp (encoding )
364
- )
365
- self ._welcome = self ._ftp .getwelcome ()
391
+ self ._ftp = self ._open_ftp ()
366
392
return self ._ftp
367
393
368
394
@property
369
395
def features (self ):
370
396
"""`dict`: features of the remote FTP server.
371
397
"""
372
- if self ._features is None :
373
- try :
374
- response = _decode (self .ftp .sendcmd ("FEAT" ), "ascii" )
375
- except error_perm :
376
- self ._features = {}
377
- else :
378
- self ._features = {}
379
- if response .split ('-' )[0 ] == '211' :
380
- for line in response .splitlines ():
381
- if line .startswith (' ' ):
382
- k , _ , v = line [1 :].partition (' ' )
383
- self ._features [k ] = v
398
+ self ._get_ftp ()
384
399
return self ._features
385
400
386
401
def _read_dir (self , path ):
@@ -401,6 +416,7 @@ def _read_dir(self, path):
401
416
402
417
@property
403
418
def supports_mlst (self ):
419
+ """Check if server supports MLST feature."""
404
420
return 'MLST' in self .features
405
421
406
422
def create (self , path , wipe = False ):
@@ -414,14 +430,15 @@ def create(self, path, wipe=False):
414
430
)
415
431
416
432
@classmethod
417
- def _parse_ftp_time (cls , t ):
433
+ def _parse_ftp_time (cls , time_text ):
434
+ """Parse a time from an ftp directory listing."""
418
435
try :
419
- tm_year = int (t [0 :4 ])
420
- tm_month = int (t [4 :6 ])
421
- tm_day = int (t [6 :8 ])
422
- tm_hour = int (t [8 :10 ])
423
- tm_min = int (t [10 :12 ])
424
- tm_sec = int (t [12 :14 ])
436
+ tm_year = int (time_text [0 :4 ])
437
+ tm_month = int (time_text [4 :6 ])
438
+ tm_day = int (time_text [6 :8 ])
439
+ tm_hour = int (time_text [8 :10 ])
440
+ tm_min = int (time_text [10 :12 ])
441
+ tm_sec = int (time_text [12 :14 ])
425
442
except ValueError :
426
443
return None
427
444
epoch_time = calendar .timegm ((
@@ -439,11 +456,11 @@ def _parse_facts(cls, line):
439
456
name = None
440
457
facts = {}
441
458
for fact in line .split (';' ):
442
- k , sep , v = fact .partition ('=' )
459
+ key , sep , value = fact .partition ('=' )
443
460
if sep :
444
- k = k .strip ().lower ()
445
- v = v .strip ()
446
- facts [k ] = v
461
+ key = key .strip ().lower ()
462
+ value = value .strip ()
463
+ facts [key ] = value
447
464
else :
448
465
name = basename (fact .rstrip ('/' ).strip ())
449
466
return name if name not in ('.' , '..' ) else None , facts
@@ -520,6 +537,7 @@ def getinfo(self, path, namespaces=None):
520
537
521
538
def getmeta (self , namespace = "standard" ):
522
539
_meta = {}
540
+ self ._get_ftp ()
523
541
if namespace == "standard" :
524
542
_meta = self ._meta .copy ()
525
543
_meta ['unicode_paths' ] = "UTF8" in self .features
@@ -548,8 +566,8 @@ def makedir(self, path, permissions=None, recreate=False):
548
566
if not (recreate and self .isdir (path )):
549
567
try :
550
568
self .ftp .mkd (_encode (_path , self .ftp .encoding ))
551
- except error_perm as e :
552
- code , _ = parse_ftp_error ( e )
569
+ except error_perm as error :
570
+ code , _ = _parse_ftp_error ( error )
553
571
if code == 550 :
554
572
if self .isdir (path ):
555
573
raise errors .DirectoryExists (path )
@@ -575,13 +593,12 @@ def openbin(self, path, mode="r", buffering=-1, **options):
575
593
raise errors .FileExpected (path )
576
594
if _mode .exclusive :
577
595
raise errors .FileExists (path )
578
- f = FTPFile (self , _path , mode )
579
- return f
596
+ ftp_file = FTPFile (self , _path , mode )
597
+ return ftp_file
580
598
581
599
def remove (self , path ):
582
600
self .check ()
583
601
_path = self .validatepath (path )
584
- dir_name , file_name = split (_path )
585
602
with self ._lock :
586
603
if self .isdir (path ):
587
604
raise errors .FileExpected (path = path )
@@ -592,13 +609,12 @@ def removedir(self, path):
592
609
_path = self .validatepath (path )
593
610
if _path == '/' :
594
611
raise errors .RemoveRootError ()
595
- _dir_name , file_name = split (_path )
596
612
597
613
with ftp_errors (self , path ):
598
614
try :
599
615
self .ftp .rmd (_encode (_path , self .ftp .encoding ))
600
- except error_perm as e :
601
- code , _ = parse_ftp_error ( e )
616
+ except error_perm as error :
617
+ code , _ = _parse_ftp_error ( error )
602
618
if code == 550 :
603
619
if self .isfile (path ):
604
620
raise errors .DirectoryExpected (path )
@@ -617,7 +633,7 @@ def _scandir(self, path, namespaces=None):
617
633
str ("MLSD " ) + _encode (_path , self .ftp .encoding ),
618
634
lambda l : lines .append (_decode (l , self .ftp .encoding ))
619
635
)
620
- except error_perm as e :
636
+ except error_perm :
621
637
if not self .getinfo (path ).is_dir :
622
638
raise errors .DirectoryExpected (path )
623
639
raise # pragma: no cover
@@ -667,8 +683,8 @@ def getbytes(self, path):
667
683
str ("RETR " ) + _encode (_path , self .ftp .encoding ),
668
684
data .write
669
685
)
670
- except error_perm as e :
671
- code , _ = parse_ftp_error ( e )
686
+ except error_perm as error :
687
+ code , _ = _parse_ftp_error ( error )
672
688
if code == 550 :
673
689
if self .isdir (path ):
674
690
raise errors .FileExpected (path )
@@ -685,8 +701,3 @@ def close(self):
685
701
pass
686
702
self ._ftp = None
687
703
super (FTPFS , self ).close ()
688
-
689
-
690
- if __name__ == "__main__" : # pragma: no cover
691
- fs = FTPFS (
'ftp.mirror.nl' ,
'anonymous' ,
'[email protected] ' )
692
- print (list (fs .scandir ('/' )))
0 commit comments