10
10
11
11
from contextlib import contextmanager
12
12
from ftplib import FTP
13
- from ftplib import error_reply
14
13
from ftplib import error_perm
15
14
from ftplib import error_temp
16
15
@@ -44,16 +43,18 @@ def ftp_errors(fs, path=None):
44
43
raise errors .RemoteConnectionError (
45
44
msg = 'unable to connect to {}' .format (fs .host )
46
45
)
47
- except error_temp as e :
46
+ except error_temp as error :
48
47
if path is not None :
49
48
raise errors .ResourceError (
50
49
path ,
51
- msg = "ftp error on resource '{}' ({})" .format (path , e )
50
+ msg = "ftp error on resource '{}' ({})" .format (path , error )
52
51
)
53
52
else :
54
- raise errors .OperationFailed (msg = 'ftp error ({})' .format (e ))
55
- except error_perm as e :
56
- code , message = parse_ftp_error (e )
53
+ raise errors .OperationFailed (
54
+ msg = 'ftp error ({})' .format (error )
55
+ )
56
+ except error_perm as error :
57
+ code , message = _parse_ftp_error (error )
57
58
if code == 552 :
58
59
raise errors .InsufficientStorage (
59
60
path = path ,
@@ -76,8 +77,10 @@ def manage_ftp(ftp):
76
77
except : # pragma: nocover
77
78
pass
78
79
79
- def parse_ftp_error (e ):
80
- code , _ , message = text_type (e ).partition (' ' )
80
+
81
+ def _parse_ftp_error (error ):
82
+ """Extract code and message from ftp error."""
83
+ code , _ , message = text_type (error ).partition (' ' )
81
84
if code .isdigit ():
82
85
code = int (code )
83
86
return code , message
@@ -98,6 +101,7 @@ def _decode(st, _):
98
101
class FTPFile (io .IOBase ):
99
102
100
103
def __init__ (self , ftpfs , path , mode ):
104
+ super (FTPFile , self ).__init__ ()
101
105
self .fs = ftpfs
102
106
self .path = path
103
107
self .mode = Mode (mode )
@@ -108,7 +112,8 @@ def __init__(self, ftpfs, path, mode):
108
112
self ._write_conn = None
109
113
110
114
def _open_ftp (self ):
111
- ftp = self .fs ._open_ftp (self .fs .ftp .encoding )
115
+ """Open an ftp object for the file."""
116
+ ftp = self .fs ._open_ftp ()
112
117
ftp .voidcmd (str ('TYPE I' ))
113
118
return ftp
114
119
@@ -302,6 +307,7 @@ def __init__(self,
302
307
self .timeout = timeout
303
308
self .port = port
304
309
310
+ self .encoding = 'latin-1'
305
311
self ._ftp = None
306
312
self ._welcome = None
307
313
self ._features = None
@@ -317,21 +323,52 @@ def __str__(self):
317
323
)
318
324
return _fmt .format (host = self .host , port = self .port )
319
325
320
- def _open_ftp (self , encoding = "utf-8" ):
326
+ @classmethod
327
+ def _parse_features (cls , feat_response ):
328
+ """Parse a dict of features from FTP feat response."""
329
+ features = {}
330
+ if feat_response .split ('-' )[0 ] == '211' :
331
+ for line in feat_response .splitlines ():
332
+ if line .startswith (' ' ):
333
+ key , _ , value = line [1 :].partition (' ' )
334
+ features [key ] = value
335
+ return features
336
+
337
+ def _open_ftp (self ):
338
+ """Open a new ftp object."""
321
339
_ftp = FTP ()
322
340
_ftp .set_debuglevel (0 )
323
341
with ftp_errors (self ):
324
- _ftp .encoding = encoding
325
342
_ftp .connect (self .host , self .port , self .timeout )
326
343
_ftp .login (self .user , self .passwd , self .acct )
344
+ self ._features = {}
345
+ try :
346
+ feat_response = _decode (_ftp .sendcmd ("FEAT" ), 'latin-1' )
347
+ except error_perm :
348
+ self .encoding = 'latin-1'
349
+ else :
350
+ self ._features = self ._parse_features (feat_response )
351
+ self .encoding = (
352
+ 'utf-8'
353
+ if 'UTF8' in self ._features
354
+ else 'latin-1'
355
+ )
356
+ if not PY2 :
357
+ _ftp .file = _ftp .sock .makefile (
358
+ 'r' ,
359
+ encoding = self .encoding
360
+ )
361
+ _ftp .encoding = self .encoding
362
+ self ._welcome = _ftp .welcome
327
363
return _ftp
328
364
329
365
def _manage_ftp (self ):
330
- ftp = self ._open_ftp (self . ftp . encoding )
366
+ ftp = self ._open_ftp ()
331
367
return manage_ftp (ftp )
332
368
333
369
@property
334
370
def ftp_url (self ):
371
+ """Get the FTP url this filesystem will open."""
335
372
url = (
336
373
"ftp://{}" .format (self .host )
337
374
if self .port == 21
@@ -342,39 +379,17 @@ def ftp_url(self):
342
379
@property
343
380
def ftp (self ):
344
381
"""Get a FTP (ftplib) object."""
382
+ return self ._get_ftp ()
383
+
384
+ def _get_ftp (self ):
345
385
if self ._ftp is None :
346
- _ftp = self ._open_ftp ('latin-1' )
347
- try :
348
- encoding = (
349
- 'utf-8'
350
- if 'UTF8' in _ftp .sendcmd ('FEAT' )
351
- else 'latin-1'
352
- )
353
- except error_perm :
354
- encoding = 'latin-1'
355
- self ._ftp = (
356
- _ftp
357
- if encoding == 'latin-1'
358
- else self ._open_ftp (encoding )
359
- )
360
- self ._welcome = self ._ftp .getwelcome ()
386
+ self ._ftp = self ._open_ftp ()
361
387
return self ._ftp
362
388
363
389
@property
364
390
def features (self ):
365
391
"""Get features dict from FTP server."""
366
- if self ._features is None :
367
- try :
368
- response = _decode (self .ftp .sendcmd ("FEAT" ), "ascii" )
369
- except error_perm :
370
- self ._features = {}
371
- else :
372
- self ._features = {}
373
- if response .split ('-' )[0 ] == '211' :
374
- for line in response .splitlines ():
375
- if line .startswith (' ' ):
376
- k , _ , v = line [1 :].partition (' ' )
377
- self ._features [k ] = v
392
+ self ._get_ftp ()
378
393
return self ._features
379
394
380
395
def _read_dir (self , path ):
@@ -395,6 +410,7 @@ def _read_dir(self, path):
395
410
396
411
@property
397
412
def supports_mlst (self ):
413
+ """Check if server supports MLST feature."""
398
414
return 'MLST' in self .features
399
415
400
416
def create (self , path , wipe = False ):
@@ -408,14 +424,15 @@ def create(self, path, wipe=False):
408
424
)
409
425
410
426
@classmethod
411
- def _parse_ftp_time (cls , t ):
427
+ def _parse_ftp_time (cls , time_text ):
428
+ """Parse a time from an ftp directory listing."""
412
429
try :
413
- tm_year = int (t [0 :4 ])
414
- tm_month = int (t [4 :6 ])
415
- tm_day = int (t [6 :8 ])
416
- tm_hour = int (t [8 :10 ])
417
- tm_min = int (t [10 :12 ])
418
- tm_sec = int (t [12 :14 ])
430
+ tm_year = int (time_text [0 :4 ])
431
+ tm_month = int (time_text [4 :6 ])
432
+ tm_day = int (time_text [6 :8 ])
433
+ tm_hour = int (time_text [8 :10 ])
434
+ tm_min = int (time_text [10 :12 ])
435
+ tm_sec = int (time_text [12 :14 ])
419
436
except ValueError :
420
437
return None
421
438
epoch_time = calendar .timegm ((
@@ -433,11 +450,11 @@ def _parse_facts(cls, line):
433
450
name = None
434
451
facts = {}
435
452
for fact in line .split (';' ):
436
- k , sep , v = fact .partition ('=' )
453
+ key , sep , value = fact .partition ('=' )
437
454
if sep :
438
- k = k .strip ().lower ()
439
- v = v .strip ()
440
- facts [k ] = v
455
+ key = key .strip ().lower ()
456
+ value = value .strip ()
457
+ facts [key ] = value
441
458
else :
442
459
name = basename (fact .rstrip ('/' ).strip ())
443
460
return name if name not in ('.' , '..' ) else None , facts
@@ -514,6 +531,7 @@ def getinfo(self, path, namespaces=None):
514
531
515
532
def getmeta (self , namespace = "standard" ):
516
533
_meta = {}
534
+ self ._get_ftp ()
517
535
if namespace == "standard" :
518
536
_meta = self ._meta .copy ()
519
537
_meta ['unicode_paths' ] = "UTF8" in self .features
@@ -542,8 +560,8 @@ def makedir(self, path, permissions=None, recreate=False):
542
560
if not (recreate and self .isdir (path )):
543
561
try :
544
562
self .ftp .mkd (_encode (_path , self .ftp .encoding ))
545
- except error_perm as e :
546
- code , _ = parse_ftp_error ( e )
563
+ except error_perm as error :
564
+ code , _ = _parse_ftp_error ( error )
547
565
if code == 550 :
548
566
if self .isdir (path ):
549
567
raise errors .DirectoryExists (path )
@@ -569,13 +587,12 @@ def openbin(self, path, mode="r", buffering=-1, **options):
569
587
raise errors .FileExpected (path )
570
588
if _mode .exclusive :
571
589
raise errors .FileExists (path )
572
- f = FTPFile (self , _path , mode )
573
- return f
590
+ ftp_file = FTPFile (self , _path , mode )
591
+ return ftp_file
574
592
575
593
def remove (self , path ):
576
594
self .check ()
577
595
_path = self .validatepath (path )
578
- dir_name , file_name = split (_path )
579
596
with self ._lock :
580
597
if self .isdir (path ):
581
598
raise errors .FileExpected (path = path )
@@ -586,13 +603,12 @@ def removedir(self, path):
586
603
_path = self .validatepath (path )
587
604
if _path == '/' :
588
605
raise errors .RemoveRootError ()
589
- _dir_name , file_name = split (_path )
590
606
591
607
with ftp_errors (self , path ):
592
608
try :
593
609
self .ftp .rmd (_encode (_path , self .ftp .encoding ))
594
- except error_perm as e :
595
- code , _ = parse_ftp_error ( e )
610
+ except error_perm as error :
611
+ code , _ = _parse_ftp_error ( error )
596
612
if code == 550 :
597
613
if self .isfile (path ):
598
614
raise errors .DirectoryExpected (path )
@@ -611,7 +627,7 @@ def _scandir(self, path, namespaces=None):
611
627
str ("MLSD " ) + _encode (_path , self .ftp .encoding ),
612
628
lambda l : lines .append (_decode (l , self .ftp .encoding ))
613
629
)
614
- except error_perm as e :
630
+ except error_perm :
615
631
if not self .getinfo (path ).is_dir :
616
632
raise errors .DirectoryExpected (path )
617
633
raise # pragma: no cover
@@ -661,8 +677,8 @@ def getbytes(self, path):
661
677
str ("RETR " ) + _encode (_path , self .ftp .encoding ),
662
678
data .write
663
679
)
664
- except error_perm as e :
665
- code , _ = parse_ftp_error ( e )
680
+ except error_perm as error :
681
+ code , _ = _parse_ftp_error ( error )
666
682
if code == 550 :
667
683
if self .isdir (path ):
668
684
raise errors .FileExpected (path )
@@ -679,8 +695,3 @@ def close(self):
679
695
pass
680
696
self ._ftp = None
681
697
super (FTPFS , self ).close ()
682
-
683
-
684
- if __name__ == "__main__" : # pragma: no cover
685
- fs = FTPFS (
'ftp.mirror.nl' ,
'anonymous' ,
'[email protected] ' )
686
- print (list (fs .scandir ('/' )))
0 commit comments