module.erp5.SFTPConnection.py 6.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2013 Nexedi SARL and Contributors. All Rights Reserved.
#                    Aurélien Calonne <aurel@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

import os, socket
31
import operator
32
from urlparse import urlparse
33
from socket import gaierror, error, socket, getaddrinfo, AF_UNSPEC, SOCK_STREAM
34 35 36
from xmlrpclib import Binary
from cStringIO import StringIO
from paramiko import Transport, RSAKey, SFTPClient
37
from paramiko.util import retry_on_signal
38 39 40 41 42 43 44 45 46 47 48 49

class SFTPError(Exception):
  """
  Default exception for the connection
  """
  pass

class SFTPConnection:
  """
  Handle a SFTP (SSH over FTP) Connection
  """

50 51
  def __init__(self, url, user_name, password=None, private_key=None,
      bind_address=None):
52 53 54 55 56 57
    self.url = url
    self.user_name = user_name
    if password and private_key:
      raise SFTPError("Password and private_key cannot be defined simultaneously")
    self.password = password
    self.private_key = private_key
58
    self.bind_address = bind_address
59 60 61 62 63 64

  def connect(self):
    """ Get a handle to a remote connection """
    # Check URL
    schema = urlparse(self.url)
    if schema.scheme == 'sftp':
65 66 67 68 69 70 71 72 73 74 75 76 77
      hostname = schema.hostname
      port = int(schema.port)
      # Socket creation code inspired from paramiko.Transport.__init__
      # with added bind support.
      for family, socktype, _, _, _ in getaddrinfo(
            hostname, port, AF_UNSPEC, SOCK_STREAM,
          ):
        if socktype == SOCK_STREAM:
          sock = socket(family, SOCK_STREAM)
          if self.bind_address:
            # XXX: Expects bind address to be of same family as hostname.
            # May not be easy if name resolution is involved.
            # Try to reconciliate them ?
78
            sock.bind((self.bind_address, 0))
79
          retry_on_signal(lambda: sock.connect((hostname, port))) # pylint: disable=cell-var-from-loop
80 81 82 83
          break
      else:
        raise SFTPError('No suitable socket family found')
      self.transport = Transport(sock)
84 85 86 87 88 89 90 91 92 93 94 95 96
    else:
      raise SFTPError('Not a valid sftp url %s, type is %s' %(self.url, schema.scheme))
    # Add authentication to transport
    try:
      if self.password:
        self.transport.connect(username=self.user_name, password=self.password)
      elif self.private_key:
        self.transport.connect(username=self.user_name,
                               pkey=RSAKey.from_private_key(StringIO(self.private_key)))
      else:
        raise SFTPError("No password or private_key defined")
      # Connect
      self.conn = SFTPClient.from_transport(self.transport)
97
    except (gaierror, error) as msg:
98 99 100 101 102 103
      raise SFTPError(str(msg) + ' while establishing connection to %s' % (self.url,))
    # Go to specified directory
    try:
      schema.path.rstrip('/')
      if len(schema.path):
        self.conn.chdir(schema.path)
104
    except IOError as msg:
105 106 107
      raise SFTPError(str(msg) + ' while changing to dir -%r-' % (schema.path,))
    return self

108
  def writeFile(self, path, filename, data, confirm=True):
109 110 111 112 113 114
    """
    Write data in provided filepath
    """
    filepath = os.path.join(path, filename)
    serialized_data = Binary(str(data))
    try:
115
      self.conn.putfo(StringIO(str(serialized_data)), filepath, confirm=confirm)
116
    except error as msg:
117
      raise SFTPError(str(msg) + ' while writing file %s on %s' % (filepath, path))
118

119
  def _getFile(self, filepath):
120 121
    """Retrieve the file"""
    try:
122 123 124 125
      # always open with binary mode, otherwise paramiko will raise
      # UnicodeDecodeError for non-utf8 data. also SFTP has no ASCII
      # mode like FTP that normalises CRLF/CR/LF.
      tmp_file = self.conn.file(filepath, 'rb')
126
      tmp_file.seek(0)
127
      return tmp_file.read()
128
    except error as msg:
129 130 131 132
      raise SFTPError(str(msg) + ' while retrieving file %s from %s' % (filepath, self.url))

  def readBinaryFile(self, filepath):
    """Retrieve the file in binary mode"""
133
    return self._getFile(filepath)
134 135 136

  def readAsciiFile(self, filepath):
    """Retrieve the file in ASCII mode"""
137 138
    # normalise CRLF/CR/LF like FTP's ASCII mode transfer.
    return os.linesep.join(self._getFile(filepath).splitlines())
139

140 141 142 143 144 145 146 147
  def getDirectoryContent(self, path, sort_on=None):
    """retrieve all entries in a givan path as a list.

    `sort_on` parameter allows to retrieve the directory content in a sorted
    order, it understands all parameters from
    paramiko.sftp_attr.SFTPAttributes, the most useful being `st_mtime` to sort
    by modification date.
    """
148
    try:
149 150
      if sort_on:
        return [x.filename for x in sorted(self.conn.listdir_attr(path), key=operator.attrgetter(sort_on))]
151
      return self.conn.listdir(path)
152
    except (EOFError, error) as msg:
153 154 155 156 157 158 159 160 161 162
      raise SFTPError(str(msg) + ' while trying to list %s on %s' % (path, self.url))

  def getDirectoryFileList(self, path):
    """Retrieve all entries in a given path with absolute paths as a list"""
    return ["%s/%s"%(path, x) for x in self.getDirectoryContent(path)]

  def removeFile(self, filepath):
    """Delete the file"""
    try:
      self.conn.unlink(filepath)
163
    except error as msg:
164
      raise SFTPError(str(msg) + 'while trying to delete %s on %s' % (filepath, self.url))
165 166 167 168 169

  def renameFile(self, old_path, new_path):
    """Rename a file"""
    try:
      self.conn.rename(old_path, new_path)
170
    except error as msg:
171 172 173
      raise SFTPError('%s while trying to rename "%s" to "%s" on %s.' % \
                     (str(msg), old_path, new_path, self.url))

174
  def createDirectory(self, path, mode=0o777):
175 176 177 178 179 180 181 182 183
    """Create a directory `path` with mode `mode`.
    """
    return self.conn.mkdir(path, mode)

  def removeDirectory(self, path):
    """Remove directory `path`.
    """
    return self.conn.rmdir(path)

184 185 186 187
  def logout(self):
    """Logout of the SFTP Server"""
    self.conn.close()
    self.transport.close()