# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import ctypes
import stat
import os
import io
from . import api
from . import compat
from . import exceptions as exp
[docs]class Sftp(object):
"""
Sftp wrapper.
Exposes api for interacting with sftp subsystem: put or get files,
open files with random read-write access, etc.
:ivar ponter sftp: c sftp session pointer
:ivar pointer session: c ssh session pointer
:param pyssh.session.Session session: initialized and connected
:py:class:`pyssh.session.Session` instance.
"""
sftp = None
session = None
def __init__(self, session, buffer_size=1024*16):
self.session_wrapper = session
self.session = session.session
self.buffer_size = buffer_size
# TODO: handle exceptions
self.sftp = api.library.sftp_new(self.session)
api.library.sftp_init(self.sftp)
def _get_file_metadata(self, file_ptr):
attrs_ptr = api.library.sftp_fstat(file_ptr)
if attrs_ptr is None:
msg = api.library.ssh_get_error(self.session)
raise exp.ConnectionError("Error raised by ssh: {0}".format(msg.decode("utf-8")))
attrs = ctypes.cast(attrs_ptr, ctypes.POINTER(api.SftpAttributes))
return attrs.contents
def _open_remote_file(self, path):
remote_file_ptr = api.library.sftp_open(self.sftp, path, os.O_RDONLY, stat.S_IRWXU)
if remote_file_ptr is None:
msg = api.library.ssh_get_error(self.session)
raise exp.ConnectionError("Error raised by ssh: {0}".format(msg.decode("utf-8")))
return remote_file_ptr
[docs] def get(self, remote_path, local_path):
"""
Get a remote file to local.
:param str remote_path: remote file path
:param str local_path: local file path
"""
remote_path = compat.to_bytes(remote_path)
# Create new pointer to remote file
remote_file_ptr = self._open_remote_file(remote_path)
# Obtain metadata with remote file size
remote_file_attrs = self._get_file_metadata(remote_file_ptr)
stats = {
"total_readed": 0,
"total_size": remote_file_attrs.size,
"errors_counter": 0,
}
def read_pipeline(f):
while True:
buffer = ctypes.create_string_buffer(self.buffer_size)
readed = api.library.sftp_read(remote_file_ptr, ctypes.byref(buffer),
self.buffer_size)
if readed == 0:
if stats["total_readed"] != stats["total_size"]:
stats["errors_counter"] += 1
return False
return True
elif readed < 0:
raise exp.ConnectionError("Connection interrumped")
else:
stats["errors_counter"] = 0
stats["total_readed"] += readed
f.write(buffer.raw[:readed])
try:
with io.open(local_path, "wb") as f:
while stats["errors_counter"] < 3:
ok = read_pipeline(f)
if ok:
break
api.library.sftp_close(remote_file_ptr)
remote_file_ptr = self._open_remote_file(remote_path)
if stats["errors_counter"] >= 3:
raise exp.Connection("Connection errors repeated more than 3 times")
except (exp.ConnectionError, RuntimeError):
api.library.sftp_close(remote_file_ptr)
raise
[docs] def put(self, path, remote_path):
"""
Puts the local file to remote host.
:param str path: local file path
:param str remote_path: remote file path
"""
if not os.path.exists(path):
raise RuntimeError("Path {0} does not exists".format(path))
if isinstance(remote_path, compat.text_type):
remote_path = compat.to_bytes(remote_path, "utf-8")
access_type = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
remote_file_ptr = api.library.sftp_open(self.sftp, remote_path, access_type, stat.S_IRWXU)
if remote_file_ptr is None:
msg = api.library.ssh_get_error(self.session)
raise exp.ConnectionError("Error raised by ssh: {0}".format(msg.decode("utf-8")))
with io.open(path, "rb") as f:
while True:
chuck = f.read(self.buffer_size)
if not chuck:
break
written = api.library.sftp_write(remote_file_ptr, chuck, len(chuck))
if written != len(chuck):
raise RuntimeError("Can't write file")
api.library.sftp_close(remote_file_ptr)
[docs] def open(self, path, mode):
"""
Open a remote file.
:param str path: remote file path
:param int mode: open file model
(see http://docs.python.org/3.3/library/os.html#open-flag-constants)
:returns: SFTP File wrapper
:rtype: pyssh.SftpFile
"""
if isinstance(path, compat.text_type):
path = compat.to_bytes(path, "utf-8")
return SftpFile(path, mode, self)
def __enter__(self):
return self
def __exit__(self, *args, **kwargs):
api.library.sftp_free(self.sftp)
[docs]class SftpFile(object):
"""
SFTP File wrapper
"""
_closed = False
def __init__(self, path, mode, sftp_wrapper):
self.sftp_wrapper = sftp_wrapper
self.sftp = sftp_wrapper.sftp
self.file = api.library.sftp_open(self.sftp, path, mode, stat.S_IRWXU)
if self.file is None:
self._closed = True
raise ext.SftpError("Can't open file {0}".format(path.decode("utf-8")))
def __enter__(self):
return self
def __exit__(self, *args, **kwargs):
self.close()
[docs] def write(self, data):
"""
Write bytes to remote file.
:param bytes data: bytes chunk of data
:returns: number of bytes are written
:rtype: int
"""
written = api.library.sftp_write(self.file, data, len(data))
if written != len(data):
raise RuntimeError("Can't write file")
return written
[docs] def read(self, num=None, buffer_length=1024):
"""
Read from remote file.
:param int num: number of bytes to read, if num is None reads all.
:returns: readed bytes chunk
:rtype: bytes
"""
if num is None:
buffer_len = buffer_length
else:
buffer_len = num
buffer = ctypes.create_string_buffer(buffer_len)
readed = api.library.sftp_read(self.file, ctypes.byref(buffer), buffer_len);
if readed == 0:
return b""
if num is not None and num > 0:
if buffer_len != readed:
raise RuntimeError("Error on read")
return buffer.raw
readed_data = [buffer.raw]
while True:
buffer = ctypes.create_string_buffer(buffer_len)
readed = api.library.sftp_read(self.file, ctypes.byref(buffer), buffer_len);
if readed == 0:
break
readed_data.append(buffer.raw[:readed])
return b"".join(readed_data)
[docs] def seek(self, offset):
"""
Change position on a remote file.
:param int offset: file position
:returns: boolean value if seek is success or not
:rtype: bool
"""
ret = api.library.sftp_seek64(self.file, offset);
if ret != api.SSH_OK:
return False
return True
[docs] def tell(self):
"""
Query the current position on a file.
:returns: a current position.
:rtype: int
"""
return api.library.sftp_tell64(self.file)
[docs] def close(self):
"""
Close a opened file.
"""
if self._closed:
raise ext.ResourceManagementError("SftpFile instance already closed.")
self._closed = True
api.library.sftp_close(self.file)