Creating a Static Directory Listing with Haskell

I recently moved my static content (including this site) over to Amazon’s S3. I had a bunch of pictures I wanted to send to family so I dumped them into a bucket, only to find there was no way to link them directly to the directory full of pictures. It seems if you want a directory listing on S3, you’ve got to do it yourself. There are some little chunks of javascript that purport to be able to do this for you, but the one I tried didn’t work out of the box, and I thought it be more fun to do it myself.

The idea is pretty simple, get a list of files in a directory (ignoring subdirectories), and generate of a list of links to those files. Given a directory containing images, for example ‘a.png’, ‘b.png’, and ‘c.png’, we want to output:

<a href="a.png">a.png</a><br />
<a href="b.png">b.png</a><br />
<a href="c.png">c.png</a>

I first wrote it in Clojure which was pretty straight forward:

(ns clojure-directory-lister.core
  (:require [clojure.string :as s])) 

(defn files [dir-str]
  (file-seq (clojure.java.io/file dir-str)))

(defn names [files]
  (map #(.getName %) files))

(defn to-html [filenames] 
  (s/join "<br />"
    (map #(format "<a href=\"%s\">%s</a>" % %) filenames)))

(defn -main [dir out] 
  (spit out (-> dir files names to-html)))

This works fine, but I thought why spend precious seconds waiting for the JVM to start, when I could take hours and write it in Haskell and have it run instantaneously.

Writing it in Haskell follows the same logic and was also easy to write once I figured out how deal with the monadic IO. While Clojure has file-seq which returns a lazy-seq of all the files in the directory as Java File objects, Haskell’s getDirectoryContents has the return type of IO [String]. This is a list (in the IO monad) of strings representing both filepaths and directories, and you need to use a seperate function doesFileExist to select only files and not directories. This was tricker than it seems. My first instict was to write this as:

filter doesFileExist $ getDirectoryContents path

However, this doesn’t work for a couple of reasons. To start, filter expects a function of type a -> Bool, and since doesFileExist needs to hit the filesystem, it returns IO Bool. Luckily there’s a version of filter in Control.Monad called filterM, which works with predicates that wrap their result in a monad, which leads us to:

filterM doesFileExist $ getDirectoryContents path

However this still doesn’t work because filterM expects a plain list of type [a], and getDirectoryContents returns a list wrapped in the IO monad. To solve this problem, we can use the bind operator >>= to unwrap the result of getDirectoryContents and apply filterM to it.

getDirectoryContents path >>= filterM doesFileExist

From there the rest of the program is simple:

import System.Directory (getDirectoryContents, doesFileExist)
import Control.Monad (filterM)
import Text.Printf (printf)
import Data.List (intercalate, sort)
import System.Environment (getArgs, getProgName)

getFiles :: FilePath -> IO [FilePath]
getFiles path = getDirectoryContents path >>= filterM doesFileExist

fileToLink :: FilePath -> String
fileToLink path = printf "<a href=\"%s\">%s</a>" path path

main = do
  progName <- getProgName
  args <- getArgs
  if length args /= 2 
    then putStrLn $ printf "Usage: %s <directory> <outfile>" progName
    else do
      let (dirpath:outfile:_) = args
      unsorted_files <- getFiles dirpath
      let files = sort unsorted_files
      let links = map fileToLink files
      writeFile outfile $ intercalate "<br />\n" links

The only other tricky thing I ran into was the fact that using an if expression inside a do block requires special indentation where ‘then’ and ‘else’ need to be indented farther than the ‘if’. Apparently there’s a compiler extension that will allow you to indent ifs in do blocks normally, but it didn’t seem worth enabling an extention for something this simple.

The full cabal project is on github here: https://github.com/cgag/directory-lister