Skip to content

Commit bd10056

Browse files
Merge pull request #66 from byteskeptical/dssbs
DSSBS
2 parents b5555a7 + 056c6e1 commit bd10056

8 files changed

Lines changed: 52 additions & 29 deletions

File tree

README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ routines with progress notifications for reliable, asynchronous transfers. A
66
Python3 optimized fork of pysftp with additional features & improvements.
77

88
* Built-in retry decorator
9+
* Channel cache
910
* Hash function for integrity checking
1011
* Improved local & remote directory mapping
1112
* Improved logging mechanism

docs/changes.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
Change Log
22
==========
33

4-
1.1.8 (current, released 2025-6-13)
4+
1.1.9 (current, released 2025-8-06)
55
-----------------------------------
6+
* removing ssh-dss key type as it was deprecated in paramiko in 4.0.0.
7+
* adding channel cache to place upper limit on creation overhead.
8+
9+
1.1.8 (released 2025-6-13)
10+
--------------------------
611
* added WARN log level to logger, mapped to QUIET in ssh config.
712
* added support for int values in CnOpts.log_level mapped to python logging.
813

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@
5454
# built documents.
5555
#
5656
# The short X.Y version.
57-
version = '1.1.8'
57+
version = '1.1.9'
5858
# The full version, including alpha/beta/rc tags.
59-
release = '1.1.8'
59+
release = '1.1.9'
6060

6161
# The language for content autogenerated by Sphinx. Refer to documentation
6262
# for a list of supported languages.

docs/cookbook.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ AVAILABLE CONNECTION OPTIONS:
189189
* ``.key_types`` - Replaces the key types parameter in the Connection method.
190190
* ``.log`` - False **Default** logs to console, True logs to temporary file,
191191
String sets custom location.
192-
* ``.log_level`` - Set logger verbosity to either debug, error, or
193-
info **Default**.
192+
* ``.log_level`` - Set logger verbosity to either debug, error,
193+
info **Default**, or warn.
194194

195195
Here is a common scenario, you have your connection information stored in a
196196
persistence mechanism, like `yamjam <https://yamjam.rtfd.org/>`_ and when you access

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ routines with progress notifications for reliable, asynchronous transfers. A
66
Python3 optimized fork of pysftp with additional features & improvements.
77

88
* Built-in retry decorator
9+
* Channel cache
910
* Hash function for integrity checking
1011
* Improved local & remote directory mapping
1112
* Improved logging mechanism

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ keywords = [
4646
name = 'sftpretty'
4747
readme = 'README.rst'
4848
requires-python = '>=3.6'
49-
version = '1.1.8'
49+
version = '1.1.9'
5050

5151
[project.scripts]
5252
sftpretty = 'sftpretty:Connection'

sftpretty/__init__.py

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
from os import environ, SEEK_END, utime
77
from paramiko import (Agent, hostkeys, SFTPClient, SSHConfig, Transport,
88
ConfigParseError, PasswordRequiredException,
9-
SSHException, DSSKey, ECDSAKey, Ed25519Key, RSAKey)
9+
SSHException, ECDSAKey, Ed25519Key, RSAKey)
1010
from pathlib import Path
1111
from sftpretty.exceptions import (CredentialException, ConnectionException,
1212
HostKeysException, LoggingException)
1313
from sftpretty.helpers import _callback, drivedrop, hash, localtree, retry
1414
from socket import gaierror
1515
from stat import S_ISDIR, S_ISREG
1616
from tempfile import mkstemp
17+
from threading import get_ident, local as cache
1718
from 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

tests/test_connection.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import pytest
44

5-
from paramiko.hostkeys import HostKeys
65
from paramiko.ed25519key import Ed25519Key
76

87
from common import conn, LOCAL, VFS

0 commit comments

Comments
 (0)