Thursday, May 21, 2009

[Python & PyQt] Ftp Client

Download:FtpClient.zip

Introduction

Recently, I'm looking for the suitable script language for GUI design. It seems Python will be the first candidate among those competitive programming languages(ex. Perl, Tcl, Awk...). Here I would like to show you a GUI-based ftp client with the combination of Python and PyQt

Project - Ftp Client

Language: Python 2.6.2
GUI: PyQt 4.4.4
OS: Ubuntu 8.04
Modules: ftplib

Pre-works

Even though this project didn't involved any difficult algorithm or complicated techniques, the program still needs lots of package for running. If you are really interest in this program, please install follow packages.

It's not too difficult to install those packages on your system. But there is an important thing I want to remind you. Don't forget to add the PyQt modules's path into your python. Below is a simple way to add the system path to your python. (For example, my PyQt modules' location is '/usr/local/lib/python2.6/site-package')

import sys
sys.path.append('/usr/local/lib/python2.6/site-package')

Maybe someone can help us to write a GUI-based environment path manager.

Coding

I split the whole source code into three parts, UiFtpCli, FileTransfer and FtpClient.

UiFtpCli
#! /usr/local/bin/python

###############################################################
# Project: Python Ftp Client
# Filename: UiFtpCli.py
# Description: The Qt-based GUI for ftp client
# Date: 2009.05.21
# Programmer: Lin Xin Yu
# E-mail: nxforce@yahoo.com
# Website: http://importcode.blogspot.com
###############################################################

#================= import modules =============================
import os
from PyQt4 import QtCore, QtGui

#================= Ui_FtpClientClass Class Begin ==============
class Ui_FtpClientClass(object):
def setupUi(self, FtpClientClass):
# MainWindow Widget
FtpClientClass.setObjectName("FtpClientClass")
FtpClientClass.resize(QtCore.QSize(QtCore.QRect(0,0,800,500).size()).expandedTo(FtpClientClass.minimumSizeHint()))
self.moveToCenter(FtpClientClass)

# Centeral Widget for containing all widgets except for menubar and statusbar
self.centralwidget = QtGui.QWidget(FtpClientClass)
self.centralwidget.setObjectName("centralwidget")

# Ftp Host label
self.label_address = QtGui.QLabel(self.centralwidget)
self.label_address.setGeometry(QtCore.QRect(10,10,70,25))
self.label_address.setObjectName("label_address")

# Ftp Host input
self.lineEdit_address = QtGui.QLineEdit(self.centralwidget)
self.lineEdit_address.setGeometry(QtCore.QRect(80,10,510,25))
self.lineEdit_address.setObjectName("lineEdit_address")

# Port Label
self.label_port = QtGui.QLabel(self.centralwidget)
self.label_port.setGeometry(QtCore.QRect(600,10,30,25))
self.label_port.setObjectName("label_port")

# Port input
self.lineEdit_port = QtGui.QLineEdit(self.centralwidget)
self.lineEdit_port.setGeometry(QtCore.QRect(640,10,50,25))
self.lineEdit_port.setObjectName("lineEdit_port")

# Ftp tranfer mode
self.transferMode = QtGui.QComboBox(self.centralwidget)
self.transferMode.setGeometry(QtCore.QRect(700,10,90,25))
self.transferMode.insertItem(0, 'Passive')
self.transferMode.insertItem(1, 'Active')

# Username label
self.label_username = QtGui.QLabel(self.centralwidget)
self.label_username.setGeometry(QtCore.QRect(10,40,70,25))
self.label_username.setObjectName("label_username")

# Username input
self.lineEdit_username = QtGui.QLineEdit(self.centralwidget)
self.lineEdit_username.setGeometry(QtCore.QRect(80,40,270,25))
self.lineEdit_username.setObjectName("lineEdit_username")

# Password label
self.label_password = QtGui.QLabel(self.centralwidget)
self.label_password.setGeometry(QtCore.QRect(360,40,70,25))
self.label_password.setObjectName("label_password")

# Password input
self.lineEdit_password = QtGui.QLineEdit(self.centralwidget)
self.lineEdit_password.setGeometry(QtCore.QRect(430,40,260,25))
self.lineEdit_password.setObjectName("lineEdit_password")
self.lineEdit_password.setEchoMode(QtGui.QLineEdit.Password)

# Connect/Disconnect button
self.pushButton_connect = QtGui.QPushButton(self.centralwidget)
self.pushButton_connect.setGeometry(QtCore.QRect(700,40,90,25))
self.pushButton_connect.setObjectName("pushButton_connect")

# TreeWidget for local file system
self.sysTree = QtGui.QTreeWidget(self.centralwidget)
self.sysTree.setGeometry(QtCore.QRect(400,75,390,400))
self.sysTree.setHeaderLabels(QtCore.QStringList(['Name', 'Size', 'Owner', 'Group','Path']));
self.sysTree.setObjectName("sysTree")
self.sysTree.setEnabled(False)
self.sysTree.setRootIsDecorated(False)
self.sysTree.header().setStretchLastSection(False)

# TreeWidget for remote file system
self.ftpTree = QtGui.QTreeWidget(self.centralwidget)
self.ftpTree.setGeometry(QtCore.QRect(10,75,390,400))
self.ftpTree.setObjectName("ftpTree")
self.ftpTree.setEnabled(False)
self.ftpTree.setRootIsDecorated(False)
self.ftpTree.setHeaderLabels(QtCore.QStringList(['Name', 'Size', 'Owner', 'Group', 'Premission']));
self.ftpTree.header().setStretchLastSection(False)

# Predefined icons
self.folderIconBlue = QtGui.QIcon("icon/folder1.png")
self.fileIconBlue = QtGui.QIcon("icon/file1.png")
self.folderIconGreen = QtGui.QIcon("icon/folder2.png")
self.fileIconGreen = QtGui.QIcon("icon/file2.png")

# Download/Upload PorgressBar
self.progressBar = QtGui.QProgressDialog(self.centralwidget)
self.progressBar.setGeometry(QtCore.QRect(200,200,200,50))
self.progressBar.setObjectName("progressBar")
self.progressBar.setMaximum(100)
self.progressBar.setMinimum(0)
self.moveToCenter(self.progressBar)

# MessageBox for welcome message
self.msgBox = QtGui.QMessageBox(self.centralwidget)
self.msgBox.setGeometry(QtCore.QRect(200,200,400,300))
self.msgBox.setObjectName("msgBox")
self.msgBox.setStandardButtons(QtGui.QMessageBox.Ok)
self.moveToCenter(self.msgBox)

# MessageBox for About
self.aboutMsg = QtGui.QMessageBox(self.centralwidget)
self.aboutMsg.setGeometry(QtCore.QRect(200,200,400,300))
self.aboutMsg.setObjectName("aboutMsg")
self.aboutMsg.setWindowTitle("Ftp Client 1.0")
self.aboutMsg.setStandardButtons(QtGui.QMessageBox.Ok)
self.aboutMsg.setInformativeText(QtGui.QApplication.translate("FtpClientClass", "Ftp Client 1.0\nAuthor: Lin Xin Yu\nE-mail:nxforce.yahoo.com\nBlog:http://importcode.blogspot.com", None, QtGui.QApplication.UnicodeUTF8))
self.moveToCenter(self.aboutMsg)

FtpClientClass.setCentralWidget(self.centralwidget)

# Menubar & actions
self.menubar = QtGui.QMenuBar(FtpClientClass)
self.menubar.setGeometry(QtCore.QRect(0, 0, 600, 22))
self.menubar.setObjectName("menubar")
self.menuFile = QtGui.QMenu(self.menubar)
self.menuFile.setObjectName("menuFile")
self.menuHelp = QtGui.QMenu(self.menubar)
self.menuHelp.setObjectName("menuHelp")
FtpClientClass.setMenuBar(self.menubar)
self.statusbar = QtGui.QStatusBar(FtpClientClass)
self.statusbar.setObjectName("statusbar")
self.statusbar.showMessage('Disconnect')
FtpClientClass.setStatusBar(self.statusbar)
self.actionConnect = QtGui.QAction(FtpClientClass)
self.actionConnect.setObjectName("actionConnect")
self.actionDisconnect = QtGui.QAction(FtpClientClass)
self.actionDisconnect.setObjectName("actionDisconnect")
self.actionQuit = QtGui.QAction(FtpClientClass)
self.actionQuit.setObjectName("actionQuit")
self.actionAbout = QtGui.QAction(FtpClientClass)
self.actionAbout.setObjectName("actionAbout")
self.menuFile.addAction(self.actionConnect)
self.menuFile.addAction(self.actionDisconnect)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionQuit)
self.menuHelp.addAction(self.actionAbout)
self.menubar.addAction(self.menuFile.menuAction())
self.menubar.addAction(self.menuHelp.menuAction())

self.retranslateUi(FtpClientClass)

# Signal & Slot
QtCore.QObject.connect(self.lineEdit_address,QtCore.SIGNAL("returnPressed()"),FtpClientClass.onSetAddress)
QtCore.QObject.connect(self.lineEdit_username,QtCore.SIGNAL("returnPressed()"),FtpClientClass.onSetUsername)
QtCore.QObject.connect(self.lineEdit_password,QtCore.SIGNAL("returnPressed()"),FtpClientClass.onSetPassword)
QtCore.QObject.connect(self.lineEdit_port,QtCore.SIGNAL("returnPressed()"),FtpClientClass.onSetPort)
QtCore.QObject.connect(self.pushButton_connect,QtCore.SIGNAL("clicked()"),FtpClientClass.onConnect)
QtCore.QObject.connect(self.sysTree,QtCore.SIGNAL("itemDoubleClicked(QTreeWidgetItem*,int)"),FtpClientClass.onSysItemDoubleClicked)
QtCore.QObject.connect(self.ftpTree,QtCore.SIGNAL("itemDoubleClicked(QTreeWidgetItem*,int)"),FtpClientClass.onFtpItemDoubleClicked)
QtCore.QObject.connect(self.transferMode, QtCore.SIGNAL("currentIndexChanged(int)"), FtpClientClass.onTransferModeChanged)
QtCore.QObject.connect(self.actionConnect, QtCore.SIGNAL('triggered(bool)'), FtpClientClass.onConnect)
QtCore.QObject.connect(self.actionDisconnect, QtCore.SIGNAL('triggered(bool)'), FtpClientClass.onConnect)
QtCore.QObject.connect(self.actionQuit, QtCore.SIGNAL('triggered(bool)'), FtpClientClass.close)
QtCore.QObject.connect(self.actionAbout, QtCore.SIGNAL('triggered(bool)'), FtpClientClass.onAboutTriggered)
QtCore.QMetaObject.connectSlotsByName(FtpClientClass)

# Text Translation for international format - UTF8
def retranslateUi(self, FtpClientClass):
FtpClientClass.setWindowTitle(QtGui.QApplication.translate("FtpClientClass", "Ftp Client", None, QtGui.QApplication.UnicodeUTF8))
self.label_address.setText(QtGui.QApplication.translate("FtpClientClass", "Ftp Host:", None, QtGui.QApplication.UnicodeUTF8))
self.label_port.setText(QtGui.QApplication.translate("FtpClientClass", "Port:", None, QtGui.QApplication.UnicodeUTF8))
self.lineEdit_port.setText(QtGui.QApplication.translate("FtpClientClass", "21", None, QtGui.QApplication.UnicodeUTF8))
self.label_username.setText(QtGui.QApplication.translate("FtpClientClass", "Username:", None, QtGui.QApplication.UnicodeUTF8))
self.label_password.setText(QtGui.QApplication.translate("FtpClientClass", "Password:", None, QtGui.QApplication.UnicodeUTF8))
self.pushButton_connect.setText(QtGui.QApplication.translate("FtpClientClass", "Connect", None, QtGui.QApplication.UnicodeUTF8))
self.progressBar.setWindowTitle(QtGui.QApplication.translate("FtpClientClass", "File Transfer", None, QtGui.QApplication.UnicodeUTF8))
self.menuFile.setTitle(QtGui.QApplication.translate("FtpClientClass", "File", None, QtGui.QApplication.UnicodeUTF8))
self.menuHelp.setTitle(QtGui.QApplication.translate("FtpClientClass", "Help", None, QtGui.QApplication.UnicodeUTF8))
self.actionConnect.setText(QtGui.QApplication.translate("FtpClientClass", "Connect", None, QtGui.QApplication.UnicodeUTF8))
self.actionDisconnect.setText(QtGui.QApplication.translate("FtpClientClass", "Disconnect", None, QtGui.QApplication.UnicodeUTF8))
self.actionQuit.setText(QtGui.QApplication.translate("FtpClientClass", "Quit", None, QtGui.QApplication.UnicodeUTF8))
self.actionAbout.setText(QtGui.QApplication.translate("FtpClientClass", "About", None, QtGui.QApplication.UnicodeUTF8))

# widget alignment
def moveToCenter(self, widget):
screen = QtGui.QDesktopWidget().screenGeometry()
size = widget.geometry()
widget.move((screen.width()-size.width())/2, (screen.height()-size.height())/2)

#================= Ui_FtpClientClass Class End ================

As you can see, everything just like C++, because I follow the original Qt style. Thus, it will be much easier to handle the source code.

FileTransfer
#! /usr/local/bin/python

###############################################################
# Project: Python Ftp Client
# Filename: FileTransfer.py
# Description: A TransferProgress class for displaing download
# or upload progress.
# Date: 2009.05.21
# Programmer: Lin Xin Yu
# E-mail: nxforce@yahoo.com
# Website:http://importcode.blogspot.com
###############################################################

#================= TransferProgress Class Start ===============

class TransferProgress:
def __init__(self, pBar, filename, filesize, msg, file=None):

# file descriptor will be used while downloding
self.file = file

# cause the value of TreeWidgetItem are all string, so typecast it
self.totalSize = int(filesize)
self.recvSize = 0

# QProgressBar
self.pBar = pBar
self.pBar.setWindowTitle(msg+filename)
self.pBar.show()

def uploadProgress(self, data):
self.recvSize += len(data)
self.pBar.setValue(self.recvSize*100 /self.totalSize)

def downloadProgress(self, data):
self.recvSize += len(data)
self.file.write(data)
self.pBar.setValue(self.recvSize*100 /self.totalSize)

#================= TransferProgress Class End =================

Displaying the downloading/uploading progress will be the most difficult part in this project. In order to get the filesize and received size, we need to create some callback functions for storbinary() and retrbinary(). uploadProgress() is the callback function for storbinary(). downloadProgress() is the callback function for retrbinary(). So, here I put all essential variables and functions into TransferProgress object.

FtpClient
#! /usr/local/bin/python

###############################################################
# Project: Python Ftp Client
# Filename: FtpClient.py
# Description: A very simple ftp client via PyQt
# Date: 2009.05.21
# Programmer: Lin Xin Yu
# E-mail: nxforce@yahoo.com
# Website:http://importcode.blogspot.com
###############################################################

#================= import modules =============================
import os
import sys
import ftplib
import UiFtpCli
import FileTransfer
from PyQt4 import QtCore, QtGui, QtNetwork

#================= FtpClient Class Start ======================

class FtpClient(QtGui.QMainWindow):
def __init__(self, parent = None):
QtGui.QMainWindow.__init__(self, parent)
self.ui = UiFtpCli.Ui_FtpClientClass()
self.ui.setupUi(self)

self.username = None
self.password = None
self.address = None
self.port = 21
self.ftp = None
self.passiveMode = True

# record current root location for local filelist
# default path is your 'Dekstop'
self.sysRootPath = QtCore.QDir.homePath()+"/Desktop"

# Update treewdiget for local filelist
self.updateSysFileList()

#=== SLOT for ftp host address input ===
def onSetAddress(self):
self.address = str(self.ui.lineEdit_address.text())

#=== SLOT for username input ===
def onSetUsername(self):
self.username = str(self.ui.lineEdit_username.text())

#=== SLOT for password input ===
def onSetPassword(self):
self.password = str(self.ui.lineEdit_password.text())

#=== SLOT for password input ===
def onSetPort(self):
self.port = str(self.ui.lineEdit_port.text())

#=== SLOT for connect/disconnect button ===
def onConnect(self):
if(self.ftp == None):
self.connectToHost()
else:
self.disconnectFromHost()

#=== connect ===
def connectToHost(self):
self.ui.lineEdit_address.emit(QtCore.SIGNAL("returnPressed()"), None)
self.ui.lineEdit_username.emit(QtCore.SIGNAL("returnPressed()"), None)
self.ui.lineEdit_password.emit(QtCore.SIGNAL("returnPressed()"), None)
self.ui.lineEdit_port.emit(QtCore.SIGNAL("returnPressed()"), None)

# user should at least enter the ftp host
if(self.address == ''):
self.ui.statusbar.showMessage('Please enter ftp host address!')
self.ui.lineEdit_address.setFocus()
return False

# username & password are not required for anonymous server

#if(self.username == ''):
# self.ui.statusbar.showMessage('Please enter username!')
# self.ui.lineEdit_username.setFocus()
# return False

#if(self.password == ''):
# self.ui.statusbar.showMessage('Please enter password!')
# self.ui.lineEdit_password.setFocus()
# return False

# Connect to ftp host
self.ftp = ftplib.FTP(self.address)

# set tranfer mode
self.ftp.set_pasv(self.passiveMode)

# login
self.ftp.login(self.username, self.password)

# Show welcome message
self.ui.msgBox.setWindowTitle(self.address)
self.ui.msgBox.setInformativeText(QtGui.QApplication.translate("FtpClientClass", self.ftp.getwelcome(), None, QtGui.QApplication.UnicodeUTF8))
self.ui.msgBox.show()

self.ui.statusbar.showMessage('Connect to '+self.address+' ...')

# update remote filelist
self.updateFtpFileList()

self.ui.sysTree.setEnabled(True)
self.ui.ftpTree.setEnabled(True)

self.ui.transferMode.setEnabled(False)
self.ui.lineEdit_address.setEnabled(False)
self.ui.lineEdit_port.setEnabled(False)
self.ui.lineEdit_username.setEnabled(False)
self.ui.lineEdit_password.setEnabled(False)
self.ui.pushButton_connect.setText('Disconnect')

return True

#=== disconnect ===
def disconnectFromHost(self):
# close ftp
self.ftp.close()

# empty object
self.ftp = None


self.ui.sysTree.setEnabled(False)
self.ui.ftpTree.setEnabled(False)

self.ui.transferMode.setEnabled(True)
self.ui.lineEdit_address.setEnabled(True)
self.ui.lineEdit_port.setEnabled(True)
self.ui.lineEdit_username.setEnabled(True)
self.ui.lineEdit_password.setEnabled(True)
self.ui.pushButton_connect.setText('Connect')
self.ui.statusbar.showMessage('Disconnected from '+self.address+' ...')

#=== upload ===
def upload(self, filename, fullname):
# open file for reading
fileout = open(fullname, "rb")

# get filesize
statinfo = os.stat(fullname)
filesize = statinfo.st_size

# create a FileTransfer object for displaying upload progress
tp = FileTransfer.TransferProgress(self.ui.progressBar, filename, filesize, 'Upload: ')

# send data to server
self.ftp.storbinary("STOR " + filename, fileout, 1024, tp.uploadProgress)

# close file
fileout.close()

#=== download ===
def download(self, filename, filesize):
# open file for writing
filein = open(self.sysRootPath+'/'+filename, "wb")

# create a FileTransfer object for displaying download progress
tp = FileTransfer.TransferProgress(self.ui.progressBar, filename, filesize, 'Download: ', filein)

# get data from server
self.ftp.retrbinary("RETR " + filename, tp.downloadProgress, 1024)

# close file
filein.close()


#=== Update treewidget for remote filelist ===
def updateFtpFileList(self):
# clear treewidget
self.ui.ftpTree.clear()

# get filelist from server
data = []
self.ftp.dir(data.append)

# re-format the structure and send into ftpTree
for line in data:
spline = line.split()
item = QtGui.QTreeWidgetItem()
item.setText(0, spline[8])
item.setText(1, spline[4])
item.setText(2, spline[2])
item.setText(3, spline[3])
item.setText(4, spline[0])

if(spline[0][0] == 'd'):
item.setIcon(0, self.ui.folderIconBlue)
else:
item.setIcon(0, self.ui.fileIconBlue)

self.ui.ftpTree.addTopLevelItem(item)


#=== Update treewidget for local filelist ===
def updateSysFileList(self):
# clear treewidget
self.ui.sysTree.clear()

# get local directory path
sysDir = QtCore.QDir(self.sysRootPath)
sysDir.setFilter(QtCore.QDir.Files | QtCore.QDir.Dirs | QtCore.QDir.NoSymLinks)
sysDir.setSorting(QtCore.QDir.DirsFirst | QtCore.QDir.Name)

# retrive the file info and pack into sysTree
fileList = sysDir.entryInfoList();
for i in range(0,len(fileList)):
fileInfo = fileList[i]
item = QtGui.QTreeWidgetItem()
item.setText(0, fileInfo.fileName())
item.setText(1, QtCore.QString.number(fileInfo.size()/1024, 10)+' KB')
item.setText(2, fileInfo.owner())
item.setText(3, fileInfo.group())
item.setText(4, fileInfo.filePath())

if(fileInfo.isDir()):
item.setIcon(0, self.ui.folderIconGreen)
else:
item.setIcon(0, self.ui.fileIconGreen)

self.ui.sysTree.addTopLevelItem(item)

#=== SLOT for ftpTree ===
def onFtpItemDoubleClicked(self, item):
permission = str(item.text(4))
if(permission[0] == 'd'):
self.ftp.cwd(str(item.text(0)))
self.updateFtpFileList()
else:
self.download(str(item.text(0)), str(item.text(1)))

#=== SLOT for sysTree ===
def onSysItemDoubleClicked(self, item):
path = str(item.text(4))
if(QtCore.QFileInfo(path).isDir()):
self.sysRootPath = path
self.updateSysFileList()
else:
self.upload(str(item.text(0)) ,path)

#=== SLOT for transfer mode combobox ===
def onTransferModeChanged(self, index):
if(index == 0):
self.passiveMode = True
else:
self.passiveMode = False

#=== SLOT for menubar's actions ===
def onAboutTriggered(self):
self.ui.aboutMsg.show()

#=== SLOT for close window ===
def closeEvent(self, event):
if(self.ftp != None):
self.ftp.close()

#================= FtpClient Class End ======================

if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
ftpClient = FtpClient()
ftpClient.show()
sys.exit(app.exec_())

I spend almost over 90 percent coding time on graphic user interface design, only 10 percent is core algorithm. So if you don't really understand the detailed concept of the source code. Don't worry about it, practice makes perfect.

Demo


Conclusion
I didn't implement too many function for this program, so ... maybe it's your turn.
  • If you are C++ & Qt expertise, don't try to switch to PyQt if no necessary.
  • There are too many Qt classes that overlap with python classes. Before using Qt class, please make sure the implementation is stable and completed
  • Interpreted language is much faster than compiled language while compiling.
  • For small-scale, non-commercial, client oriented software, python is still good for programmers.

0 意見:

Post a Comment