14
14
-- Automatic gzip compression of responses.
16
16
---------------------------------------------------------
17
module Network.Wai.Middleware.Gzip (gzip) where
17
module Network.Wai.Middleware.Gzip
20
import Network.Wai.Zlib
21
27
import Data.Maybe (fromMaybe)
22
import Data.Enumerator (($$), joinI)
23
28
import qualified Data.ByteString.Char8 as S8
24
29
import qualified Data.ByteString as S
31
import Network.HTTP.Types (Status, Header)
32
import Control.Monad.IO.Class (liftIO)
33
import System.Directory (doesFileExist, createDirectoryIfMissing)
34
import qualified Data.Conduit as C
35
import qualified Data.Conduit.Zlib as CZ
36
import qualified Data.Conduit.Binary as CB
37
import qualified Data.Conduit.List as CL
38
import Data.Conduit.Blaze (builderToByteStringFlush)
39
import Blaze.ByteString.Builder (fromByteString)
40
import Control.Exception (try, SomeException)
42
data GzipSettings = GzipSettings
43
{ gzipFiles :: GzipFiles
44
, gzipCheckMime :: S.ByteString -> Bool
47
data GzipFiles = GzipIgnore | GzipCompress | GzipCacheFolder FilePath
48
deriving (Show, Eq, Read)
50
instance Default GzipSettings where
51
def = GzipSettings GzipIgnore defaultCheckMime
53
defaultCheckMime :: S.ByteString -> Bool
54
defaultCheckMime = S8.isPrefixOf "text/"
26
56
-- | Use gzip to compress the body of the response.
31
61
-- Possible future enhancements:
33
63
-- * Only compress if the response is above a certain size.
35
gzip :: Bool -- ^ should we gzip files?
37
gzip files app env = do
64
gzip :: GzipSettings -> Middleware
41
ResponseFile{} | not files -> res
42
_ -> if "gzip" `elem` enc && not isMSIE6
43
then ResponseEnumerator $ compressE $ responseEnumerator res
68
ResponseFile{} | gzipFiles set == GzipIgnore -> return res
69
_ -> if "gzip" `elem` enc && not isMSIE6
71
case (res, gzipFiles set) of
72
(ResponseFile s hs file Nothing, GzipCacheFolder cache) ->
73
case lookup "content-type" hs of
75
| gzipCheckMime set m -> liftIO $ compressFile s hs file cache
77
_ -> return $ compressE set res
46
80
enc = fromMaybe [] $ (splitCommas . S8.unpack)
47
81
`fmap` lookup "Accept-Encoding" (requestHeaders env)
48
82
ua = fromMaybe "" $ lookup "user-agent" $ requestHeaders env
49
83
isMSIE6 = "MSIE 6" `S.isInfixOf` ua
51
compressE :: (forall a. ResponseEnumerator a)
52
-> (forall a. ResponseEnumerator a)
58
joinI $ compress $$ f s hs'
60
-- Remove Content-Length header, since we will certainly have a
61
-- different length after gzip compression.
62
hs' = ("Content-Encoding", "gzip") : filter notLength hs
63
notLength (x, _) = x /= "content-length"
85
compressFile :: Status -> [Header] -> FilePath -> FilePath -> IO Response
86
compressFile s hs file cache = do
87
e <- doesFileExist tmpfile
91
createDirectoryIfMissing True cache
93
try $ C.runResourceT $ CB.sourceFile file
94
C.$$ CZ.gzip C.=$ CB.sinkFile tmpfile
95
either onErr (const onSucc) x
97
onSucc = return $ ResponseFile s (fixHeaders hs) tmpfile Nothing
99
onErr :: SomeException -> IO Response
100
onErr = const $ return $ ResponseFile s hs file Nothing -- FIXME log the error message
102
tmpfile = cache ++ '/' : map safe file
104
| 'A' <= c && c <= 'Z' = c
105
| 'a' <= c && c <= 'z' = c
106
| '0' <= c && c <= '9' = c
111
compressE :: GzipSettings
115
case lookup "content-type" hs of
116
Just m | gzipCheckMime set m ->
117
let hs' = fixHeaders hs
118
in ResponseSource s hs' $ b C.$= builderToByteStringFlush
119
C.$= CZ.compressFlush 1 (CZ.WindowBits 31)
120
C.$= CL.map (fmap fromByteString)
123
(s, hs, b) = responseSource res
125
-- Remove Content-Length header, since we will certainly have a
126
-- different length after gzip compression.
127
fixHeaders :: [Header] -> [Header]
129
(("Content-Encoding", "gzip") :) . filter notLength
131
notLength (x, _) = x /= "content-length"
65
133
splitCommas :: String -> [String]
66
134
splitCommas [] = []