Lately I’ve submitted a patch to Hsenv featuring the ability to download a file over HTTP without external tools (e.g. curl), but using nothing more than awesome Haskell. I’ve posted the cool snippet on Reddit, and someone asked for a version with a progress bar, similar to what have been implemented in Python here:
Download a file with Python - SO
I came up with a nice version which does rudimental error handling over HTTP response codes, which implements a progress bar and that has a constant memory usage, because we still retain all the benefits of programming with streaming libraries, namely io-streams and http-streams. Without further ado, let me show you the code:
{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-}
module Main where
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as C8
import System.IO.Streams (InputStream, OutputStream)
import qualified System.IO.Streams as S
import Network.Http.Client
import Network.Socket
import Text.Printf (printf)
withProgressBar :: Integer
-> InputStream ByteString
-> OutputStream ByteString
-> IO ()
= go (0 :: Int)
withProgressBar fileSize inS outS where
= do
go blocksRead <- S.read inS
block case block of
Just d) -> do
(let currentBlocks = blocksRead + B.length d
let percentage = fromIntegral (currentBlocks * 100) /
fromIntegral fileSize
"%10d [%3.2f%%]\r" currentBlocks (percentage :: Double)
printf Just d) outS >> go currentBlocks
S.write (Nothing -> return ()
downloadFile :: URL -> FilePath -> IO ()
= withSocketsDo $ get url $ \response inStream ->
downloadFile url name case getStatusCode response of
200 -> let fileSize = maybe 0 (\fs -> read (C8.unpack fs) :: Integer)
"Content-Length")
(getHeader response in S.withFileAsOutput name (withProgressBar fileSize inStream)
-> error $ "Failed to download " ++
code ++
name ": http response returned " ++
show code
main :: IO ()
= do
main let url = "http://audacity.googlecode.com/files/audacity-macosx-ub-2.0.3.dmg"
print $ "Downloading " ++ url
"audacity.dmg" downloadFile (C8.pack url)
As you can see, it’s less than 40 lines of code! (ok, a bit more, but just because I’ve splitted the type signature and the error message on multiple lines to make it fit the blog template). Even better, as someone suggested on Reddit, we can make this code multi-platform using withSocketsDo
before get
. We need this on Windows machines only (to initialize the underlying socket), but due to the fact withSocketsDo
is implemented as id
on *nix platforms, everything will just work. Once again, Haskell rocks.