#
# FtpCube
# Copyright (C) 2001 Michael Gilfix
#
# This file is part of FtpCube.
#
# You should have received a file COPYING containing license terms
# along with this program; if not, write to Michael Gilfix
# (mgilfix@eecs.tufts.edu) for a copy.
#
# This version of FtpCube is open source; you can redistribute it and/or
# modify it under the terms listed in the file COPYING.
#
# 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.
#

import gtk, GtkExtra
import sys
import string # For backwards compatibility with 1.5.x
import socket, threading, re, os, errno, time
import exceptions
import main, app, logging

from intl import _

CRLF = '\r\n'

DOWNLOAD = 0
UPLOAD = 1

class FtpException (exceptions.Exception):

	"""FTP-specific exception

	This class marks exceptions which are raised during any of the FTP
	functionality."""

	def __init__ (self, args=None):
		self.args = args

class FtpControlConnection (threading.Thread):

	"""An FTP control session object

	This class provides an interface for managing an FTP control session. The
	control session handles all sending and receiving in a separate thread so
	that multiple threaded connections can be maintained simultaneously.

	The FtpControlConnection object contains synchornization primitives to
	manage sending and receiving of FTP messages, as all calls to the control
	interface come from separate threads of execution. All outgoing FTP
	commands are command objects and implement the abstract interface in the
	FTPAbstractCommand class. The control thread then manages the queuing
	of the outgoing objects and the receipt of their responses. Methods are
	also provided to allow some finer-grained control over the queueing
	process, viewing status, as well as retrieving the response data."""

	def __init__ (self, host, port, silence=0, logging=0):
		"""Creates a control connection object and initializes necessary
		instance variables. The 'host' and 'port' arguments indicate which
		the other party of the connection. The 'silence' argument indicates
		whether the status of the FTP commands should appear in the application
		command-window (not desirable for transfer threads) and 'logging'
		argument indicates whether or not sent and received FTP commands
		should be logged. A call to the connect method should follow directly
		after object creation."""

		threading.Thread.__init__ (self)
		self.setDaemon (1)

		# Initialize instance variables
		self.sock = None
		self.file = None
		self.multiline_code = None
		self.cmdobj = None
		self.exception_handler = None
		self.incoming = [ ]
		self.outgoing = [ ]
		self.outgoing_event = threading.Event ()
		self.die = 0
		self.host = host
		self.port = port
		self.silence = silence
		self.cmd_log = None
		self.set_log_active (logging)

	def connect (self):
		"""Sets up a connection to the FTP server in  a non-blocking fashion
		(to allow for aborting of the connection process). This creates the
		instance variable 'sock', which is a reference to the connection
		socket, and 'file', which is a file object representing the socket."""

		# Create the socket
		proto = socket.getprotobyname ('tcp')
		try:
			self.sock = socket.socket (socket.AF_INET, socket.SOCK_STREAM, proto)
		except socket.error, strerror:
			raise FtpException (_("Error opening control socket: %s") %strerror)

		gtk.threads_enter ()
		if not self.silence: main.app.ftp_local_tell_user (_("Connecting to %s on port %d.") %(self.host, self.port))
		gtk.threads_leave ()

		self.sock.setblocking (0)

		try:
			err = self.sock.connect_ex ((self.host, self.port))
		except socket.error, strerror:
			# If we're in here, we probably caught an error such as hostname lookups
			# This is pretty fatal, so let's clear any exception handlers before raising
			# the exception
			self.clear_exception_handler ()
			raise FtpException (_("Error connecting to host %s: %s") %(self.host, strerror))

		gtk.threads_enter ()
		if not self.silence: main.app.ftp_local_tell_user (_("Connected to %s.") %self.host)
		gtk.threads_leave ()

		while err == errno.EINPROGRESS and not self.die:

			time.sleep (1)
			err = self.sock.getsockopt (socket.SOL_SOCKET, socket.SO_ERROR)

		if err != 0 or self.die:
			raise FtpException (_("Error connecting to host %s: %s") %(self.host, os.strerror (err)))
		else:
			self.sock.setblocking (1)
			self.file = self.sock.makefile ('rb')

	def destroy (self):
		"""Tear down the control connection gracefully."""

		# This might need some revising at some point
		if not self.silence: main.app.ftp_local_tell_user (_("Closing connection to %s...") %(self.host))
		self.die = 1
		self.outgoing_event.set ()
		if self.file is not None:
			self.file.close ()
		if self.sock is not None:
			self.sock.close ()
		if self.logging:
			self.log (_("Closing connection to %s:%d...") %(self.host, self.port))
			self.log ('')

	def get_host (self):
		"""Returns the host that the control connection is connected to."""

		return self.host

	def get_port (self):
		"""Returns the port that the control connection is using."""

		return self.port

	def reset_queue (self):
		"""Clears the outgoing FTP command queue and puts the connection thread in
		blocking mode."""

		self.outgoing = [ ]
		self.outgoing_event.set ()

	def wait_for_response (self):
		"""Provides a method for command objects to indicate that they would like
		to wait until the FTP server issues a response to their last command (usually
		always the case)."""

		while 1:
			line = self.file.readline ()

			if not line:
				gtk.threads_enter ()
				if not self.silence: main.app.ftp_local_tell_user (_("Connection to %s terminated...") %self.host)
				gtk.threads_leave ()
				self.sock.close ()
				return

			# Clean up the line
			if line[-2:] == CRLF: line = line[:-2]
			elif line[-1:] in CRLF: line = line[:-1]

			data = self.process_data (line)
			if data is not None:
				return data

	def process_data (self, line):
		"""Determines whether the response 'line' is part of a single-line response or
		a multi-line response. Also displays the response in the application command
		window if the control connection is not silent and logs the response line if
		appropriate. The response is stored in the instance variable 'self.incoming',
		to be retrieved if desired."""

		if not self.silence:
			gtk.threads_enter ()
			main.app.ftp_remote_tell_user (line)
			gtk.threads_leave ()

		if self.logging:
			self.log (line)

		self.incoming.append (line)

		if line[3:4] == '-' or self.multiline_code is not None:
			if self.multiline_code is None:
				self.multiline_code = line[:3]
				return
			elif line[:3] == self.multiline_code and line[3:4] != '-':
				self.multiline_code = None
				data = self.incoming
				self.incoming = [ ]
				return data
		else:
			data = self.incoming
			self.incoming = [ ]
			return data

	def send (self, msg, sensitive=0):
		"""Appends an end of message marker to the FTP message string and sends
		the message."""

		if self.logging:
			if not sensitive:
				self.log (msg)
			else:
				# Star out the right-most word
				cmd, secret = string.split (msg, ' ')
				newsecret = ''
				for i in range (len (secret)):
					newsecret = newsecret + '*'
				self.log (string.join ((cmd, newsecret), ' '))
		return self.sock.send (msg + CRLF)

	def send_oob (self, msg):
		"""Appends an end of message marker to the FTP message string and
		toggles the out-of-band TCP flag when sending the message."""

		return self.sock.send (msg + CRLF, socket.MSG_OOB)

	def run (self):
		"""Executes the command objects from the outgoing queue in a separate
		execution thread. The thread blocks when the queue is empty. If an
		exception occurs during the execution of the command object, the
		exception handler is called if it is set or by default, an error message
		appears in the command window. The outgoing queue is also always reset
		with the advent of an exception."""

		while not self.die:
			if self.outgoing == [ ]:
				self.outgoing_event.clear ()
				self.outgoing_event.wait ()

			if not self.outgoing == [ ]:
				self.cmdobj = self.outgoing.pop (0)
				if self.cmdobj is None:
					continue
				else:
					try:
						# Call the execute function directly so
						# it runs in our thread
						self.cmdobj.execute ()
					except Exception, strerror:

						# Always reset the queue
						self.reset_queue ()

						if not self.die:
							if self.exception_handler is not None:
								self.exception_handler (strerror)
							else:
								gtk.threads_enter ()
								main.app.ftp_error_tell_user (strerror)
								gtk.threads_leave ()

	def set_exception_handler (self, callback):
		"""Sets an exception handler to deal with exceptions during command
		exceution asynchronously."""

		self.exception_handler = callback

	def get_executing_obj (self):
		"""Returns a reference to the last executed or currently executing
		command object. This is useful for interacting directly with the
		object after the command has been executed, such as retried the
		result of the response."""

		return self.cmdobj

	def busy (self):
		"""Indicates whether or not a command object is currently being
		executed."""

		return self.outgoing_event.isSet ()

	def add_outgoing (self, cmdobj):
		"""Adds the command object to the outgoing queue and unblocks the
		execution thread if necessary."""

		self.outgoing.append (cmdobj)
		if not self.outgoing_event.isSet ():
			self.outgoing_event.set ()

	def set_log_active (self, bool):
		"""Creates a logging object if 'bool' is 1, and destroy the
		logging object is bool is false."""

		if bool:
			if self.cmd_log is None:
				self.cmd_log = logging.Log (os.path.join (app.CONFIG_DIR, main.app['cmd_log']))
		else:
			if self.cmd_log is not None:
				del self.cmd_log
				self.cmd_log = None
		self.logging = bool

	def log (self, cmd):
		"""Writes the cmd string to the log."""

		self.cmd_log.write (cmd)

class FTPAbstractCommand:

	"""Abstract interface for FTP command objects."""

	def execute (self):
		"""This needs to be overriden in the derived class."""

		pass

class FTPGetWelcomeMsg (FTPAbstractCommand):

	"""Welcome Message command object

	A command object for receiving the welcome message. This command object
	should be added to the control queue shortly after establishing the
	connection."""

	def __init__ (self, queue):
		self.control_queue = queue

	def execute (self):

		response = self.control_queue.wait_for_response ()
		c = response[-1][:1]
		if c not in '123':
			raise FtpException (_("Error connecting to server: Welcome message not received correctly"))

class FTPConnect (FTPAbstractCommand):

	"""Connection command object

	Causes the control connection to initiate a connection with the FTP
	server in a separate execution thread."""

	def __init__ (self, queue):
		self.control_queue = queue

	def execute (self):
		self.control_queue.connect ()

class FTPAbortCmd (FTPAbstractCommand):

	"""Abort command object

	Sends an abort to the FTP server to abort the current message
	sequence."""

	def __init__ (self, queue, silence=0):

		self.control_queue = queue
		self.silence = silence

	def execute (self):

		if not self.silence:
			gtk.threads_enter ()
			main.app.ftp_local_tell_user ("ABOR")
			gtk.threads_leave ()

		self.control_queue.send_oob ("ABOR")
		response = self.control_queue.wait_for_response ()
		if response[-1][:3] not in ('426', '226'):
			raise FtpException (response[-1])

class FTPVoidCmd (FTPAbstractCommand):

	"""General command object

	This models the basic FTP message exchange: a command is
	sent and execution pauses while a response is awaited. The
	VoidCmd object is used when we don't need any further processing
	on the response."""

	def __init__ (self, queue, cmd, silence=0):

		self.control_queue = queue
		self.silence = silence
		self.cmd = cmd

	def execute (self):

		if not self.silence:
			gtk.threads_enter ()
			main.app.ftp_local_tell_user (self.cmd)
			gtk.threads_leave ()

		self.control_queue.send (self.cmd)
		self.response = self.control_queue.wait_for_response ()
		if self.response[-1][0] != '2':
			raise FtpException (self.response[-1])

class FTPBasicCmd (FTPVoidCmd):

	"""Expanded command object

	Provides the same functionality as FTPVoidCmd, except provides
	a method for retrieving the actual response for further
	processing."""

	def __init__ (self, queue, cmd, silence=0):
		FTPVoidCmd.__init__ (self, queue, cmd, silence)

	def get_response (self):
		return self.response

class FTPPendingCmd (FTPAbstractCommand):

	"""Pending command object

	Pending commands are FTP commands which set up conditions
	for subsequent commands and thus should return a 3xx
	error code."""

	def __init__ (self, queue, cmd, silence=0):

		self.control_queue = queue
		self.silence = silence
		self.cmd = cmd

	def execute (self):

		if not self.silence:
			gtk.threads_enter ()
			main.app.ftp_local_tell_user (self.cmd)
			gtk.threads_leave ()

		self.control_queue.send (self.cmd)
		self.response = self.control_queue.wait_for_response ()
		if self.response[-1][0] != '3':
			raise FtpException (self.response[-1])


class FTPLogin (FTPAbstractCommand):

	"""Login command object

	Performs the necessary sequence to log the user into the
	FTP server with a 'user' name and password 'pw'. Acct is
	currently not used and ignored."""

	def __init__ (self, queue, user, pw, acct, silence=0):

		self.control_queue = queue
		self.silence = silence
		self.user = user
		self.pw = pw
		self.acct = acct

	def execute (self):

		usr_string = "USER " + self.user

		if not self.silence:
			gtk.threads_enter ()
			main.app.ftp_local_tell_user (usr_string)
			gtk.threads_leave ()

		self.control_queue.send (usr_string)
		response = self.control_queue.wait_for_response ()

		if response[-1][0] == '3':

			pw_string = "PASS " + self.pw

			# Create a print friendly version
			chars = list (self.pw)
			for i in range (len (chars)):
				chars[i] = "*"
			safe_pw = "PASS " + string.join (chars, '')

			if not self.silence:
				gtk.threads_enter ()
				main.app.ftp_local_tell_user (safe_pw)
				gtk.threads_leave ()

			self.control_queue.send (pw_string, sensitive=1)
			response = self.control_queue.wait_for_response ()

		if response[-1][0] == '3':
			# For now, assume ACCT to be ''
			acct_string = "ACCT "

			if not self.silence:
				gtk.threads_enter ()
				main.app.ftp_local_tell_user (acct_string)
				gtk.threads_leave ()

			self.control_queue.send (acct_string)
			response = self.control_queue.wait_for_response ()

		if response[-1][0] != '2':
			raise FtpException (_("Error logging in to %s: %s") %(self.control_queue.get_host (), response[-1]))

class FTPTransferConnection:

	"""An FTP transfer object

	This class provides a wrapper interface for managing FTP transfers, which
	occur on different ports, separate from the control connection. Currently,
	only passive transfers (meaning the client initiates the connection to a
	port designated by the server and the only kind that works behind a
	firewall) are supported."""

	def __init__ (self, control, host, port, passive=1):
		"""Initializes the transfer object and establishes a connection to
		'host' on 'port'. The connection attempt is made in non-blocking mode
		to allow for aborting."""

		self.control_queue = control
		self.abort_flag = 0

		if passive:
			# Create the socket
			proto = socket.getprotobyname ('tcp')
			try:
				self.sock = socket.socket (socket.AF_INET, socket.SOCK_STREAM, proto)
			except socket.error, strerror:
				raise FtpException (_("Error creating socket for transfer: %s") %strerror)

			self.sock.setblocking (0)

			try:
				err = self.sock.connect_ex ((host, port))
			except socket.error, strerror:
				raise FtpException (_("Error creating socket for transfer: %s") %(strerror))

			while err == errno.EINPROGRESS and not self.abort_flag:

				time.sleep (0.25)
				err = self.sock.getsockopt (socket.SOL_SOCKET, socket.SO_ERROR)

			if err != 0 or self.abort_flag:
				raise FtpException (_("Error creating socket for transfer: %s") %os.strerror (err))
			else:
				self.sock.setblocking (1)
		else:
			# This is currently untested
			host, port = self.create_server_sock ()
			try:
				self.sock = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
				self.sock.bind (('', 0))
				self.sock.listen (1)
				dummyhost, port = self.sock.getsockname ()
				host, dummyport = control.getsockname ()
			except socket.error, (strerror):
				raise FtpException (_("Error creating socket for transfer: %s") %strerror)

			self.send_port (host, port)
			self.sock, sockaddr = self.sock.accept ()

	def send_port (self, host, port):
		"""Provides a method for sending the 'host'/'port' combination for
		a non-passive FTP transfer (meaning the listening port is opened
		on the client-side)."""

		hbytes = string.split (host, '.')
		pbytes = [ `port/256`, `port%256` ]
		bytes = hbytes + pbytes
		cmd = 'PORT ' + string.join (bytes, ',')
		return self.control.basic_cmd (cmd)

	def create_server_sock (self):
		pass

	def get_sock (self):
		return self.sock

	def get_file (self):
		return self.sock.makefile ('rb')

	def close (self):
		self.sock.close ()

	def abort (self):
		self.abort_flag = 1

class FTPTransferCmd (FTPAbstractCommand):

	"""The issue transfer sequence command object

	This models the message sequence necessary for initiating the
	actual transfer."""

	# For parsing server return codes
	re150 = re.compile ("150 .* \((\d+) bytes\)", re.IGNORECASE)

	def __init__ (self, queue, cmd, silence=0):

		self.control_queue = queue
		self.silence = silence
		self.cmd = cmd
		self.size = None

	def execute (self):

		self.control_queue.send (self.cmd)
		response = self.control_queue.wait_for_response ()

		if response[-1][0] != '1':
			raise FtpException (_("Error initiating transfer: %s") %response[-1])
		if response[-1][:3] == '150':
			self.size = self.parse150 (response[-1])

	def parse150 (self, resp):
		"""Parses the 150 return code in response string 'resp' given after a
		download is initiated for retrieval of the file size."""

		# Sanity checking
		if resp[:3] != '150':
			raise FtpException (_("Error parsing 150 response: 150 response not given"))

		matched = self.re150.match (resp)
		if matched:
			return int (matched.group (1))
		return None

	def get_size (self):
		"""Returns the file size retrieved from the 150 return code while the
		transfer is initiated."""

		return self.size


class FTPTransfer:

	"""FTP transfer object

	This models the sequence necessary for initaited an FTP transfer of any
	type (textual/binary). It does not say anything about the type of transfer
	but rather provides an interface for settings up and managing the transfer
	connection."""

	def __init__ (self, queue, rest=None, passive=1, silence=0):

		self.control_queue = queue
		self.silence = silence
		self.rest = rest
		self.passive = passive

	def initiate_transfer (self):
		"""Creates and initiates the connection for file transfer (negotiating a
		transfer port and the actual connection process."""

		if self.passive:
			cmd = FTPBasicCmd (self.control_queue, 'PASV', silence=self.silence)
			cmd.execute ()
			host, port = self.parse227 ( cmd.get_response()[-1] )

			self.transfer = FTPTransferConnection (self.control_queue, host, port, passive=self.passive)

			if self.rest is not None:
				cmd = FTPPendingCmd (self.control_queue, "REST %s" %self.rest, silence=self.silence)
				cmd.execute ()

			if not self.silence:
				gtk.threads_enter ()
				main.app.ftp_local_tell_user (self.cmd)
				gtk.threads_leave ()

			cmd = FTPTransferCmd (self.control_queue, self.cmd, silence=self.silence)
			cmd.execute ()

			self.size = cmd.get_size ()
		else:
			# Non passive stuff will go here
			pass

	def parse227 (self, resp):
		"""Parses the response string 'resp' which contains the passive port to connect to
		when setting up the transfer connection."""

		# Sanity checking
		if resp[:3] != '227':
			raise FtpException (_("Error parsing 227 response: 227 response not given"))

		left_bracket = string.find (resp, '(')
		right_bracket = string.find (resp, ')')
		if left_bracket < 0 or right_bracket < 0:
			raise FtpException (_("Error parsing 227 response: Format should be (h1,h2,h3,h4,p1,p2) and given: %s") %resp)
		fields = string.split (resp[left_bracket + 1: right_bracket], ',')
		if len (fields) != 6:
			raise FtpException (_("Error parsing 227 response: Unable to pack appropriate number of fields"))
		host = string.join (fields[:4], '.')
		port = (int (fields[4]) << 8) + int (fields[5])
		return host, port

	def abort (self):
		self.transfer.abort ()

class FTPList (FTPTransfer):

	"""An FTP list command object

	Models the sequence necessary for retrieving a remote FTP listing. Methods
	are also provided for performing futher processing on the data."""

	def __init__ (self, queue, cmd, callback=None, rest=None, passive=1, silence=0):
		"""Initializes the list object by calling the base class to set up the
		actual transfer and initializes necessary instance variables. 'callback' is
		a function reference that is executed at the end of the transfer with a
		reference to the data. If 'callback' is none, the remote file window is
		updated by default."""

		FTPTransfer.__init__ (self, queue, rest=rest, passive=passive, silence=silence)
		self.control_queue = queue
		self.silence = silence
		self.callback = callback
		self.cmd = cmd
		self.abort_flag = 0
		self.data = [ ]              # Local copy of data

	def execute (self):

		# Set the appropriate transfer type
		type = FTPBasicCmd (self.control_queue, 'TYPE A', silence=self.silence)
		type.execute ()

		# Now prepare for the transfer
		self.initiate_transfer ()

		self.sock = self.transfer.get_sock ()
		self.file = self.transfer.get_file ()

		bytes = 0

		while not self.abort_flag:
			line = self.file.readline ()
			if not line:
				break
			if line[-2:] == CRLF:
				line = line[:-2]
			elif line[-1:] in CRLF:
				line = line[:-1]

			# For updating the status window
			bytes = bytes + len (line)
			if not self.silence:
				gtk.threads_enter ()
				main.app.update_list_transfer (bytes)
				gtk.threads_leave ()

			self.data.append (line)

		# Wait for the finished transfer and then get the response
		self.file.close ()
		self.sock.close ()
		self.control_queue.wait_for_response ()

		# Now reset the status window to the current directory
		gtk.threads_enter ()
		main.app.list_transfer_finished ()
		gtk.threads_leave ()

		# Check if we should execute the call back
		if self.callback is None:
			gtk.threads_enter ()
			main.app.update_remote_listing (self.get_data ())
			gtk.threads_leave ()
		else:
			self.callback (self.get_data ())

	def get_data (self):
		"""Returns a reference to an array of the actual listing."""

		return self.data

	def abort (self):

		self.transfer.abort ()
		self.abort_flag = 1

class FTPAsciiTransfer (FTPTransfer):

	"""An FTP ascii transfer command object

	Models the sequence necessary for performing an ascii file transfer and
	handles writing the transferred file to disk."""

	def __init__ (self, queue, cmd, local_file, direction, progress=None, passive=1, silence=0):
		"""Initializes the ascii transfer object by first setting up the transfer
		connection when the base class is initialized and then appropriately
		initalizing instance variables. The 'direction' variable can take on the
		values of ftp.DOWNLOAD and ftp.UPLOAD respectively. 'progress' points to
		an object that can implements the "update_transfer" method and allows for
		tracking of the transfer's status. A file is created from the socket and
		readline is used for the actual transfer."""

		FTPTransfer.__init__ (self, queue, passive=passive)
		self.control_queue = queue
		self.cmd = cmd
		self.local_file = local_file
		self.direction = direction
		self.silence = silence
		self.progress = progress
		self.passive = passive
		self.abort_flag = 0               # For aborting the transfer

	def execute (self):

		# Set the appropriate transfer type
		type = FTPBasicCmd (self.control_queue, 'TYPE A', silence=self.silence)
		type.execute ()

		# Now prepare for the transfer
		self.initiate_transfer ()

		self.sock = self.transfer.get_sock ()
		self.file = self.transfer.get_file ()

		if self.direction == DOWNLOAD:
			file = open (self.local_file, 'w')

			while not self.abort_flag:
				line = self.file.readline ()
				if not line:
					break
				if self.progress is not None:
					gtk.threads_enter ()
					self.progress.update_transfer (len (line))
					gtk.threads_leave ()
				file.write (line)
		elif self.direction == UPLOAD:
			file = open (self.local_file, 'r')

			while not self.abort_flag:
				line = file.readline ()
				if not line:
					break
				self.sock.send (line)
				if self.progress is not None:
					gtk.threads_enter ()
					self.progress.update_transfer (len (line))
					gtk.threads_leave ()

		# Wait for the finished transfer and then get the response
		self.file.close ()
		self.sock.close ()
		self.control_queue.wait_for_response ()

		# Update our status
		if self.direction == DOWNLOAD:
			gtk.threads_enter ()
			main.app.add_to_download_list (self.local_file)
			main.app.update_local_listing ('.')
			gtk.threads_leave ()
		elif self.direction == UPLOAD:
			gtk.threads_enter ()
			main.app.main_thread.list ()
			gtk.threads_leave ()

	def abort (self):
		self.abort_flag = 1

class FTPBinaryTransfer (FTPTransfer):

	"""An FTP binary transfer command object

	Models the sequence necessary for performing a binary file transfer and
	handles writing the transferred file to disk."""

	def __init__ (self, queue, cmd, local_file, direction, progress=None, blocksize=8192, rest=None, passive=1, silence=0):
		"""Initializes the binary transfer object by first setting up the transfer
		connection when the base class is initialized and then appropriately
		initalizing instance variables. The 'direction' variable can take on the
		values of ftp.DOWNLOAD and ftp.UPLOAD respectively. 'progress' points to
		an object that can implements the "update_transfer" method and allows for
		tracking of the transfer's status. The 'blocksize' is used for figuring
		out how much should be read from the socket and written to disk in a
		given time."""

		FTPTransfer.__init__ (self, queue, rest, passive)
		self.control_queue = queue
		self.cmd = cmd
		self.local_file = local_file
		self.direction = direction
		self.silence = silence
		self.progress = progress
		self.blocksize = blocksize
		self.rest = rest
		self.passive = passive
		self.abort_flag = 0               # For aborting the transfer

	def execute (self):

		# Set the appropriate transfer type
		type = FTPBasicCmd (self.control_queue, 'TYPE I', silence=self.silence)
		type.execute ()

		# Now prepare for the transfer
		self.initiate_transfer ()

		self.sock = self.transfer.get_sock ()

		# Perform the actual transferring of the data
		if self.direction == DOWNLOAD:
			if self.rest is not None:
				file = open (self.local_file, 'ab')
			else:
				file = open (self.local_file, 'wb')

			while not self.abort_flag:
				data = self.sock.recv (self.blocksize)
				if not data:
					break
				if self.progress is not None:
					gtk.threads_enter ()
					self.progress.update_transfer (len (data))
					gtk.threads_leave ()
				file.write (data)
		elif self.direction == UPLOAD:
			file = open (self.local_file, 'rb')

			if self.rest is not None:
				file.seek (self.rest)

			while not self.abort_flag:
				data = file.read (self.blocksize)
				if not data:
					break
				self.sock.send (data)
				if self.progress is not None:
					gtk.threads_enter ()
					self.progress.update_transfer (len (data))
					gtk.threads_leave ()

		file.close ()
		self.sock.close ()
		self.control_queue.wait_for_response ()

		# Update our status
		if self.direction == DOWNLOAD:
			gtk.threads_enter ()
			main.app.add_to_download_list (self.local_file)
			main.app.update_local_listing ('.')
			gtk.threads_leave ()
		elif self.direction == UPLOAD:
			gtk.threads_enter ()
			main.app.main_thread.list ()
			gtk.threads_leave ()

	def abort (self):
		self.abort_flag = 1

class FTP:

	"""The FTP core functionality class

	This class provides a wrapper for managing all aspects of the ftp session,
	including settings up, tearing down, and issuing commands in the ftp
	control session, as well as initiating file transfers. This class provides
	the all-encompassing interface to FTP functionality. Each instance is useful
	for interacting with a single FTP session."""

	def __init__ (self, host=None, port=None, silence=0, logging=0):
		"""Some sanity checking is performed and the control session object
		is created and initialized with destination 'host':'port'."""

		# Initialize instance variables
		self.passive_server = 1
		self.silence = silence
		self.handler = None

		# Sanity checking on our options
		if host is None:
			raise FtpException (_("Error opening FTP connection: No host given"))
		if port is None or not (port > 0 and port < 65534):
			raise FtpException (_("Error opening FTP connection: Invalid port given"))

		self.control_connect = FtpControlConnection (host, port, silence=self.silence, logging=logging)
		self.control_connect.start ()

	def destroy (self):
		"""Destroys the current control session, tearing down the connection."""

		self.control_connect.destroy ()

	def set_exception_handler (self, callback):
		"""Sets an exception handler of type:
		      def handler (msg)
		which is executed upon receipt of an exception during execution in a
		separate thread. Setting a handler of None clears the exception
		handler"""

		self.handler = callback

	def set_log_active (self, bool):
		"""Toggles whether logging of FTP commands and transfers is active."""

		self.control_connect.set_log_active (bool)

	def reset (self):
		"""Clears the outgoing command queue."""

		self.control_connect.reset_queue ()

	def ftp_busy (self):
		"""Indicates whether the ftp control session is currently busy sending
		and receiving FTP message."""

		return self.control_connect.busy ()

	def add_outgoing (self, obj):
		"""Sets up the current exception handler and adds the object to
		the control object's outgoing queue."""

		self.control_connect.set_exception_handler (self.handler)
		self.control_connect.add_outgoing (obj)

	def initiate_connect (self):
		"""Sets up the FTP control session connection."""

		connect = FTPConnect (self.control_connect)
		self.add_outgoing (connect)

	def get_welcome_msg (self):
		"""Receives the FTP welcome message, which should be done promptly after
		establishing a connection."""

		welcome = FTPGetWelcomeMsg (self.control_connect)
		self.add_outgoing (welcome)

	def perform_login (self, username, password):
		"""Performs the login sequence using 'username' and 'password'."""

		login = FTPLogin (self.control_connect, username, password, '', silence=self.silence)
		self.add_outgoing (login)

	def noop (self):
		"""Performs a "no operation". Useful for keeping the FTP connection
		from idling."""

		cmd = 'NOOP'
		ftpcmd = FTPVoidCmd (self.control_connect, cmd, silence=self.silence)
		self.add_outgoing (ftpcmd)

	def cwd (self, dir):
		"""Changes the remote directory to 'dir'."""

		# Default to '/' if dir is blank
		if not dir:
			dir = '/'

		if dir == '..':
			cmd = "CDUP"
		elif dir == ".":
			# Actually do nothing and return
			return
		else:
			cmd = "CWD " + dir
		ftpcmd = FTPVoidCmd (self.control_connect, cmd, silence=self.silence)
		self.add_outgoing (ftpcmd)

	def delete (self, file):
		"""Deletes the remote 'file' in the current remote directory."""

		cmd = 'DELE ' + file
		ftpcmd = FTPVoidCmd (self.control_connect, cmd, silence=self.silence)
		self.add_outgoing (ftpcmd)

	def rename (self, name, new_name):
		"""Renames the remote file from 'name' to 'new_name'."""

		cmd = 'RNFR ' + name
		ftpcmd = FTPPendingCmd (self.control_connect, cmd, silence=self.silence)
		self.add_outgoing (ftpcmd)
		cmd = 'RNTO ' + new_name
		ftpcmd = FTPVoidCmd (self.control_connect, cmd, silence=self.silence)
		self.add_outgoing (ftpcmd)

	def mkdir (self, directory):
		"""Creates the 'directory' in the current remote directory."""

		cmd = 'MKD ' + directory
		ftpcmd = FTPVoidCmd (self.control_connect, cmd, silence=self.silence)
		self.add_outgoing (ftpcmd)

	def rmdir (self, directory):
		"""Removes the 'directory' in the current remote directory."""

		cmd = 'RMD ' + directory
		ftpcmd = FTPVoidCmd (self.control_connect, cmd, silence=self.silence)
		self.add_outgoing (ftpcmd)

	def abort (self):
		"""Aborts the currently issuing command."""

		self.control_connect.reset_queue ()
		aborcmd = FTPAbortCmd (self.control_connect, silence=self.silence)

		# Check if we're cancelling an outstanding transfer such as a binary
		# transfer or list
		executing = self.control_connect.get_executing_obj ()
		if isinstance (executing, FTPTransfer):
			executing.abort ()
		elif isinstance (executing, FTPConnect):
			self.control_connect.destroy ()
		self.add_outgoing (aborcmd)

	def list (self, args=None, callback=None, rest=None):
		"""Retrieves a remote listing."""

		cmd = 'LIST'
		if args is not None:
			for i in args:
				if i:
					cmd = cmd + (' ' + i)
		listcmd = FTPList (self.control_connect, cmd, callback=callback, silence=self.silence)
		self.add_outgoing (listcmd)

	def retrieve_ascii (self, file, local_dir, progress, passive=1):
		"""Performs an ascii retrieval of a file."""

		cmd = 'RETR ' + file
		local_file = os.path.join (local_dir, file)
		asciicmd = FTPAsciiTransfer (self.control_connect, cmd, local_file, DOWNLOAD, progress, passive, silence=self.silence)
		self.add_outgoing (asciicmd)

	def upload_ascii (self, file, local_dir, progress, passive=1):
		"""Performs an ascii upload."""

		cmd = 'STOR ' + file
		local_file = os.path.join (local_dir, file)
		asciicmd = FTPAsciiTransfer (self.control_connect, cmd, local_file, UPLOAD, progress, passive, silence=self.silence)
		self.add_outgoing (asciicmd)

	def retrieve_binary (self, file, local_dir, progress, blocksize=8192, passive=1, rest=None):
		"""Performs a binary retrieval of a file."""

		cmd = 'RETR ' + file
		local_file = os.path.join (local_dir, file)
		binarycmd = FTPBinaryTransfer (self.control_connect, cmd, local_file, DOWNLOAD, progress, blocksize, rest, passive, silence=self.silence)
		self.add_outgoing (binarycmd)

	def upload_binary (self, file, local_dir, progress, blocksize=8192, passive=1, rest=None):
		"""Uploads a binary file."""

		cmd = 'STOR ' + file
		local_file = os.path.join (local_dir, file)
		binarycmd = FTPBinaryTransfer (self.control_connect, cmd, local_file, UPLOAD, progress, blocksize, rest, passive, silence=self.silence)
		self.add_outgoing (binarycmd)
