This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "snap-core".
The branch, master has been updated
via ef6c351c14594517dea3a5159d75af38c47700f5 (commit)
from 6dc149cd34142785d1460a757b219008a9d16fe6 (commit)
Summary of changes:
snap-core.cabal | 3 +
src/Snap/Internal/Http/Types.hs | 11 ++-
src/Snap/Internal/Parsing.hs | 26 +++++
src/Snap/Internal/Types.hs | 19 ++++-
src/Snap/Iteratee.hs | 92 ++++++++++++++++--
src/Snap/Types.hs | 1 +
src/Snap/Util/FileServe.hs | 157 +++++++++++++++++++++++++++++--
src/Snap/Util/GZip.hs | 14 +---
test/runTestsAndCoverage.sh | 5 +-
test/snap-core-testsuite.cabal | 1 +
test/suite/Snap/Util/FileServe/Tests.hs | 35 ++++++-
11 files changed, 322 insertions(+), 42 deletions(-)
create mode 100644 src/Snap/Internal/Parsing.hs
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit ef6c351c14594517dea3a5159d75af38c47700f5
Author: Gregory Collins <[email protected]>
Date: Fri Sep 24 21:25:20 2010 -0400
Add 'Range:' support to fileServe
diff --git a/snap-core.cabal b/snap-core.cabal
index cea34b3..1f264d3 100644
--- a/snap-core.cabal
+++ b/snap-core.cabal
@@ -140,6 +140,7 @@ Library
Snap.Util.GZip
other-modules:
+ Snap.Internal.Parsing,
Snap.Internal.Routing,
Snap.Internal.Types
@@ -148,6 +149,7 @@ Library
base >= 4 && < 5,
bytestring,
bytestring-nums,
+ bytestring-show >= 0.3.2 && < 0.4,
cereal >= 0.3 && < 0.4,
containers,
deepseq >= 1.1 && <1.2,
@@ -189,6 +191,7 @@ Executable snap
base >= 4 && < 5,
bytestring,
bytestring-nums,
+ bytestring-show >= 0.3.2 && < 0.4,
cereal >= 0.3 && < 0.4,
containers,
deepseq >= 1.1 && <1.2,
diff --git a/src/Snap/Internal/Http/Types.hs b/src/Snap/Internal/Http/Types.hs
index 66016d0..d10f638 100644
--- a/src/Snap/Internal/Http/Types.hs
+++ b/src/Snap/Internal/Http/Types.hs
@@ -351,8 +351,12 @@ instance HasHeaders Headers where
-- response type
------------------------------------------------------------------------------
-data ResponseBody = Enum (forall a . Enumerator a) -- ^ output body is
enumerator
- | SendFile FilePath -- ^ output body is
sendfile()
+data ResponseBody = Enum (forall a . Enumerator a)
+ -- ^ output body is enumerator
+
+ | SendFile FilePath (Maybe (Int64,Int64))
+ -- ^ output body is sendfile(), optional second argument
+ -- is a byte range to send
------------------------------------------------------------------------------
@@ -365,7 +369,8 @@ rspBodyMap f b = Enum $ f $ rspBodyToEnum b
------------------------------------------------------------------------------
rspBodyToEnum :: ResponseBody -> Enumerator a
rspBodyToEnum (Enum e) = e
-rspBodyToEnum (SendFile fp) = I.enumFile fp
+rspBodyToEnum (SendFile fp Nothing) = I.enumFile fp
+rspBodyToEnum (SendFile fp (Just s)) = I.enumFilePartial fp s
------------------------------------------------------------------------------
diff --git a/src/Snap/Internal/Parsing.hs b/src/Snap/Internal/Parsing.hs
new file mode 100644
index 0000000..7e1bca8
--- /dev/null
+++ b/src/Snap/Internal/Parsing.hs
@@ -0,0 +1,26 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Snap.Internal.Parsing where
+
+import Control.Monad
+import Data.Attoparsec.Char8 hiding (Done)
+import qualified Data.Attoparsec.Char8 as Atto
+import Data.ByteString.Char8 (ByteString)
+import Data.ByteString.Nums.Careless.Int (int)
+import Data.Int
+
+------------------------------------------------------------------------------
+fullyParse :: ByteString -> Parser a -> Either String a
+fullyParse s p =
+ case r' of
+ (Fail _ _ e) -> Left e
+ (Partial _) -> Left "parse failed"
+ (Atto.Done _ x) -> Right x
+ where
+ r = parse p s
+ r' = feed r ""
+
+
+------------------------------------------------------------------------------
+parseNum :: Parser Int64
+parseNum = liftM int $ Atto.takeWhile1 Atto.isDigit
diff --git a/src/Snap/Internal/Types.hs b/src/Snap/Internal/Types.hs
index 37429c0..89dc694 100644
--- a/src/Snap/Internal/Types.hs
+++ b/src/Snap/Internal/Types.hs
@@ -14,6 +14,7 @@ import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as S
import qualified Data.ByteString.Lazy.Char8 as L
import qualified Data.CIByteString as CIB
+import Data.Int
import Data.IORef
import qualified Data.Iteratee as Iter
import Data.Maybe
@@ -480,7 +481,23 @@ writeLazyText s = writeLBS $ LT.encodeUtf8 s
-- If the response body is modified (using 'modifyResponseBody'), the file will
-- be read using @mmap()@.
sendFile :: FilePath -> Snap ()
-sendFile f = modifyResponse $ \r -> r { rspBody = SendFile f }
+sendFile f = modifyResponse $ \r -> r { rspBody = SendFile f Nothing }
+
+
+------------------------------------------------------------------------------
+-- | Sets the output to be the contents of the specified file, within the given
+-- (start,end) range.
+--
+-- Calling 'sendFilePartial' will overwrite any output queued to be sent in the
+-- 'Response'. If the response body is not modified after the call to
+-- 'sendFilePartial', Snap will use the efficient @sendfile()@ system call on
+-- platforms that support it.
+--
+-- If the response body is modified (using 'modifyResponseBody'), the file will
+-- be read using @mmap()@.
+sendFilePartial :: FilePath -> (Int64,Int64) -> Snap ()
+sendFilePartial f rng = modifyResponse $ \r ->
+ r { rspBody = SendFile f (Just rng) }
------------------------------------------------------------------------------
diff --git a/src/Snap/Iteratee.hs b/src/Snap/Iteratee.hs
index 2caf633..2b87aab 100644
--- a/src/Snap/Iteratee.hs
+++ b/src/Snap/Iteratee.hs
@@ -1,7 +1,9 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
+{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PackageImports #-}
+{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeSynonymInstances #-}
-- | Snap Framework type aliases and utilities for iteratees. Note that as a
@@ -28,6 +30,8 @@ module Snap.Iteratee
, enumBS
, enumLBS
, enumFile
+ , enumFilePartial
+ , InvalidRangeException
-- ** Conversion to/from 'WrappedByteString'
, fromWrap
@@ -47,6 +51,7 @@ module Snap.Iteratee
------------------------------------------------------------------------------
import Control.Monad
import Control.Monad.CatchIO
+import Control.Exception (Exception, SomeException)
import Data.ByteString (ByteString)
import qualified Data.ByteString as S
import qualified Data.ByteString.Unsafe as S
@@ -60,6 +65,7 @@ import qualified Data.Iteratee.Base.StreamChunk as SC
import Data.Iteratee.WrappedByteString
import qualified Data.ListLike as LL
import Data.Monoid (mappend)
+import Data.Typeable
import Foreign
import Foreign.C.Types
import GHC.ForeignPtr
@@ -68,7 +74,6 @@ import System.IO
import "monads-fd" Control.Monad.Trans (liftIO)
#ifndef PORTABLE
-import Control.Exception (SomeException)
import System.IO.Posix.MMap
import System.PosixCompat.Files
#endif
@@ -411,18 +416,53 @@ takeNoMoreThan n' iter =
------------------------------------------------------------------------------
+{-# INLINE _enumFile #-}
_enumFile :: FilePath -> Iteratee IO a -> IO (Iteratee IO a)
_enumFile fp iter = do
h <- liftIO $ openBinaryFile fp ReadMode
- i' <- enumHandle h iter
- return (i' `finally` liftIO (hClose h))
+ enumHandle h iter `finally` hClose h
+
+
+------------------------------------------------------------------------------
+data InvalidRangeException = InvalidRangeException
+ deriving (Typeable)
+
+instance Show InvalidRangeException where
+ show InvalidRangeException = "Invalid range"
+
+instance Exception InvalidRangeException
+
+
+------------------------------------------------------------------------------
+{-# INLINE _enumFilePartial #-}
+_enumFilePartial :: FilePath
+ -> (Int64,Int64)
+ -> Iteratee IO a
+ -> IO (Iteratee IO a)
+_enumFilePartial fp (start,end) iter = do
+ let len = end - start
+
+ h <- liftIO $ openBinaryFile fp ReadMode
+ unless (start == 0) $
+ hSeek h AbsoluteSeek $ toInteger start
+
+ let i' = joinI $ takeExactly len iter
+
+ enumHandle h i' `finally` hClose h
enumFile :: FilePath -> Iteratee IO a -> IO (Iteratee IO a)
+enumFilePartial :: FilePath
+ -> (Int64,Int64)
+ -> Iteratee IO a
+ -> IO (Iteratee IO a)
#ifdef PORTABLE
enumFile = _enumFile
+enumFilePartial fp rng@(start,end) iter = do
+ when (end < start) $ throw InvalidRangeException
+ _enumFilePartial fp rng iter
#else
@@ -430,19 +470,53 @@ enumFile = _enumFile
maxMMapFileSize :: FileOffset
maxMMapFileSize = 41943040
+tooBigForMMap :: FilePath -> IO Bool
+tooBigForMMap fp = do
+ stat <- getFileStatus fp
+ return $ fileSize stat > maxMMapFileSize
+
+
enumFile fp iter = do
-- for small files we'll use mmap to save ourselves a copy, otherwise we'll
-- stream it
- stat <- getFileStatus fp
- if fileSize stat > maxMMapFileSize
+ tooBig <- tooBigForMMap fp
+
+ if tooBig
then _enumFile fp iter
else do
- es <- (try $
- liftM WrapBS $
- unsafeMMapFile fp) :: IO (Either SomeException
(WrappedByteString Word8))
+ es <- try $
+ liftM WrapBS $
+ unsafeMMapFile fp
case es of
- (Left e) -> return $ throwErr $ Err $ "IO error" ++ show e
+ (Left (e :: SomeException)) -> return $ throwErr
+ $ Err
+ $ "IO error" ++ show e
+
(Right s) -> liftM liftI $ runIter iter $ Chunk s
+
+enumFilePartial fp rng@(start,end) iter = do
+ when (end < start) $ throw InvalidRangeException
+
+ let len = end - start
+
+ tooBig <- tooBigForMMap fp
+
+ if tooBig
+ then _enumFilePartial fp rng iter
+ else do
+ es <- try $ unsafeMMapFile fp
+
+ case es of
+ (Left (e::SomeException)) -> return $ throwErr
+ $ Err
+ $ "IO error" ++ show e
+
+ (Right s) -> liftM liftI $ runIter iter
+ $ Chunk
+ $ WrapBS
+ $ S.take (fromEnum len)
+ $ S.drop (fromEnum start) s
+
#endif
diff --git a/src/Snap/Types.hs b/src/Snap/Types.hs
index bf18d4b..512326a 100644
--- a/src/Snap/Types.hs
+++ b/src/Snap/Types.hs
@@ -106,6 +106,7 @@ module Snap.Types
, writeText
, writeLBS
, sendFile
+ , sendFilePartial
-- * Iteratee
, Enumerator
diff --git a/src/Snap/Util/FileServe.hs b/src/Snap/Util/FileServe.hs
index 3a01289..c9b60cf 100644
--- a/src/Snap/Util/FileServe.hs
+++ b/src/Snap/Util/FileServe.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
@@ -15,23 +16,32 @@ module Snap.Util.FileServe
) where
------------------------------------------------------------------------------
+import Control.Applicative
import Control.Monad
import Control.Monad.Trans
+import Data.Attoparsec.Char8 hiding (Done)
import qualified Data.ByteString.Char8 as S
+import qualified Data.ByteString.Lazy.Char8 as L
import Data.ByteString.Char8 (ByteString)
+import Data.Int
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
+import Prelude hiding (show, Show)
+import qualified Prelude
import System.Directory
import System.FilePath
import System.PosixCompat.Files
-
+import Text.Show.ByteString hiding (runPut)
------------------------------------------------------------------------------
+import Snap.Internal.Debug
+import Snap.Internal.Parsing
+import Snap.Iteratee hiding (drop)
import Snap.Types
------------------------------------------------------------------------------
--- | A type alias for MIME type
+-- | A type alias for MIME type
type MimeMap = Map FilePath ByteString
@@ -218,32 +228,55 @@ fileServeSingle' :: ByteString -- ^ MIME type
mapping
-> Snap ()
fileServeSingle' mime fp = do
req <- getRequest
-
+
+ -- check "If-Modified-Since" and "If-Range" headers
let mbH = getHeader "if-modified-since" req
mbIfModified <- liftIO $ case mbH of
Nothing -> return Nothing
(Just s) -> liftM Just $ parseHttpTime s
+ mbIfRange <- liftIO $ case getHeader "if-range" req of
+ Nothing -> return Nothing
+ (Just s) -> liftM Just $ parseHttpTime s
+
+
-- check modification time and bug out early if the file is not modified.
filestat <- liftIO $ getFileStatus fp
let mt = modificationTime filestat
- maybe (return ()) (chkModificationTime mt) mbIfModified
+ maybe (return ()) (\lt -> when (mt <= lt) notModified) mbIfModified
let sz = fromIntegral $ fileSize filestat
lm <- liftIO $ formatHttpTime mt
+ -- ok, at this point we know the last-modified time and the
+ -- content-type. set those.
modifyResponse $ setHeader "Last-Modified" lm
+ . setHeader "Accept-Ranges" "bytes"
. setContentType mime
- . setContentLength sz
- sendFile fp
- where
- --------------------------------------------------------------------------
- chkModificationTime mt lt = when (mt <= lt) notModified
+ -- now check: is this a range request? If there is an 'If-Range' header
+ -- with an old modification time we skip this check and send a 200 response
+ let skipRangeCheck = maybe (False)
+ (\lt -> mt > lt)
+ mbIfRange
+
+
+ -- checkRangeReq checks for a Range: header in the request and sends a
+ -- partial response if it matches.
+ wasRange <- if skipRangeCheck
+ then return False
+ else checkRangeReq req fp sz
+
+ -- if we didn't have a range request, we just do normal sendfile
+ unless wasRange $ do
+ modifyResponse $ setResponseCode 200
+ sendFile fp
+
+ where
--------------------------------------------------------------------------
notModified = finishWith $
- setResponseStatus 304 "Not Modified" emptyResponse
+ setResponseCode 304 emptyResponse
------------------------------------------------------------------------------
@@ -262,3 +295,107 @@ fileType mm f =
------------------------------------------------------------------------------
defaultMimeType :: ByteString
defaultMimeType = "application/octet-stream"
+
+
+------------------------------------------------------------------------------
+data RangeReq = RangeReq { _rangeFirst :: !Int64
+ , _rangeLast :: !(Maybe Int64)
+ }
+ | SuffixRangeReq { _suffixLength :: !Int64 }
+ deriving (Eq, Prelude.Show)
+
+
+------------------------------------------------------------------------------
+rangeParser :: Parser RangeReq
+rangeParser = string "bytes=" *> (byteRangeSpec <|> suffixByteRangeSpec)
+ where
+ byteRangeSpec = do
+ start <- parseNum
+ end <- option Nothing $ liftM Just (char '-' *> parseNum)
+
+ return $ RangeReq start end
+
+ suffixByteRangeSpec = liftM SuffixRangeReq $ char '-' *> parseNum
+
+
+------------------------------------------------------------------------------
+checkRangeReq :: Request -> FilePath -> Int64 -> Snap Bool
+checkRangeReq req fp sz = do
+ dbg $ "checkRangeReq, fp=" ++ fp ++ ", sz=" ++ Prelude.show sz
+ maybe (return False)
+ (\s -> either (const $ return False)
+ withRange
+ (fullyParse s rangeParser))
+ (getHeader "range" req)
+
+ where
+ withRange rng@(RangeReq start mend) = do
+ dbg $ "withRange: got Range request: " ++ Prelude.show rng
+ let end = fromMaybe (sz-1) mend
+ dbg $ "withRange: start=" ++ Prelude.show start
+ ++ ", end=" ++ Prelude.show end
+
+ if start < 0 || end < start || start >= sz || end >= sz
+ then send416
+ else send206 start end
+
+ withRange rng@(SuffixRangeReq nbytes) = do
+ dbg $ "withRange: got Range request: " ++ Prelude.show rng
+ let end = sz-1
+ let start = sz - nbytes
+
+ dbg $ "withRange: start=" ++ Prelude.show start
+ ++ ", end=" ++ Prelude.show end
+
+ 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
+ dbg "inside send206"
+ let crng = S.concat $
+ L.toChunks $
+ L.concat [ "bytes "
+ , show start
+ , "-"
+ , show end
+ , "/"
+ , show sz ]
+
+ modifyResponse $ setResponseCode 206
+ . setHeader "Content-Range" crng
+
+ dbg $ "send206: sending range (" ++ Prelude.show start
+ ++ "," ++ Prelude.show (end+1) ++ ") to sendFilePartial"
+
+ -- end here was inclusive, sendFilePartial is exclusive
+ sendFilePartial fp (start,end+1)
+ return True
+
+
+ send416 = do
+ dbg "inside send416"
+ -- 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 $
+ L.concat ["bytes */", show sz]
+
+ modifyResponse $ setResponseCode 416
+ . setHeader "Content-Range" crng
+ . setContentLength 0
+ . deleteHeader "Content-Type"
+ . deleteHeader "Content-Encoding"
+ . deleteHeader "Transfer-Encoding"
+ . setResponseBody (enumBS "")
+
+ return True
+
+
+
+dbg :: (MonadIO m) => String -> m ()
+dbg s = debug $ "FileServe:" ++ s
diff --git a/src/Snap/Util/GZip.hs b/src/Snap/Util/GZip.hs
index 63a1ee9..fa65db0 100644
--- a/src/Snap/Util/GZip.hs
+++ b/src/Snap/Util/GZip.hs
@@ -15,7 +15,6 @@ import Control.Exception
import Control.Monad
import Control.Monad.Trans
import Data.Attoparsec.Char8 hiding (Done)
-import qualified Data.Attoparsec.Char8 as Atto
import qualified Data.ByteString.Lazy.Char8 as L
import Data.ByteString.Char8 (ByteString)
import Data.Iteratee.WrappedByteString
@@ -27,6 +26,7 @@ import Prelude hiding (catch, takeWhile)
------------------------------------------------------------------------------
import Snap.Internal.Debug
+import Snap.Internal.Parsing
import Snap.Iteratee hiding (Enumerator)
import Snap.Types
@@ -263,18 +263,6 @@ compressEnumerator compFunc enum iteratee = do
------------------------------------------------------------------------------
-fullyParse :: ByteString -> Parser a -> Either String a
-fullyParse s p =
- case r' of
- (Fail _ _ e) -> Left e
- (Partial _) -> Left "parse failed"
- (Atto.Done _ x) -> Right x
- where
- r = parse p s
- r' = feed r ""
-
-
-------------------------------------------------------------------------------
-- We're not gonna bother with quality values; we'll do gzip or compress in
-- that order.
acceptParser :: Parser [ByteString]
diff --git a/test/runTestsAndCoverage.sh b/test/runTestsAndCoverage.sh
index 260c339..5f2479f 100755
--- a/test/runTestsAndCoverage.sh
+++ b/test/runTestsAndCoverage.sh
@@ -2,7 +2,10 @@
set -e
-export DEBUG=testsuite
+if [ "x$DEBUG" == "x" ]; then
+ export DEBUG=testsuite
+fi
+
SUITE=./dist/build/testsuite/testsuite
export LC_ALL=C
diff --git a/test/snap-core-testsuite.cabal b/test/snap-core-testsuite.cabal
index 180ef35..593cfd7 100644
--- a/test/snap-core-testsuite.cabal
+++ b/test/snap-core-testsuite.cabal
@@ -25,6 +25,7 @@ Executable testsuite
base >= 4 && < 5,
bytestring,
bytestring-nums,
+ bytestring-show >= 0.3.2 && < 0.4,
cereal >= 0.3 && < 0.4,
containers,
deepseq >= 1.1 && <1.2,
diff --git a/test/suite/Snap/Util/FileServe/Tests.hs
b/test/suite/Snap/Util/FileServe/Tests.hs
index bf99a90..6b1a5c9 100644
--- a/test/suite/Snap/Util/FileServe/Tests.hs
+++ b/test/suite/Snap/Util/FileServe/Tests.hs
@@ -7,7 +7,7 @@ module Snap.Util.FileServe.Tests
import Control.Monad
import Data.ByteString (ByteString)
-import qualified Data.ByteString.Char8 as B
+import qualified Data.ByteString.Char8 as S
import qualified Data.ByteString.Lazy.Char8 as L
import Data.IORef
import qualified Data.Map as Map
@@ -24,7 +24,9 @@ import Snap.Iteratee
tests :: [Test]
tests = [ testFs
- , testFsSingle ]
+ , testFsSingle
+ , testRangeOK
+ , testRangeBad ]
expect404 :: IO Response -> IO ()
@@ -49,12 +51,21 @@ goIfModifiedSince m s lm = do
liftM snd (run $ runSnap m (const $ return ()) r)
+goRange :: Snap a -> ByteString -> (Int,Int) -> IO Response
+goRange m s (start,end) = do
+ rq' <- mkRequest s
+ let rq = setHeader "Range"
+ (S.pack $ "bytes=" ++ show start ++ "-" ++ show end)
+ rq'
+ liftM snd (run $ runSnap m (const $ return ()) rq)
+
+
mkRequest :: ByteString -> IO Request
mkRequest uri = do
enum <- newIORef $ SomeEnumerator return
return $ Request "foo" 80 "foo" 999 "foo" 1000 "foo" False Map.empty
enum Nothing GET (1,1) [] "" uri "/"
- (B.concat ["/",uri]) "" Map.empty
+ (S.concat ["/",uri]) "" Map.empty
fs :: Snap ()
fs = do
@@ -68,7 +79,7 @@ fsSingle = do
testFs :: Test
-testFs = testCase "fileServe" $ do
+testFs = testCase "fileServe/multi" $ do
r1 <- go fs "foo.bin"
b1 <- getBody r1
@@ -123,7 +134,7 @@ testFs = testCase "fileServe" $ do
testFsSingle :: Test
-testFsSingle = testCase "fileServeSingle" $ do
+testFsSingle = testCase "fileServe/Single" $ do
r1 <- go fsSingle "foo.html"
b1 <- getBody r1
@@ -135,6 +146,20 @@ testFsSingle = testCase "fileServeSingle" $ do
assertEqual "foo.html size" (Just 4) (rspContentLength r1)
+testRangeOK :: Test
+testRangeOK = testCase "fileServe/range/ok" $ do
+ r1 <- goRange fsSingle "foo.html" (1,2)
+ b1 <- getBody r1
+
+ assertEqual "foo.html partial" "OO" b1
+ assertEqual "foo.html partial size" (Just 2) (rspContentLength r1)
+
+
+testRangeBad :: Test
+testRangeBad = testCase "fileServe/range/bad" $ do
+ r1 <- goRange fsSingle "foo.html" (1,17)
+ assertEqual "bad range" 416 (rspStatus r1)
+
coverMimeMap :: (Monad m) => m ()
coverMimeMap = Prelude.mapM_ f $ Map.toList defaultMimeTypes
-----------------------------------------------------------------------
hooks/post-receive
--
snap-core
_______________________________________________
Snap mailing list
[email protected]
http://mailman-mail5.webfaction.com/listinfo/snap