snap-stream-0.1: src/Snap/Util/FileServe/Stream.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | Contains web handlers to stream files
module Snap.Util.FileServe.Stream where
------------------------------------------------------------------------------
import Control.Applicative
import Control.Monad
import Control.Monad.IO.Class
import Data.Attoparsec.ByteString.Char8 hiding (char8)
import Data.ByteString.Builder
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as S
import qualified Data.ByteString.Lazy.Char8 as L
import Data.Maybe (fromMaybe, isNothing)
import Data.Word (Word64)
import Prelude
import Snap.Core
import Snap.Internal.Parsing (fullyParse, parseNum)
import System.IO.Streams (OutputStream)
------------------------------------------------------------------------------
-- | Same as 'serveFile', with control over the MIME mapping used and streamed
serveStreamAs :: MonadSnap m
=> ByteString -- ^ MIME type
-> Word64
-> (Word64 -> Word64 -> OutputStream Builder -> IO ())
-> (OutputStream Builder -> IO ())
-> m ()
serveStreamAs mime sz stream streamAll = do
reqOrig <- getRequest
-- If-Range header must be ignored if there is no Range: header in the
-- request (RFC 2616 section 14.27)
let req = if isNothing $ getHeader "range" reqOrig
then deleteHeader "if-range" reqOrig
else reqOrig
-- ok, at this point we know the last-modified time and the
-- content-type. set those.
modifyResponse $ setHeader "Accept-Ranges" "bytes"
. setContentType mime
-- checkRangeReq checks for a Range: header in the request and sends a
-- partial response if it matches.
wasRange <- liftSnap $ checkRangeReq req stream sz
-- if we didn't have a range request, we just do normal sendfile
unless wasRange $ do
modifyResponse $ setResponseCode 200
. setContentLength sz
addToOutput $ \str -> liftIO (streamAll str) >> return str
------------------------------------------------------------------------------
data RangeReq = RangeReq !Word64 !(Maybe Word64)
| SuffixRangeReq !Word64
------------------------------------------------------------------------------
rangeParser :: Parser RangeReq
rangeParser = string "bytes=" *>
(byteRangeSpec <|> suffixByteRangeSpec) <*
endOfInput
where
byteRangeSpec = do
start <- fromIntegral <$> parseNum
void $! char '-'
end <- option Nothing $ liftM Just parseNum
return $! RangeReq start (fromIntegral <$> end)
suffixByteRangeSpec =
liftM (SuffixRangeReq . fromIntegral) $ char '-' *> parseNum
------------------------------------------------------------------------------
checkRangeReq :: (MonadSnap m)
=> Request
-> (Word64 -> Word64 -> OutputStream Builder -> IO ())
-> Word64
-> m Bool
checkRangeReq req stream sz = do
-- TODO/FIXME: multiple ranges
maybe (return False)
(\s -> either (const $ return False)
withRange
(fullyParse s rangeParser))
(getHeader "range" req)
where
withRange (RangeReq start mend) = do
let end = fromMaybe (sz-1) mend
if start < 0 || end < start || start >= sz || end >= sz
then send416
else send206 start end
withRange (SuffixRangeReq nbytes) = do
let end = sz-1
let start = sz - nbytes
if start < 0 || end < start || start >= sz || end >= sz
then send416
else send206 start end
-- note: start and end INCLUSIVE here
send206 start end = do
let !len = end-start+1
let crng = S.concat . L.toChunks $
toLazyByteString $
mconcat [ byteString "bytes "
, fromShow start
, char8 '-'
, fromShow end
, char8 '/'
, fromShow sz ]
modifyResponse $ setResponseCode 206
. setHeader "Content-Range" crng
. setContentLength len
-- end here was inclusive, sendFilePartial is exclusive
addToOutput $ \str -> liftIO (stream start (end+1) str) >> return str
return True
send416 = do
-- if there's an "If-Range" header in the request, then we just send
-- back 200
if getHeader "If-Range" req /= Nothing
then return False
else do
let crng = S.concat . L.toChunks $
toLazyByteString $
mconcat [ byteString "bytes */"
, fromShow sz ]
modifyResponse $ setResponseCode 416
. setHeader "Content-Range" crng
. setContentLength 0
. deleteHeader "Content-Type"
. deleteHeader "Content-Encoding"
. deleteHeader "Transfer-Encoding"
. setResponseBody (return . id)
return True
------------------------------------------------------------------------------
fromShow :: Show a => a -> Builder
fromShow = stringUtf8 . show