Back to Table of Contents

Serving Files from Disk

Happstack can be used to serve static files from disk, such as .html, .jpg, etc.

The file serving capabilities can be divided into two categories:

  1. Serving files from a directory based on a direct mapping of a portion of the URI to file names on the disk
  2. Serving an specific, individual file on disk, whose name may be different from the URI

Serving Files from a Directory

The most common way to serve files is by using serveDirectory:

> data Browsing  = EnableBrowsing | DisableBrowsing
>
> serveDirectory :: ( WebMonad Response m, ServerMonad m, FilterMonad Response m
>                   , MonadIO m, MonadPlus m ) =>
>                   Browsing    -- ^ enable/disable directory browsing
>                -> [FilePath]  -- ^ index file names, used when path is a directory
>                -> FilePath    -- ^ file/directory to serve
>                -> m Response

For example:

> serveDirectory EnableBrowsing ["index.html"] "path/to/directory/on/disk"

If the requested path does not map to a file or directory, then serveDirectory returns mzero.

If the requested path is a file then the file is served normally using serveFile.

When a directory is requested, serveDirectory will first try to find one of the index files (in the order they are listed). If that fails, it will show a directory listing if EnableBrowsing, otherwise it will return forbidden "Directory index forbidden".

The formula for mapping the URL to a file on disk is just what you would expect:

path/to/directory/on/disk </> unconsumed/portion/of/request/url

So if the handler is:

dir "static" $ serveDirectory EnableBrowsing ["index.html"] "/srv/mysite/datafiles"

And the request URL is:

http://localhost/static/foo/bar.html

Then we are going to look for:

/srv/mysite/datafiles </> foo/bar.html => /srv/mysite/datafiles/foo/bar.html

The following demo will allow you to browse the directory that the server is running in. (So be careful where you run it).

> module Main where
>
> import Happstack.Server (Browsing(EnableBrowsing), nullConf, serveDirectory, simpleHTTP)
>
> main :: IO ()
> main = simpleHTTP nullConf $ serveDirectory EnableBrowsing [] "."

[Source code for the app is here.]

Simply run it and point your browser at http://localhost:8000/.

File Serving Security

The request URL is sanitized so that users can not escape the top-level directory by adding extra .. or / characters to the URL.

The file serving code will follow symlinks. If you do not want that behavior then you will need to roll your own serving function. See the section on Advanced File Serving for more information.

Serving a Single File

Sometimes we want to serve files from disk whose name is not a direct mapping from the URL. For example, let's say that you have an image and you want to allow the client to request the images in different sizes by setting a query parameter. e.g.

http://localhost:8000/images/photo.jpg?size=medium

Clearly, we can not just map the path info portion of the URL to a file disk, because all the different sizes have the same name -- only the query parameter is different. Instead, the application will use some custom algorithm to calculate where the image lives on the disk. It may even need to generate the resized image on-demand. Once the application knows where the file lives on disk it can use serveFile to send that file as a Response using sendFile:

> serveFile :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) =>
>              (FilePath -> m String)   -- ^ function for determining content-type of file.
>                                       --   Usually 'asContentType' or 'guessContentTypeM'
>           -> FilePath                 -- ^ path to the file to serve
>           -> m Response

The first argument is a function which calculates the mime-type for a FilePath. The second argument is path to the file to send. So we might do something like:

> serveFile (guessContentTypeM mimeTypes) "/path/to/photos/photo_medium.jpg"

Note that even though the file is named photo_medium.jpg on the disk, that name is not exposed to the client. They will only see the name they requested, i.e., photo.jpg.

guessContentTypeM will guess the content-type of the file by looking at the filename extension. But, if our photo app only supports JPEG files, there is no need to guess. Furthermore, the name of the file on the disk may not even have the proper extension. It could just be the md5sum of the file or something. So we can also hardcode the correct content-type:

> serveFile (asContentType "image/jpeg") "/path/to/photos/photo_medium.jpg"

The following, example attempts to serve its own source code for any incoming request.

> module Main where
>
> import Happstack.Server (asContentType, nullConf, serveFile, simpleHTTP)
>
> main :: IO ()
> main =
>  simpleHTTP nullConf $ serveFile (asContentType "text/x-haskell") "FileServingSingle.hs"

[Source code for the app is here.]

Advanced File Serving

serveDirectory and serveFile should cover a majority of your file serving needs. But if you want something a little different, it is also possible to roll-your-own solution. The Happstack.Server.FileServe.BuildingBlocks module contains all the pieces used to assemble the high-level serveDirectory and serveFile functions. You can reuse those pieces to build your own custom serving functions. For example, you might want to use a different method for calculating the mime-types, or perhaps you want to create a different look-and-feel for directory browsing, or maybe you want to use something other than sendFile for sending the files. I recommend starting by copying the source for serveDirectory or serveFile and then modifying it to suit your needs.

Next: Reform