attribute: Phillie Casablanca

Category: python sftp (1)

Putty PSFTP python wrapper for sFTP



Here's a sftp wrapper I did a while back for putty's psftp.exe.
It provides sftp access through the same functions that the ftplib module does.
I've only supported storbinary, since I didn't need upload functions for my use case.

Putty is a free ssh/sftp client for windows.

Putty Download:
http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html

The twisted implementation mentioned by Glyph here (http://stackoverflow.com/questions/207939/python-module-that-implements-ftps) may be a better solution for you.
http://twistedmatrix.com/trac/browser/trunk/twisted/conch/ssh/filetransfer.py?rev=24609


import ftplib
import os
import time
import socket
import subprocess
import stat

SLEEP_SECONDS = 1
PROCESS_TIMEOUT_SEC = 10
PSFTP_PATH = '''C:\\Program Files\\PuTTY\\psftp.exe'''

# Exceptions for this program
class PsFtpInvalidCommand(Exception):
def __init__(self,value):
self.value = value
self.message = repr(value)

def __str__(self):
return repr(self.value)


class ProcessTimeout(Exception):
def __init__(self,value):
self.value = value
self.message = repr(value)

def __str__(self):
return repr(self.value)


class FileNotFound(Exception):
def __init__(self,value):
self.value = value
self.message = repr(value)

def __str__(self):
return repr(self.value)


def timeout(func, args=(), kwargs={}, timeout_duration=1, default=None):
'''This function will spwan a thread and
return the given default value if the timeout_duration is exceeded or the function all raises an exception.
'''
import threading
class InterruptableThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.result = default
def run(self):
try:
self.result = func(*args, **kwargs)
except:
self.result = default
it = InterruptableThread()
it.start()
it.join(timeout_duration)
if it.isAlive():
return it.result
else:
return it.result

class SftpHandler:
'''A subprocess wrapper for the psftp tool for encrypted data transfer'''
connected=False

def __init__(self):
self.sftp_path = PSFTP_PATH
self.proc = None
self.min_dl_rate_kbps = 15
self.endline = '\r\n'

def connect(self, server=None):
'''Connect to remote server via the psftp program'''

if os.path.exists(self.sftp_path):
proc_args = (self.sftp_path, server)

try:
self.proc = subprocess.Popen(proc_args,
bufsize=1,
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)

except:
raise socket.gaierror('unable to open process to (%s)' % self.sftp_path)
else:
msg = 'Invalid Path to sftp tool: %s\n' % (self.sftp_path)
msg += '\tThis wrapper uses the Putty psftp tool for secure file transfer.\n'
msg += '\tPutty can be downloaded from: http://www.putty.org/\n' % ()
msg += '\tPSFTP_PATH needs to be set to the proper path for the installed psftp.exe file\n'

raise IOError(msg)


def _wait_for_text(self, text = 'login as', timeout=PROCESS_TIMEOUT_SEC):
'''Function reads the output from self.proc via stdout.read().
This function is blocking and should be run in a separate thread.

In this program the "timeout" function is used to launch
this function in a separate thread with a timeout.

NOTE: this function does have a timeout value, but will only function
if the read() function has continuous input. read() waiting for new character will BLOCK!
'''
resp = ''

cur_time = time.time()
while text not in resp or duration > timeout :

letter = self.proc.stdout.read(1)
resp += letter

prev_time = cur_time
cur_time = time.time()

duration = cur_time - prev_time

self.proc.stdout.flush()
return True


def login(self, username=None, password=None):
'''Login to given server via psftp
If login proceeds without exception login is considered a success
'''
username_prompt = 'login as:'
password_prompt = 'password:'
cmd_prompt = 'psftp>'

# Wait for the username login prompt
# --> use timeout on wait4prompt function
prompt_found = timeout(self._wait_for_text,args=(username_prompt,), timeout_duration=PROCESS_TIMEOUT_SEC, default=False )

if prompt_found:
username_input = username + self.endline
self.proc.stdin.write(username_input)
self.proc.stdin.flush()

# Wait for the username login prompt
# --> use timeout on wait4prompt function
prompt_found = timeout(self._wait_for_text,args=(password_prompt,), timeout_duration=PROCESS_TIMEOUT_SEC, default=False )

if prompt_found:
password_input = password + self.endline
self.proc.stdin.write(password_input)
self.proc.stdin.flush()

# Wait for standard command prompt
# --> If timeout assume invalid username/password and raise ftplib.error_perm
# --> use timeout on wait4prompt function
prompt_found = timeout(self._wait_for_text,args=(cmd_prompt,), timeout_duration=PROCESS_TIMEOUT_SEC, default=False )

if not prompt_found:
self.kill()
self.proc.wait()
raise ftplib.error_perm('Invalid Username/Password: process timedout (prompt: %s)' % (cmd_prompt))
else:
self.kill()
self.proc.wait()
raise ProcessTimeout('Expected Prompt not found, process timedout (prompt: %s)' % (password_prompt))
else:
self.kill()
self.proc.wait()
raise ProcessTimeout('Expected Prompt not found, process timedout (prompt: %s)' % (username_prompt))


def cwd(self, path=None):
'''Issue change working directory (cwd) command'''

if path:
success_text = 'Remote directory is now'
cwd_cmd = 'cd '
path_input = cwd_cmd + path + self.endline
self.proc.stdin.write(path_input)
self.proc.stdin.flush()

# --> use timeout on wait4prompt function
text_found = timeout(self._wait_for_text,args=(success_text,), timeout_duration=PROCESS_TIMEOUT_SEC, default=False )

if not text_found:
msg = 'Command Not Successful: "%s"' % (path_input)
raise PsFtpInvalidCommand(msg)

def storbinary(self, cmd=None, file_handle=None):
'''Used to send a file to the remote server
sftp doesn't use the cmd, it will be ignored
'''

expected_duration = int(os.stat(file_handle.name)[stat.ST_SIZE] /1000)/self.min_dl_rate_kbps
if expected_duration < 5:
expected_duration = PROCESS_TIMEOUT_SEC

sftp_cmd = 'put %s' % (file_handle.name) + self.endline
self.proc.stdin.write(sftp_cmd)
self.proc.stdin.flush()

cmd_prompt = 'psftp>'
# --> use timeout on wait4prompt function
prompt_found = timeout(self._wait_for_text,args=(cmd_prompt,), timeout_duration=expected_duration, default=False )

if not prompt_found:
self.kill()
self.proc.wait()
raise ProcessTimeout('Process took longer than expected, transfer failed')


def kill(self):
'''Kill the proces using pywin32 and pid'''
import win32api
PROCESS_TERMINATE = 1
handle = win32api.OpenProcess(PROCESS_TERMINATE, False, self.proc.pid)
win32api.TerminateProcess(handle, -1)
win32api.CloseHandle(handle)


def quit(self):
'''Quit/Exit psftp if it's still running, ignore if not'''
if self.proc.returncode == None:
exit_cmd = 'exit' + self.endline
self.proc.stdin.write(exit_cmd)
self.proc.stdin.flush()

print 'Waiting for process to close...'
self.proc.wait()



monkut // Oct. 16, 2008 // 9:51 p.m.