How to show the progress of a download

How to show the progress of a download using libURLSetStatusCallback.

Introduction

Before downloading a file, the libURLSetStatusCallback can be set to trigger a handler periodically as the file comes in. This handler can be written to display the status of the download in many different ways.

Create a download stack

Create a download stack

Select "New Stack- default size" from the File menu.

Drag in a button from the Tools palette and use the Inspector to change the name of the button to "Download".

Edit the script of the Download button as follows:

on mouseUp

-- put the address of the file we want to download into a variable

put "http://consulting.livecode.com/lessons/LiveCodeUserGuide.pdf" into tDownloadLink  -- 2MB

-- set up a message to show the status of the download as it progresses

libURLSetStatusCallback "showProgress", the long name of me

-- start the download process, telling LiveCode to call the "downloadComplete" handler when finished

load URL tDownloadLink with message "downloadComplete"

end mouseUp

command showProgress pURL, pStatus

-- this is the status callback message which gets called regularly during the download

-- pStatus will show some initialization messages, then

-- loading,bytesReceived,bytesTotal

put pStatus

end showProgress

We are using the "load" command here as it is a non-blocking command. You send off a load command and then the handler continues, windows can still be moved and resized so it doesn't feel as if your app has frozen. Callbacks are used to track the progress of the download and to detect the end of the download.

Compile the script, close the editor and change to the browse tool. Then click the button to test it. Watch the Message box as the file downloads and you will see various messages appear.

Now you have seen what messages the libURLSetStatusCallback produces, so let's go on to do something more interesting with this data,

What does the URL status display?

Check the Dictionary entry for URLStatus. In the Additional Comments section, you will see a list of the values that the URLstatus can have. These are what is sent to the status callback handler as it's second parameter. The first parameter is the URL of the file being downloaded.

For a successful download, you will see the following sequence of status reports:

- contacted

- requested

- loading,number_of_bytes_downloaded,total_number_of_bytes

- loading,number_of_bytes_downloaded,total_number_of_bytes

- .... (repeated many times)

- downloaded

- cached

The "cached" status message is actually sent to the "downloadComplete" handler, which we haven't written yet.

A problem will give a status of "error" or "timeout".

The messages of interest when scripting a progress indicator are the "loading" messages which come as a line with three items.

The second item shows the number of bytes already downloaded.

The third item shows the total number of bytes to be downloaded in this file.

These two numeric items give us the data we need to set up a progress bar and script the changing display.

Adding a progress bar.

Adding a progress bar.

Drag a progress bar into the window from the Tools palette. Make it wider if you like, although it doesn't matter.

Use the Inspector to change the name to "ProgressBar" (1).

Now edit the script of the Download button and replace the script with this:

on mouseUp

-- put the address of the file we want to download into a variable

put "http://consulting.livecode.com/lessons/LiveCodeUserGuide.pdf" into tDownloadLink  -- 2.3 MB

-- set up a message to show the status of the download as it progresses

libURLSetStatusCallback "showProgress", the long name of me

-- make sure the progress bar is hidden, as this property is used to initialize it

hide scrollbar "ProgressBar"

-- start the download process, telling LiveCode to call the "downloadComplete" handler when finished

load URL tDownloadLink with message "downloadComplete"

end mouseUp

command showProgress pURL, pStatus

-- this is the status callback message which gets called regularly during the download

-- pStatus will show some initialization messages, then

-- loading,bytesReceived,bytesTotal

-- using a graphical progress bar instead

-- the first time this is called, find the total number of bytes that are to be downloaded

-- use this info to set the span of the progress bar

-- wait until the download info is being received

if the number of items in pStatus = 3 then

if the visible of scrollbar "ProgressBar" = false then

put the last item of pStatus into tTotalBytes

set the startValue of scrollbar "ProgressBar" to 0

set the endValue of scrollbar "ProgressBar" to tTotalBytes

show scrollbar "ProgressBar"

end if

set the thumbPosition of scrollbar "ProgressBar" to item 2 of pStatus

end if

end showProgress

The mouseUp handler only has one new section which hides the progress bar to start with.

The showProgress handler is quite different.

Firstly, it checks the number of items in the status report. The progress bar is only interested in those with three items, showing the numeric progress of the download.

If there are three items, it checks to see if the progress bar is visible. If it is not visible, then the download has only just started and the progress bar needs to be set up to match the length of the incoming file.

The properties of a scrollbar that specify the range are startValue and endValue. So we set the startValue to 0 and the endValue to the total number of bytes in the file being downloaded. Really we don't need to set the startValue every time, but I like to do it anyway, just in case I have another function that uses the same progress indicator.

Once the progress bar has been initialized, it can be shown. This tells the script that the endValue has been set, so it doesn't have to be set every time the status callback handler runs.

The property of a scrollbar that shows progress is called the thumbPosition, or thumbPos for short. This gets set to the second item of the status report, so it shows the progress as the file comes in.

Apply this script, switch to run mode and click the Download button to test it. You should see the progress bar appear, the progress indicator move along it and stop at the end.

Note: if you click the button again, you will not see any progress indication. This is because the downloaded file is still cached in memory and so LiveCode knows that there is no need to download it again.

After the download has completed

If you look back to the last line of the mouseUp handler, where the download is actually started, you will see that it has a callback message.

When the download is finished, the "downloadComplete" handler will be called.

Not having a downloadComplete handler didn't cause any errors, but now it's time to write one.

When the download has finished, we need to do something useful with the downloaded file which is only in stored in memory at that point.

We also need to tidy up the progress display and prepare for any future downloads.

Edit the script of the Download button and add this handler to the end of the existing script:

command downloadComplete pURL, pStatus

-- this is the handler called when the download is finished

-- the pStatus variable will show the result

-- since the download is complete, there is no need to leave the progress bar visible

hide scrollbar "ProgressBar"

-- check if there was a problem with the download

if pStatus = "error" or pStatus = "timeout" then

answer error "The file could not be downloaded."

else

-- if there was no problem, save the PDF to the desktop

-- work out a file name for saving this file to the desktop

set the itemDel to slash

put last item of pURL into tFilename

put specialFolderPath("Desktop") & slash before tFileName

put URL pURL into URL ("binfile:" & tFileName)

-- open the downloaded document in the default PDF viewer just to show that it worked

launch document tFileName

-- to save memory, now unload the URL from memory

-- this also means that if you want to run the test again, it does not just use a cached version

unload pURL

end if

end downloadComplete

This handler will be called when the download is complete, either due to an error or after a complete download.

First thing is to hide the progress bar, since there will be no more progress after this.

Next, check for errors in the download.

If the download is OK, then we will save the downloaded file to the desktop.

Work out a file path for this, using the last portion of the download address (remember this is sent to the handler as the first parameter) and the specialFolderPath() function.

Once a file has been downloaded and is cached in memory, it can be accessed any time using the URL keyword.

So to save the file, just "put" the URL into the file path. Don't forget to use binfile: not file: since this is not just a text file.

We should really check to see that the save worked, but for now, we are just going to open the downloaded PDF in the default PDF application.

The final step is to unload the downloaded file from memory. This saves memory, so it is a good idea after a download, once the downloaded file has been saved.

For this example, it is essential to allow repeated tests. If this doesn't happen, any further download attempts will just go straight to the downloadComplete handler with a status of "cached".

More feedback would be good.

So we now have a nice bar that shows the progress of the download, but there are some other details that would be nice to display.

The progress bar doesn't tell us anything about the size of the download, or the speed of download.

We have the number of bytes downloaded in the URL status, so if we record the starting time of the download, we can calculate the speed and display this information.

To store the start time of the download, we are going to use a script local variable. This is a variable that is declared as a local variable outside any handler. The top of the script is usually the best place for these. The value of this variable is available to any handler in the script, but not to anything outside the script. Using this technique, we can store a starting time in the mouseUp handler and use it in the showProgress handler to see how much time has elapsed and how fast the data is coming through.

The other neat thing we can do is to change the download figures from bytes to kilobytes. This makes the numbers much more readable, especially as they are changing fast.

Progress text

Drag a field from the Tools palette and use the Inspector to name it "ProgressField". Drag the handles to make it nearly as wide as the stack.

Edit the script of the Download button and insert the following line at the top of the script, before the mouseUp handler starts. This line must be outside all of the handlers.

local sDownloadStart

Add these two lines to the mouseUp handler - I have them just after the line hiding the scrollbar, but they can be anywhere, so long as they are before the line that starts the download.

put empty into field "ProgressField"

put the seconds into sDownloadStart

Now replace the showProgress handler with the version below:

command showProgress pURL, pStatus

-- this is the status callback message which gets called regularly during the download

-- pStatus will show some initialisation messages, then

-- loading,bytesReceived,bytesTotal

-- using a graphical progress bar instead

-- ths first time this is called, find the total number of bytes that are to be downloaded

-- use this info to set the span of the progress bar

-- wait until the download info is being received

if the number of items in pStatus = 3 then

if the visible of scrollbar "ProgressBar" = false then

put the last item of pStatus into tTotalBytes

set the startValue of scrollbar "ProgressBar" to 0

set the endValue of scrollbar "ProgressBar" to tTotalBytes

show scrollbar "ProgressBar"

end if

set the thumbPosition of scrollbar "ProgressBar" to item 2 of pStatus

end if

-- better text information

if the number of items in pStatus = 3 then

put item 2 of pStatus into tBytesReceived

put item 3 of pStatus into tTotalBytes

-- this gives very large numbers that are not easily read, so convert to KB

put tBytesReceived div 1024 into tKBreceived

put tTotalBytes div 1024 into tTotalKB

-- calculate speed

put the seconds - sDownloadStart into tElapsedSeconds

if tElapsedSeconds = 0 then

-- make sure we don't divide by zero at the start

put "unknown" into tKBperSecond

else

put round(tKBreceived / tElapsedSeconds, 1) into tKBperSecond

end if

put "Received " & tKBreceived & " KB of " & tTotalKB & " KB at " into field "ProgressField"

put tKBperSecond & " KB/sec" after field "ProgressField"

end if

end showProgress

The first part is exactly the same, but there is a new part added on to show the text data. It gets the two numbers (bytes received and total bytes) and divides them by 1024 to convert to kilobytes. I use the "div" command as that divides and rounds, so we will get integers here instead of numbers with lots of decimals.

The sDownloadStart script local variable was set to the seconds in the mouseUp handler, now we use it to work out the number of seconds that have elapsed since the download started. At the start, this may by zero seconds, so I have added a check to make sure we don't get a divide by zero error. Then the KB received divided by the elapsed seconds gives KB per second. This time I use the round function to get a number with one decimal place.

The rest of the script just displays the text in the ProgressField field.

(The text progress section duplicates the check for the number of items in pStatus, but I wanted to make each section independent.)

Finishing touches

Finishing touches

Now the scripts are all in place, you can arrange the interface to suit your application. Choose "Show Invisible Objects" from the View menu to see the hidden progress bar so that you can move it around and resize it.

You can also add a section to the downloadComplete handler that tidies up the text display at the end of the download. It could blank it or change it to show the result of the download and the overall download speed.

If you have a fast connection, you may want to download a larger file to test these progress displays.

There is a line at the start of the mouseUp handler that sets the tDownloadLink variable. Replace it with the following line for a bigger PDF:

put "http://livecodestatic.com/downloads/livecode/9_0_0/LiveCodeCommunity-9_0_0_dp_10-Mac.dmg" into tDownloadLink    -- 408 MB

or use any download link you prefer.

For details on uploading a file, check out Uploading a file using FTP.

The scripts in this lesson can also be used to show the progress of an upload.

3 Comments

Tim Franklin

Hi, first this is not a comment, so you do not have to approve it, but I was interested in getting it to work,

I was looking at your tutorial, which is very interesting, but I could not get it to work when I tested, for some reason, I keep getting the error message the file could not be downloaded,

I tried a few things, but could not get it to work, I thought perhaps I had somehow, did a typo or some other issue but could not find the issue, so I tried another solution using the liburl library and it too failed, so I am wondering if now this may not be something broken or if it is just an error in my attempt to follow your directions,

do you have a stack example that I could test out to see if it is in the lib or in the syntax?

thanks in advance Tim

Tim Franklin

Very nice example, works great inside the IDE, but in my tests outside the IDE, the message path seems to stop with the callback message.

I was hoping that I could find out why this happens, because I cannot seem to trace the message path outside the IDE.

Sarah Reichelt

There was a typo in the script for showing progress text - in the mouseUp handler, the sDownloadStart script local variable needed to be set to the seconds, not to 0 as described in the original lesson. This is now corrected in the lesson text above.

I have attached my test stack to this lesson. I tested it as a standalone on Mac OS X, and that worked fine.

Sarah

Add your comment

E-Mail me when someone replies to this comment