66from os import environ , SEEK_END , utime
77from paramiko import (Agent , hostkeys , SFTPClient , SSHConfig , Transport ,
88 ConfigParseError , PasswordRequiredException ,
9- SSHException , DSSKey , ECDSAKey , Ed25519Key , RSAKey )
9+ SSHException , ECDSAKey , Ed25519Key , RSAKey )
1010from pathlib import Path
1111from sftpretty .exceptions import (CredentialException , ConnectionException ,
1212 HostKeysException , LoggingException )
1313from sftpretty .helpers import _callback , drivedrop , hash , localtree , retry
1414from socket import gaierror
1515from stat import S_ISDIR , S_ISREG
1616from tempfile import mkstemp
17+ from threading import get_ident , local as cache
1718from uuid import uuid4
1819
1920
@@ -76,7 +77,7 @@ def __init__(self, config=None, knownhosts=Path(
7677 'diffie-hellman-group-exchange-sha1' )
7778 self .key_types = ('ssh-ed25519' , 'ecdsa-sha2-nistp521' ,
7879 'ecdsa-sha2-nistp384' , 'ecdsa-sha2-nistp256' ,
79- 'rsa-sha2-512' , 'rsa-sha2-256' , 'ssh-rsa' , 'ssh-dss' )
80+ 'rsa-sha2-512' , 'rsa-sha2-256' , 'ssh-rsa' )
8081 self .log = False
8182 self .log_level = 'info'
8283 self .ssh_config = SSHConfig ()
@@ -189,6 +190,8 @@ class Connection(object):
189190 def __init__ (self , host , cnopts = None , default_path = None , password = None ,
190191 port = 22 , private_key = None , private_key_pass = None ,
191192 timeout = None , username = None ):
193+ self ._cache = cache ()
194+ self ._channels = []
192195 self ._cnopts = cnopts or CnOpts ()
193196 self ._config = self ._cnopts .get_config (host )
194197 self ._default_path = default_path
@@ -206,8 +209,7 @@ def _set_authentication(self, password, private_key, private_key_pass):
206209 private_key = self ._config ['identityfile' ][0 ]
207210 if private_key is not None :
208211 # Use key path or provided key object
209- key_types = {'DSA' : DSSKey , 'EC' : ECDSAKey , 'OPENSSH' : Ed25519Key ,
210- 'RSA' : RSAKey }
212+ key_types = {'EC' : ECDSAKey , 'OPENSSH' : Ed25519Key , 'RSA' : RSAKey }
211213 if isinstance (private_key , str ):
212214 key_file = Path (private_key ).expanduser ().absolute ().as_posix ()
213215 try :
@@ -290,29 +292,36 @@ def _set_username(self, username):
290292 raise CredentialException ('No username specified.' )
291293
292294 @contextmanager
293- def _sftp_channel (self , keepalive = False ):
295+ def _sftp_channel (self ):
294296 '''Establish new SFTP channel.'''
295- _channel = None
297+ _channel = getattr ( self . _cache , 'channel' , None )
296298
297299 try :
298- _channel = SFTPClient .from_transport (self ._transport )
299-
300- channel = _channel .get_channel ()
301- channel_name = uuid4 ().hex
302- channel .set_name (channel_name )
303- channel .settimeout (self ._timeout )
304- log .debug (f'Channel Name: [{ channel_name } ]' )
305-
306- if self ._default_path is not None :
307- _channel .chdir (drivedrop (self ._default_path ))
308- log .info (f'Current Working Directory: [{ self ._default_path } ]' )
300+ if _channel is None or _channel .get_channel ().closed :
301+ _channel = SFTPClient .from_transport (self ._transport )
302+ channel = _channel .get_channel ()
303+ channel_name = uuid4 ().hex
304+ channel .set_name (channel_name )
305+ channel .settimeout (self ._timeout )
306+ log .debug (f'Channel Name: [{ channel_name } ]' )
307+
308+ if self ._default_path is not None :
309+ _channel .chdir (drivedrop (self ._default_path ))
310+ log .info (('Current Working Directory: '
311+ f'[{ self ._default_path } ]' ))
312+
313+ self ._cache .channel = _channel
314+ self ._channels .append (_channel )
315+ log .debug (f'Thread Cached: [{ get_ident ()} ]' )
316+ else :
317+ channel = _channel .get_channel ()
318+ channel .settimeout (self ._timeout )
309319
310320 yield _channel
311321 except Exception as err :
322+ _channel .close ()
323+ self ._cache .channel = None
312324 raise err
313- finally :
314- if _channel and not keepalive :
315- _channel .close ()
316325
317326 def _start_transport (self , host , port ):
318327 '''Start the transport and set connection options if specified.'''
@@ -1127,10 +1136,18 @@ def chown(self, remotepath, uid=None, gid=None):
11271136 def close (self ):
11281137 '''Terminate transport connection and clean up the bits.'''
11291138 try :
1139+ # Close cached channels
1140+ for channel in self ._channels :
1141+ if not channel .closed :
1142+ channel .close ()
11301143 # Close the transport.
11311144 if self ._transport and self ._transport .is_active ():
11321145 self ._transport .close ()
1146+
1147+ self ._cache = cache ()
1148+ self ._channels = []
11331149 self ._transport = None
1150+
11341151 # Clean up any loggers
11351152 if log .hasHandlers ():
11361153 # remove lingering handlers if any
@@ -1330,7 +1347,7 @@ def open(self, remotefile, bufsize=-1, mode='r'):
13301347
13311348 :raises: IOError, if the file could not be opened.
13321349 '''
1333- with self ._sftp_channel (keepalive = True ) as channel :
1350+ with self ._sftp_channel () as channel :
13341351 remotefile = drivedrop (remotefile )
13351352 flo = channel .open (remotefile , bufsize = bufsize , mode = mode )
13361353
@@ -1530,7 +1547,7 @@ def sftp_client(self):
15301547
15311548 :returns: (obj) Active SFTPClient object.
15321549 '''
1533- with self ._sftp_channel (keepalive = True ) as channel :
1550+ with self ._sftp_channel () as channel :
15341551 return channel
15351552
15361553 @property
0 commit comments