Sequentially download multiple files (with jQuery)

On my previous job, I was working together with a colleague on a file transfer application which lived in the browser. We were up -and downloading multiple files at the same time and needed a rich user experience. To ensure the best experience when uploading we turned to swfupload, it's free and I highly recommend it. But download was another story. The user experience for downloading files is actually something for which we entirely depend on the browser. It's not that there is a lot of choice. You click a link to a file which the browser can't open then you get a save dialog box and the browser shows you a progress bar. All that is out of the hands of the developer you just put the link there or stream the file directly to the client and let the browser do his thing.

But we offered the user multiple files and he could determine which files he wanted to download. This is a simplified mockup of how the screen looked.

image

If a user wanted to download the files he could click the links one by one and then save them. But how to make the "Download selected files" button work. After some googling I found that there are numerous solutions for upload but didn't find any decent solution for a 'richer' download experience. And after thinking it through it makes sense, because downloading requires access to the hard disk of the user. This would be a serious risk to allow javascript to write to a certain folder on disk.

So we were stuck, some suggested of zipping all the zips to one file but we didn't want to burden the end user with the extra effort of unzipping.  I got a hint on a forum post (can't remember which one sorry for that) that if you pointed the source attribute of an iframe to the file the browser would offer it as a download. So the first attempt to to this was with the following HTML code:

<html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <script src="j/jquery.js" type="text/javascript"></script> <script src="j/index.js" type="text/javascript"></script> </head> <body> <h1>Download files</h1> <iframe src="" id="download-iframe" style="display:none;" /> </body> </html>

And the following JavaScript was executed when the document was loaded.

$(function() { $('#download-iframe').attr('src', 'file1.zip'); $('#download-iframe').attr('src', 'file2.zip'); $('#download-iframe').attr('src', 'file3.zip'); });

You can see I'm using some jQuery magic. If you haven't used it yet, stop reading and go check it out. Did I already mention I'm a fan?

But there is a problem with the code. Only the last file is offered as download. I guessed, I'm not 100 % sure and please correct me if I'm wrong, that the problem was that the statements were executed directly after each other. So I used a little timeout to see if that was the solution to our problem, like so:

$(function() { var fileIndex = 0; var fileArray = [ 'file1.zip', 'file2.zip', 'file3.zip' ]; $('#download-iframe').attr('src', fileArray[fileIndex]); fileIndex++; var interval = setInterval(function() { if(fileIndex < fileArray.length) { $('#download-iframe').attr('src', fileArray[fileIndex]); fileIndex++; } else { clearInterval(interval); } }, 100); });

As you can see the code is not that much longer, I now have an array of the filenames (could be extracted from a list of checkboxes) and offer the first file as download. Then I set an interval that fires every 100 milliseconds which offers the next file as a download. Once all files have been handled, the interval is cleared and we're done.

Now the user gets prompted with 3 download dialog boxes. I know, it sucks that he still has to click three times to save each download individually. But if the user sets a default download location the files are automatically saved to that location and he doesn't have to click for each file that gets downloaded. It's certainly not perfect and from a user experience standpoint it still kinda sucks ... but it works. If you have another idea of solving this problem, please feel free to elaborate in the comments.

Tags from Technorati: ,,

Comments

January 18. 2010 10:28 PM

That worked fine until I tried to download a large file, then the timeout happened too soon.  So I dug around and came up with this solution:

In IE, you can wait for the onreadystatechange event to load on the iframe, then check the readyState to see if it is 'interactive'.  If it is, then your document has loaded and you can set the source on the next one.  Alas, you can't set it right away, or you run into the two downloads at once issue we're trying to work around.  But if you set it with a minimal timeout (I used 100 ms, but 0 ms even seems to work, you just have to get it to the back of the queue), the Open/Save dialog opens up first, and the timer events don't fire until after it closes, so everything works fine.

This won't work on anything but IE, because only IE supports readyState (I suspect it is non-standard).  Then again, I suspect the other browsers work like Firefox does.  In Firefox, you can just set all the srcs in the iframes at once, with no delay needed, and it will open up all the dialogs immediately, no problem (mind you, the user sees the last first, so you may want to set them in reverse order).

Thus, I get something like:

In IE:

var urls = ["first.txt", "second.txt", "third.txt"];
var currentUrl = 0;
var numberUrls = 3;

function onReadyStateChange()
{
    var i = document.getElementById('iframeid' + String(currentURL - 1));
    if (i.readyState == 'interactive')
    {
        window.setTimeout('continueFileDownload();', 100);
    }
}

function continueFileDownload()
{
    if ( currentUrl >= numberUrls )
        return;
    var d = document.getElementById('clientIFrames');
    var i = document.createElement('iframe');
    i.setAttribute('id', 'iframeid' + String(currentUrl));
    i.setAttribute('width', '0');
    i.setAttribute('height', '0');
    i.setAttribute('frameborder', '0');
    i.onreadystatechange = function(){OnReadyStateChange();};
    i.setAttribute('src', urls[currentUrl]);
    d.appendChild(i);
}

In Firefox:

Just use your src = code above that you threw out because it didn't work in IE.

Sorry about this not being in jQuery, but I'm not familiar enough with it to type it on the fly.  You can make the changes if you want and stick this idea into your function above.

Oh, I actually gave my users a choice of this or downloading a zip file (self extracting or not, it's up to them), because while it is nasty to deal with 15 open/save dialog boxes, it is also nasty to have to unzip a file when you just want to open two pdf files.

-Guy

Guy Schalnat

January 18. 2010 10:30 PM

I forgot to add the currentUrl = currentUrl + 1; in the above code right before the d.appendChild(i);

That's what I get for cut and paste.

-Guy

Guy Schalnat

January 20. 2010 12:47 PM

Hey Guy,

We didn't run into this problem so I'm guessing we're handling smaller files.
Your solution is pretty nice, I'll update the code when I have the time. Thanks a lot for your effort.

Peter

Peter

January 21. 2010 10:39 PM

You're welcome.  Some of my files were big monthly and yearly report type spreadsheets (or pdfs).  And it doesn't work on IE6 at all (neither of our solutions, as far as I can tell).  All I could do was to set a timeout of 5 seconds and hope the user had closed the dialog by then.  Blah!  Luckily, not many IE6 users left (and I did give them an option to zip the files, should they want, and I remember which option they picked for next time).

-Guy

Guy Schalnat

Add comment




  Country flag

Click to change captcha
biuquote
Loading