Download Files Async With Gio And Python

412
Recently I asked for some help on how to download a file without blocking the GUI. Thanks to everyone who contributed their expertise in the post comments: I now have my program working great.

I wanted to now share my conclusions so that others can benefit from them too. To do this I am going to first explain how this works, and secondly I have created a Python Snippet and added it to the Python Snippets libray so there is a great working example you folks can play with. You can use Acire to load the snippet and play with it. This is the first gio snippet, and I hope there will be many more.

The goal I set out with was to download a file without freezing the GUI. This was somewhat inspired from a recent Shot Of Jaq shot that we did on async programming, and I used this app as a good example to play with. Typically I had downloaded files the manual way and this had blocked my GUI hard, but I was aware that this is exactly what gio, part of the GNOME platform is here to solve.

The way async basically works is that you kick off an operation and then you wait for confirmation of the result before you proceed. It is the opposite of procedural programming: you don’t kick off an operation and in the next line process it. When you do things the async way, you start an operation and then tell it what callback should be called when it is complete. It feels very event-driven: kind of how you connect a handler to a signal in a widget so that when that signal is generated, the handler is called.

When I started playing with this the docs insinuated that read_async() and read_finish() were what I needed to use. I started off with code that looked a little like this:

def download_latest_shot(self):
    audiourl = "http://....the url to the Ogg file...."

    self.shot_stream = gio.File(audiourl)
    self.shot_stream.read_async(self.download_latest_shot_complete)

It then calls this callback:

def download_latest_shot_complete(self, gdaemonfile, result):
    f = self.shot_stream.read_finish(result).read()

    outputfile = open("/home/jono/Desktop/shot.ogg","w")
    outputfile.writelines(f)

After some helpful notes from the GNOME community, it turned out that what I really needed to use was load_contents_async() to download the full content of the file (read_async() merely kicks off a read operation) and load_contents_finish() as the callback that is called when it is complete. This worked great for me.

As such, here is the snippet which I have added to the Python Snippets library which downloads the Ubuntu HTML index page, shows it in a GUI without blocking it and writes it to the disk:

#!/usr/bin/env python
#
# [SNIPPET_NAME: Download a file asynchronously]
# [SNIPPET_CATEGORIES: GIO]
# [SNIPPET_DESCRIPTION: Download a file async (useful for not blocking the GUI)]
# [SNIPPET_AUTHOR: Jono Bacon <
 This e-mail address is being protected from spambots. You need JavaScript enabled to view it
 >]
# [SNIPPET_LICENSE: GPL]

import gio, gtk, os

# Downloading a file in an async way is a great way of not blocking a GUI. This snippet will show a simple GUI and
# download the main HTML file from ubuntu.com without blocking the GUI. You will see the dialog appear with no content
# and when the content has downloaded, the GUI will be refreshed. This snippet also writes the content to the home
# directory as pythonsnippetexample-ubuntuwebsite.html.

# To download in an async way you kick off the download and when it is complete, another callback is called to process
# it (namely, display it in the window and write it to the disk). This separation means you can download large files and
# not block the GUI if needed. 

class Example(object):
    def download_file(self, data, url):
        """Download the file using gio"""

        # create a gio stream and download the URL passed to the method
        self.stream = gio.File(url)

        # there are two methods of downloading content: load_contents_async and read_async. Here we use load_contents_async as it
        # downloads the full contents of the file, which is what we want. We pass it a method to be called when the download has
        # complete: in this case, self.download_file_complete
        self.stream.load_contents_async(self.download_file_complete)

    def download_file_complete(self, gdaemonfile, result):
        """Method called after the file has downloaded"""

        # the result from the download is actually a tuple with three elements. The first element is the actual content
        # so let's grab that
        content = self.stream.load_contents_finish(result)[0]

        # update the label with the content
        label.set_text(content)

        # let's now save the content to the user's home directory
        outputfile = open(os.path.expanduser('~') + "/pythonsnippetexample-ubuntuwebsite.html","w")
        outputfile.write(content)

ex = Example()

dial = gtk.Dialog()
label = gtk.Label()
dial.action_area.pack_start(label)
label.show_all()
label.connect('realize', ex.download_file, "http://www.ubuntu.com")
dial.run()

I am still pretty new to this, and I am sure there is plenty that can be improved in the snippet, so feel free submit a merge request if you would like to improve it. Hope this helps!