Friday, July 20, 2007

FileServlet

Serve your files

If you have stored files in a path outside of the web container or in a database, then the client cannot access the files directly by a relative URI. A good practice is to create a Servlet which loads the file from a path outside of the web container or from a database and then streams the file to the HttpServletResponse. The client should get a 'Save as' popup dialogue, thanks to the Content-disposition header being set to attachment. You can pass the file name or the file ID as a part of the request URI. You can also consider to pass it as a request parameter, but that would cause problems with getting the filename right during saving in certain web browsers (Internet Explorer and so on).

Back to top

FileServlet serving from absolute path

Here is a basic example of a FileServlet which serves a file from a path outside of the web container.

package mypackage;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLConnection;
import java.net.URLDecoder;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * The File servlet for serving from absolute path.
 * @author BalusC
 * @link http://balusc.blogspot.com/2007/07/fileservlet.html
 */
public class FileServlet extends HttpServlet {

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException
    {
        // Define base path somehow. You can define it as init-param of the servlet.
        String filePath = "/files";
        // In a Windows environment with the Applicationserver running on the
        // c: volume, the above path is exactly the same as "c:\files".
        // In UNIX, it is just straightforward "/files".
        // If you have stored files in the WebContent of a WAR, for example in the
        // "/WEB-INF/files" folder, then you can retrieve the absolute path by:
        // String filePath = getServletContext().getRealPath("/WEB-INF/files");
        
        // Get file name from request URI.
        String requestURI = request.getRequestURI();
        String fileName = requestURI.substring(requestURI.lastIndexOf('/') + 1);

        // Check if file name is actually supplied to the request URI (and is not the servlet itself).
        if (request.getServletPath().endsWith(fileName)) {
            // Do your thing if the file name is not supplied to the request URI.
            // Throw an exception, or show default/warning page, or just ignore it.
            response.sendRedirect("/FileNotFoundError.jsp");
            return;
        }

        // Decode the file name and prepare file object.
        fileName = URLDecoder.decode(fileName, "UTF-8");
        File file = new File(filePath, fileName);

        // Check if file actually exists in filesystem.
        if (!file.exists()) {
            // Do your thing if the file appears to be non-existing.
            // Throw an exception, or show default/warning page, or just ignore it.
            response.sendRedirect("/FileNotFoundError.jsp");
            return;
        }

        // Get content type by filename.
        String contentType = URLConnection.guessContentTypeFromName(fileName);

        // If content type is unknown, then set the default value.
        // For all content types, see: http://www.w3schools.com/media/media_mimeref.asp
        if (contentType == null) {
            contentType = "application/octet-stream";
        }

        // Prepare streams.
        BufferedInputStream input = null;
        BufferedOutputStream output = null;

        try {
            // Open file file.
            input = new BufferedInputStream(new FileInputStream(file));
            int contentLength = input.available();

            // Init servlet response.
            response.reset();
            response.setContentLength(contentLength);
            response.setContentType(contentType);
            response.setHeader(
                "Content-disposition", "attachment; filename=\"" + fileName + "\"");
            output = new BufferedOutputStream(response.getOutputStream());

            // Write file contents to response.
            while (contentLength-- > 0) {
                output.write(input.read());
            }

            // Finalize task.
            output.flush();
        } catch (IOException e) {
            // Something went wrong?
            e.printStackTrace();
        } finally {
            // Gently close streams.
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    // Do your thing with the exception. Print it, log it or mail it.
                    e.printStackTrace();
                }
            }
            if (output != null) {
                try {
                    output.close();
                } catch (IOException e) {
                    // Do your thing with the exception. Print it, log it or mail it.
                    e.printStackTrace();
                }
            }
        }
    }

}

In order to get the FileServlet to work, add the following entries to the Web Deployment Descriptor web.xml:

<servlet>
    <servlet-name>fileServlet</servlet-name>
    <servlet-class>mypackage.FileServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>fileServlet</servlet-name>
    <url-pattern>/file/*</url-pattern>
</servlet-mapping>

Of course you can change the url-pattern of the servlet-mapping as you like it.

Here are some basic use examples:

<!-- XHTML or JSP -->
<a href="file/foo.exe">download foo.exe</a>
<a href="file/bar.zip">download bar.zip</a>

<!-- JSF -->
<h:outputLink value="file/foo.exe">download foo.exe</h:outputLink>
<h:outputLink value="file/bar.zip">download bar.zip</h:outputLink>
<h:outputLink value="file/#{myBean.fileName}">
    <h:outputText value="download #{myBean.fileName}" />
</h:outputLink>

Important note: this servlet example does not take the requested file as request parameter, but just as part of the absolute URL, because a certain widely used inferior browser would take the last part of the servlet URL path as filename during the Save As instead of the in the headers supplied filename. Using the filename as part of the absolute URL (and thus not as request parameter) will fix this utterly stupid behaviour.

Back to top

FileServlet serving from database

First prepare a DTO (Data Transfer Object) for File which can be used to hold information about the file (this is not the same as java.io.File! you may choose another name if this is too confusing). You can map this DTO to the database.

package mydata;

public class File {

    // Init ---------------------------------------------------------------------------------------

    private String id;
    private String fileName;
    private String contentType;
    private byte[] content;

    // Implement default getters and setters here.

}

Here is a basic example of a FileServlet which serves a file from a database.

package mypackage;

import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import mydao.*;
import mydata.File;

/**
 * The File servlet for serving from database.
 * @author BalusC
 * @link http://balusc.blogspot.com/2007/07/fileservlet.html
 */
public class FileServlet extends HttpServlet {

    private static FileDao fileDao = new FileDao();

    protected void doGet(HttpServletRequest request, HttpServletResponse response) {

        // Get ID from request.
        String fileId = request.getParameter("id");

        // Check if ID is supplied to the request.
        if (fileId == null) {
            // Do your thing if the ID is not supplied to the request.
            // Throw an exception, or show default/warning page, or just ignore it.
            response.sendRedirect("/FileNotFoundError.jsp");
            return;
        }

        // Lookup File by FileId in database.
        // Do your "SELECT * FROM File WHERE FileID" thing.
        File file = fileDao.load(fileId);

        // Check if file is actually retrieved from database.
        if (file == null) {
            // Do your thing if the file does not exist in database.
            // Throw an exception, or show default/warning file, or just ignore it.
            response.sendRedirect("/FileNotFoundError.jsp");
            return;
        }

        // Prepare stream.
        BufferedOutputStream output = null;

        try {
            // Get file content.
            ByteArrayInputStream input = new ByteArrayInputStream(file.getContent());
            int contentLength = input.available();

            // Init servlet response.
            response.reset();
            response.setContentLength(contentLength);
            response.setContentType(file.getContentType());
            response.setHeader("Content-disposition",
                "attachment; filename=\"" + file.getFileName() + "\"");
            output = new BufferedOutputStream(response.getOutputStream());

            // Write file contents to response.
            while (contentLength-- > 0) {
                output.write(input.read());
            }

            // Finalize task.
            output.flush();
        } catch (IOException e) {
            // Something went wrong?
            e.printStackTrace();
        } finally {
            // Gently close stream.
            if (output != null) {
                try {
                    output.close();
                } catch (IOException e) {
                    // Do your thing with the exception. Print it, log it or mail it.
                    e.printStackTrace();
                }
            }
        }
    }

}

In order to get the FileServlet to work, add the following entries to the Web Deployment Descriptor web.xml:

<servlet>
    <servlet-name>fileServlet</servlet-name>
    <servlet-class>mypackage.FileServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>fileServlet</servlet-name>
    <url-pattern>/file/*</url-pattern>
</servlet-mapping>

Of course you can change the url-pattern of the servlet-mapping as you like it.

Here are some basic use examples:

<!-- XHTML or JSP -->
<a href="file?id=250d7f5086d02a46f9aeec9c51d43c63">download file1</a>
<a href="file?id=0412c29576c708cf0155e8de242169b1">download file2</a>

<!-- JSF -->
<h:outputLink value="file?id=250d7f5086d02a46f9aeec9c51d43c63">download file1</h:outputLink>
<h:outputLink value="file?id=0412c29576c708cf0155e8de242169b1">download file2</h:outputLink>
<h:outputLink value="file?id=#{myBean.fileId}">download file1</h:outputLink>
Back to top

Security considerations

In the last example of an FileServlet serving from database, the ID is encrypted by MD5. It's your choice how you want to implement the use of ID, but keep in mind that plain numeric ID's like 1, 2, 3 and so on makes the hacker easy to guess for another files in the database, which they probably may not view at all. Then rather use a MD5 hash based on a combination of the numeric ID, the filename and the filesize for example. And last but not least, use PreparedStatement instead of a basic Statement to request the file by ID from database, otherwise you will risk an SQL injection when a hacker calls for example "file?id=';TRUNCATE TABLE File--".

Back to top

Copyright - There is no copyright on the code. You can copy, change and distribute it freely. Just mentioning this site should be fair.

(C) July 2007, BalusC