How to show the progress of a download
How to show the progress of a download using libURLSetStatusCallback.
You can download the sample stack from this url: https://tinyurl.com/y8pp756b
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.
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 "https://downloads.livecode.com/learning/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,
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:
- .... (repeated many times)
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.
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.
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".
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.
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.
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.)
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.