Friday, July 20, 2007

FileServlet

Serve your files

Update: there's a newer article out for more effective file serving supporting caching, resume and range requests (which is required by most media/video players). You may find it useful: FileServlet supporting resume and caching and GZIP.

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.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLDecoder;

import javax.servlet.ServletException;
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 {

    // Constants ----------------------------------------------------------------------------------

    private static final int DEFAULT_BUFFER_SIZE = 10240; // 10KB.

    // Properties ---------------------------------------------------------------------------------

    private String filePath;

    // Actions ------------------------------------------------------------------------------------

    public void init() throws ServletException {

        // Define base path somehow. You can define it as init-param of the servlet.
        this.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".
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        // Get requested file by path info.
        String requestedFile = request.getPathInfo();

        // Check if file is actually supplied to the request URI.
        if (requestedFile == null) {
            // Do your thing if the file is not supplied to the request URI.
            // Throw an exception, or send 404, or show default/warning page, or just ignore it.
            response.sendError(HttpServletResponse.SC_NOT_FOUND); // 404.
            return;
        }

        // Decode the file name (might contain spaces and on) and prepare file object.
        File file = new File(filePath, URLDecoder.decode(requestedFile, "UTF-8"));

        // 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 send 404, or show default/warning page, or just ignore it.
            response.sendError(HttpServletResponse.SC_NOT_FOUND); // 404.
            return;
        }

        // Get content type by filename.
        String contentType = getServletContext().getMimeType(file.getName());

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

        // Init servlet response.
        response.reset();
        response.setBufferSize(DEFAULT_BUFFER_SIZE);
        response.setContentType(contentType);
        response.setHeader("Content-Length", String.valueOf(file.length()));
        response.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");

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

        try {
            // Open streams.
            input = new BufferedInputStream(new FileInputStream(file), DEFAULT_BUFFER_SIZE);
            output = new BufferedOutputStream(response.getOutputStream(), DEFAULT_BUFFER_SIZE);

            // Write file contents to response.
            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
            int length;
            while ((length = input.read(buffer)) > 0) {
                output.write(buffer, 0, length);
            }
        } finally {
            // Gently close streams.
            close(output);
            close(input);
        }
    }

    // Helpers (can be refactored to public utility class) ----------------------------------------

    private static void close(Closeable resource) {
        if (resource != null) {
            try {
                resource.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 some browsers such as Internet Explorer would take the last part of the servlet URL path as filename during the 'Save As' dialogue instead of the in the headers supplied filename. Some browsers would also not be able to detect the correct content type and the associated application (yes, it ignores the Content-Type header as well!!). Using the filename as part of the absolute URL (and thus not as request parameter) will fix this stupid browser 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 and use a DAO class to obtain it. You can get the file as InputStream from the database using ResultSet#getBinaryStream().

The data layer and the DAO pattern is explained in this tutorial: DAO tutorial - the data layer.

package mymodel;

import java.io.InputStream;

public class File {

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

    private String id;
    private String name;
    private String contentType;
    private InputStream content;
    private Integer length;

    // 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.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.IOException;

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

import mydao.DAOFactory;
import mydao.FileDAO;
import mymodel.File;

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

    // Constants ----------------------------------------------------------------------------------

    private static final int DEFAULT_BUFFER_SIZE = 10240; // 10KB.

    // Statics ------------------------------------------------------------------------------------

    private static FileDAO fileDAO = DAOFactory.getFileDAO();

    // Actions ------------------------------------------------------------------------------------

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        // 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 send 404, or show default/warning page, or just ignore it.
            response.sendError(HttpServletResponse.SC_NOT_FOUND); // 404.
            return;
        }

        // Lookup File by FileId in database.
        // Do your "SELECT * FROM File WHERE FileID" thing.
        File file = fileDAO.find(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 send 404, or show default/warning page, or just ignore it.
            response.sendError(HttpServletResponse.SC_NOT_FOUND); // 404.
            return;
        }

        // Init servlet response.
        response.reset();
        response.setBufferSize(DEFAULT_BUFFER_SIZE);
        response.setContentType(file.getContentType());
        response.setHeader("Content-Length", String.valueOf(file.getLength()));
        response.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");

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

        try {
            // Open streams.
            input = new BufferedInputStream(file.getContent(), DEFAULT_BUFFER_SIZE);
            output = new BufferedOutputStream(response.getOutputStream(), DEFAULT_BUFFER_SIZE);

            // Write file contents to response.
            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
            int length;
            while ((length = input.read(buffer)) > 0) {
                output.write(buffer, 0, length);
            }
        } finally {
            // Gently close streams.
            close(output);
            close(input);
        }
    }

    // Helpers (can be refactored to public utility class) ----------------------------------------

    private static void close(Closeable resource) {
        if (resource != null) {
            try {
                resource.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