It’s time for a bit of fun. To test, let’s use these scripts to
transfer a copy of the
Monty Python theme song audio file I have at my
website. First, let’s write a module that downloads and plays the
sample file, as shown in
Example 13-6
.
Example 13-6. PP4E\Internet\Ftp\sousa.py
#!/usr/local/bin/python
"""
Usage: sousa.py. Fetch and play the Monty Python theme song.
This will not work on your system as is: it requires a machine with Internet access
and an FTP server account you can access, and uses audio filters on Unix and your
.au player on Windows. Configure this and playfile.py as needed for your platform.
"""
from getpass import getpass
from PP4E.Internet.Ftp.getfile import getfile
from PP4E.System.Media.playfile import playfile
file = 'sousa.au' # default file coordinates
site = 'ftp.rmi.net' # Monty Python theme song
dir = '.'
user = ('lutz', getpass('Pswd?'))
getfile(file, site, dir, user) # fetch audio file by FTP
playfile(file) # send it to audio player
# import os
# os.system('getone.py sousa.au') # equivalent command line
There’s not much to this script, because it really just combines
two tools we’ve already coded. We’re reusing
Example 13-4
’sgetfile
to download, and
Chapter 6
’splayfile
module (
Example 6-23
) to play the audio
sample after it is downloaded (turn back to that example for more
details on the player part of the task). Also notice the last two
lines in this file—we can achieve the same effect by passing in the
audio filename as a command-line argument to our original script, but
it’s less direct.
As is, this script assumes my FTP server account; configure as
desired (alas, this file used to be at the
ftp.python.org
anonymous FTP site, but that site
went dark for security reasons between editions of this book). Once
configured, this script will run on any machine with Python, an
Internet link, and a recognizable audio player; it works on my Windows
laptop with a broadband Internet connection, and it plays the music
clip in Windows Media Player (and if I could insert an audio file
hyperlink here to show what it sounds like, I would…):
C:\...\PP4E\Internet\Ftp>sousa.py
Pswd?
Downloading sousa.au
Download done.
C:\...\PP4E\Internet\Ftp>sousa.py
Pswd?
sousa.au already fetched
Thegetfile
andputfile
modules themselves can be used to move the sample file
around too. Both can either be imported by clients that wish to use
their functions, or run as top-level programs to trigger self-tests
and command-line usage. For variety, let’s run these scripts from a
command line and the interactive prompt to see how they work. When run
standalone, the filename is passed in the command line toputfile
and both use password input and
default site settings:
C:\...\PP4E\Internet\Ftp>putfile.py sousa.py
ftp.rmi.net pswd?
Uploading sousa.py
Upload done.
When imported, parameters are passed explicitly to
functions:
C:\...\PP4E\Internet\Ftp>python
>>>from getfile import getfile
>>>getfile(file='sousa.au', site='ftp.rmi.net', dir='.', user=('lutz', 'XXX'))
sousa.au already fetched
C:\...\PP4E\Internet\Ftp>del sousa.au
C:\...\PP4E\Internet\Ftp>python
>>>from getfile import getfile
>>>getfile(file='sousa.au', site='ftp.rmi.net', dir='.', user=('lutz', 'XXX'))
Downloading sousa.au
Download done.
>>>from PP4E.System.Media.playfile import playfile
>>>playfile('sousa.au')
Although Python’sftplib
already automates the underlying socket and message formatting chores
of FTP, tools of our own like these can make the process even
simpler.
If you read the preceding
chapter, you’ll recall that it concluded with a quick look
at scripts that added a user interface to a socket-basedgetfile
script—one that transferred files over
a proprietary socket dialog, instead of over FTP. At the end of that
presentation, I mentioned that FTP is a much more generally useful way
to move files around because FTP servers are so widely available on the
Net. For illustration purposes,
Example 13-7
shows a simple mutation
of the prior chapter’s user interface, implemented as a new subclass of
the preceding chapter’s general form builder,
form.py
of
Example 12-20
.
Example 13-7. PP4E\Internet\Ftp\getfilegui.py
"""
#################################################################################
launch FTP getfile function with a reusable form GUI class; uses os.chdir to
goto target local dir (getfile currently assumes that filename has no local
directory path prefix); runs getfile.getfile in thread to allow more than one
to be running at once and avoid blocking GUI during downloads; this differs
from socket-based getfilegui, but reuses Form GUI builder tool; supports both
user and anonymous FTP as currently coded;
caveats: the password field is not displayed as stars here, errors are printed
to the console instead of shown in the GUI (threads can't generally update the
GUI on Windows), this isn't 100% thread safe (there is a slight delay between
os.chdir here and opening the local output file in getfile) and we could
display both a save-as popup for picking the local dir, and a remote directory
listing for picking the file to get; suggested exercises: improve me;
#################################################################################
"""
from tkinter import Tk, mainloop
from tkinter.messagebox import showinfo
import getfile, os, sys, _thread # FTP getfile here, not socket
from PP4E.Internet.Sockets.form import Form # reuse form tool in socket dir
class FtpForm(Form):
def __init__(self):
root = Tk()
root.title(self.title)
labels = ['Server Name', 'Remote Dir', 'File Name',
'Local Dir', 'User Name?', 'Password?']
Form.__init__(self, labels, root)
self.mutex = _thread.allocate_lock()
self.threads = 0
def transfer(self, filename, servername, remotedir, userinfo):
try:
self.do_transfer(filename, servername, remotedir, userinfo)
print('%s of "%s" successful' % (self.mode, filename))
except:
print('%s of "%s" has failed:' % (self.mode, filename), end=' ')
print(sys.exc_info()[0], sys.exc_info()[1])
self.mutex.acquire()
self.threads -= 1
self.mutex.release()
def onSubmit(self):
Form.onSubmit(self)
localdir = self.content['Local Dir'].get()
remotedir = self.content['Remote Dir'].get()
servername = self.content['Server Name'].get()
filename = self.content['File Name'].get()
username = self.content['User Name?'].get()
password = self.content['Password?'].get()
userinfo = ()
if username and password:
userinfo = (username, password)
if localdir:
os.chdir(localdir)
self.mutex.acquire()
self.threads += 1
self.mutex.release()
ftpargs = (filename, servername, remotedir, userinfo)
_thread.start_new_thread(self.transfer, ftpargs)
showinfo(self.title, '%s of "%s" started' % (self.mode, filename))
def onCancel(self):
if self.threads == 0:
Tk().quit()
else:
showinfo(self.title,
'Cannot exit: %d threads running' % self.threads)
class FtpGetfileForm(FtpForm):
title = 'FtpGetfileGui'
mode = 'Download'
def do_transfer(self, filename, servername, remotedir, userinfo):
getfile.getfile(
filename, servername, remotedir, userinfo, verbose=False, refetch=True)
if __name__ == '__main__':
FtpGetfileForm()
mainloop()
If you flip back to the end of the preceding chapter, you’ll find
that this version is similar in structure to its counterpart there; in
fact, it has the same name (and is distinct only because it lives in a
different directory). The class here, though, knows how to use the
FTP-basedgetfile
module from earlier
in this chapter instead of the socket-basedgetfile
module we met a chapter ago. When run,
this version also implements more input fields, as in
Figure 13-2
, shown on Windows 7.
Figure 13-2. FTP getfile input form
Notice that a full absolute file path can be entered for the local
directory here. If not, the script assumes the current working
directory, which changes after each download and can vary depending on
where the GUI is launched (e.g., the current directory differs when this
script is run by the PyDemos program at the top of the examples tree).
When we click this GUI’s Submit button (or press the Enter key), the
script simply passes the form’s input field values as arguments to thegetfile.getfile
FTP utility function
of
Example 13-4
earlier in this
section. It also posts a pop up to tell us the download has begun (
Figure 13-3
).
Figure 13-3. FTP getfile info pop up
As currently coded, further download status messages, including
any FTP error messages, show up in the console window; here are the
messages for successful downloads as well as one that fails (with added
blank lines for readability):
C:\...\PP4E\Internet\Ftp>getfilegui.py
Server Name => ftp.rmi.net
User Name? => lutz
Local Dir => test
File Name => about-pp.html
Password? => xxxxxxxx
Remote Dir => .
Download of "about-pp.html" successful
Server Name => ftp.rmi.net
User Name? => lutz
Local Dir => C:\temp
File Name => ora-lp4e-big.jpg
Password? => xxxxxxxx
Remote Dir => .
Download of "ora-lp4e-big.jpg" successful
Server Name => ftp.rmi.net
User Name? => lutz
Local Dir => C:\temp
File Name => ora-lp4e.jpg
Password? => xxxxxxxx
Remote Dir => .
Download of "ora-lp4e.jpg" has failed:
550 ora-lp4e.jpg: No such file or directory
Given a username and password, the downloader logs into the
specified account. To do anonymous FTP instead, leave the username and
password fields blank.
Now, to illustrate the threading capabilities of this GUI, start a
download of a large file, then start another download while this one is
in progress. The GUI stays active while downloads are underway, so we
simply change the input fields and press Submit again.
This second download starts and runs in parallel with the first,
because each download is run in a thread, and more than one Internet
connection can be active at once. In fact, the GUI itself stays active
during downloads only because downloads are run in threads; if they were
not, even screen redraws wouldn’t happen until a download
finished.
We discussed threads in
Chapter 5
,
and their application to GUIs in Chapters
9
and
10
, but this script illustrates some
practical thread concerns:
This program takes care to not do anything GUI-related in a
download thread. As we’ve learned, only the thread that makes GUIs
can generally process them.
To avoid killing spawned download threads on some platforms,
the GUI must also be careful not to exit while any downloads are in
progress. It keeps track of the number of in-progress threads, and
just displays a pop up if we try to kill the GUI by pressing the
Cancel button while both of these downloads are in progress.
We learned about ways to work around the no-GUI rule for threads
in
Chapter 10
, and we will apply such
techniques when we explore the PyMailGUI example in the next chapter. To
be portable, though, we can’t really close the GUI until the
active-thread count falls to zero; the exit model of thethreading
module of
Chapter 5
can be used to achieve the same
effect. Here is the sort of output that appears in the console window
when two downloads overlap in time:
C:\...\PP4E\Internet\Ftp>python getfilegui.py
Server Name => ftp.rmi.net
User Name? => lutz
Local Dir => C:\temp
File Name => spain08.JPG
Password? => xxxxxxxx
Remote Dir => .
Server Name => ftp.rmi.net
User Name? => lutz
Local Dir => C:\temp
File Name => index.html
Password? => xxxxxxxx
Remote Dir => .
Download of "index.html" successful
Download of "spain08.JPG" successful
This example isn’t much more useful than a command line-based
tool, of course, but it can be easily modified by changing its Python
code, and it provides enough of a GUI to qualify as a simple, first-cut
FTP user interface. Moreover, because this GUI runs downloads in Python
threads, more than one can be run at the same time from this GUI without
having to start or restart a different FTP client tool.
While we’re in a GUI mood, let’s add a simple interface to theputfile
utility, too. The script in
Example 13-8
creates a dialog
that starts uploads in threads, using core FTP logic imported from
Example 13-5
. It’s almost the same as
thegetfile
GUI we just wrote, so
there’s not much new to say. In fact, becauseget
andput
operations are so similar from an interface perspective, most of theget
form’s logic was deliberately
factored out into a single generic class (FtpForm
), so changes need be made in only a
single place. That is, theput
GUI
here is mostly just a reuse of theget
GUI, with distinct output labels and
transfer methods. It’s in a file by itself, though, to make it easy to
launch as a standalone program.
Example 13-8. PP4E\Internet\Ftp\putfilegui.py
"""
###############################################################
launch FTP putfile function with a reusable form GUI class;
see getfilegui for notes: most of the same caveats apply;
the get and put forms have been factored into a single
class such that changes need be made in only one place;
###############################################################
"""
from tkinter import mainloop
import putfile, getfilegui
class FtpPutfileForm(getfilegui.FtpForm):
title = 'FtpPutfileGui'
mode = 'Upload'
def do_transfer(self, filename, servername, remotedir, userinfo):
putfile.putfile(filename, servername, remotedir, userinfo, verbose=False)
if __name__ == '__main__':
FtpPutfileForm()
mainloop()
Running this script looks much like running the download GUI,
because it’s almost entirely the same code at work. Let’s upload some
files from the client machine to the server;
Figure 13-4
shows the state of the GUI while
starting one.
Figure 13-4. FTP putfile input form