Forcing attachments to always download

Jake Howlett, 19 May 2002

Category: Java; Keywords: Servlet download attachment

Knowing how a certain browser on a certain operating-system will respond to a click on a link to a file of a certain type is something of a guessing-game. We know and expect that clicking a link to an HTML will open that file in the browser. Anything else and it depends on what software the machine has installed and how it's configured. To some extent it also depends how the web server itself is configured.

Take an Excel file for example. When you click a link that ends in .xls and you have MS Office installed you may expect this to open in-line in the browser. Whether or not this happens depends on whether the browser is setup to allow it to do so. Not as simple as you may have thought. In order to fully understand the whole "saga" it's well worth reading this attempt to demystify it all.

These uncertainties in the behaviour of links can make designing applications where you want to have some files available for download very awkward. More than once I've had to include, alongside my "download" links, text along the lines of:

To download this file, right-click the link and, in Internet Explorer choose 'Save target as' in Netscape choose....."


The problem:

You're getting the picture right? Wouldn't it be nice if there were a way we could guarantee that every time a user clicked a link that said download, that that was what actually happened.

You could of course do this by fudging all files that are available for download in to Zip files. This way you can be pretty sure of the result. Not perfect though - how will you get all the files uploaded in to Zip archives for a start?

What we need is a way to tell the browser that they should always prompt the user to download the file. This is, in essence, not as hard as it may sound.

The solution:

It's not often that reading the Notes.net forum leaves me intrigued. However, when I read this post it got me thinking - can this be done? Sure enough, a quick search on Google led me to an answer and I was happy to be able to respond - even if they didn't keep their promise.

This wasn't enough though - I was curious. Surely something like this could be really useful. My initial response to the post on the forum was enough to prove a point but it wasn't very practical. It only works for text-based content that you can stream back to the browser using the agent's PrintWriter object i.e. .txt, .csv, .xml etc. What about attachments of a binary content like .exe and .gif?!

Having written the simple Java agent that could stream text back to the browser in the guise of a file of any name I was pretty much reaching as far as my Java knowledge at the time would let me.

At the same time as I was playing with these ideas I got a mail from Jon LeDrew of Newfoundland, Canada, that included some servlets that he wanted to share with us all. Not wanting to miss the oppurtunity of a knowledgeable Java brain, willing to share, I asked him his thoughts on the download code and this is about the point where he took over. I would love to say I had written all the code for this servlet myself but that would be a lie. Luckily Jon's boss decided this would be a "nice to have" for their intranet and so he could then justify the time he spent on the code. Which was nice.

While Jon sent me regular updates of the code I also passed this on the Brendon Upson who knows a hell of a lot when it comes to Java. This he will prove when he gets round to releasing his amazing Puakma web server. With his suggestions "we" could then make major improvements to the performance of the servlet and get it to the point where it was practical to use it in a live environment.

Using it:

Whether or not you carry on to the next section and see what code's involved is down to you. What you'll need to do though is see it in action. Download the files I've attached to this document. Place the database on a Notes server and put the .class servlet file in the server's Servlet directory. Open the database in the browser at its root and follow the instructions on that page.

You will notice that when you create a document and re-open it after having uploaded a file there are a pair of lists of the attachments. One set of links opens the files "normally" via the normal Domino URL access to the attachment while the other references the attached file via the servlet. To use the servlet to get to a file the syntax is similar to the following:

http://hostname/servlet/GetNotesAttachments?dbpath=dir/db.nsf
&unid=637D5FAF2F8B993580256BA9005ED9FB&filename=myphoto.jpg


Notice that the three parameters that we pass in are enough to mean that you can use the servlet to download a file from any database. That being one of the pros of using servlets. If this were an agent then in which database would it belong?

Having tested the servlet with various file types and sizes up to about 20MB in size I can see hardly any issue with performance. Download times seem to be the same as, if not quicker than, the usual method. How this translates to an environment with multiple users and downloads remains to be seen.

The code:

The key to this whole solution lies in our ability to specify the type of the content. We do this with the setContentType method of the servlet's response object.

response.setContentType("application/download");

Another essential line of code is the one that adds the field "Content-Disposition" to the response's header. With this we let the browser know it's receiving an attachment and what the default name should be for the user to save this file as. We do that with the following code:

String filename = request.getParameter("filename");
response.setHeader("Content-Disposition","attachment;filename=\"" + filename + "\"");

We then need to stream the whole file to the browser. To do this we use a BufferedInputStream object. In to this we place the contents of the file. The servlet contains a method called "getAttachment()". In to this we pass all three of the servlet's parameters in a call like below:

BufferedInputStream bis = getAttachment(unid, filename, dbpath);

Assuming that the above method is successful in getting the attachment out of the Notes document we can carry on now, using the BufferedInputStream object that's returned. The first thing we need to do is find out how big this object is. We then create a byte array that holds every byte of the attached file.

int bytesA = bis.available();
response.setContentLength(bytesA);
byte[] attachment = new byte[bytesA];

Notice in the above code that we also set the Content-Length field of the header. This is how, when you're downloading a file, the browser knows you've downloaded "X of 10.3 MB" and have X minutes remaining.

Download times..

The next important code fragment is the one that does all the work. After getting a handle on an OutputStream for the servlet we pass every byte of the attachment in to it and subsequently to the browser. Here's how:
ServletOutputStream op = response.getOutputStream();

while (true) {
int bytesRead = bis.read(attachment, 0, attachment.length);
if (bytesRead < 0)
break;
op.write(attachment, 0, bytesRead);
}
Obviously there's a lot more to it than the above snippets. If you want to know more about what's going on and see how you can tailor it to you own needs, there's a copy of the source code attached to this document and stored in the sample database. Enjoy.

Afterthoughts:

As it stands the servlet has a major problem. If a user cancels the download before it has finished then the servlet doesn't "tidy up" properly and this leads to the JVM running out of memory and the HTTP server becoming unresponsive. Obviously this isn't acceptable. As soon as I get it sorted I'll publish it again as v1.0.1 or something. Unless one of you guys can find the answer before that.

What I've also failed to mention in all of this is that there is an inherent security problem involved. Using the above URL format it doesn't take a genius to work out that they can pass in the location of any file attachment and the servlet will happily fetch it for them. What's needed is the servlet to use the getRemoteUser() method and to make sure the user isn't trying to get at files they wouldn't normally have permissions to.

Note that Internet Explorer 5.5 SP1 has problems with Content-Disposition: Attachment. Apart from that I think this is pretty much a working solution for all browsers on all platforms. Not very often you hear me saying something like that is it ;o)