@@ -37,6 +37,14 @@ import Blaze.ByteString.Builder (fromByteString)
37import Control.Exception (try, SomeException)
37import Control.Exception (try, SomeException)
38import qualified Data.Set as Set
38import qualified Data.Set as Set
39import Network.Wai.Internal
39import Network.Wai.Internal
40+import qualified Data.Streaming.Blaze as B
41+import qualified Data.Streaming.Zlib as Z
42+import qualified Blaze.ByteString.Builder as Blaze
43+import Control.Monad (unless)
44+import Data.Function (fix)
45+import Control.Exception (throwIO)
46+import qualified System.IO as IO
47+import Data.ByteString.Lazy.Internal (defaultChunkSize)
41data GzipSettings = GzipSettings
49data GzipSettings = GzipSettings
42 { gzipFiles :: GzipFiles
50 { gzipFiles :: GzipFiles
@@ -69,20 +77,19 @@ defaultCheckMime bs =
70-- * Only compress if the response is above a certain size.
78-- * Only compress if the response is above a certain size.
71gzip :: GzipSettings -> Middleware
79gzip :: GzipSettings -> Middleware
72−gzip set app env = do
80+gzip set app env sendResponse = app env $ \res ->
74 case res of
81 case res of
75− ResponseFile{} | gzipFiles set == GzipIgnore -> return res
82+ ResponseFile{} | gzipFiles set == GzipIgnore -> sendResponse res
76 _ -> if "gzip" `elem` enc && not isMSIE6 && not (isEncoded res)
83 _ -> if "gzip" `elem` enc && not isMSIE6 && not (isEncoded res)
78 case (res, gzipFiles set) of
85 case (res, gzipFiles set) of
79 (ResponseFile s hs file Nothing, GzipCacheFolder cache) ->
86 (ResponseFile s hs file Nothing, GzipCacheFolder cache) ->
80 case lookup "content-type" hs of
87 case lookup "content-type" hs of
82− | gzipCheckMime set m -> liftIO $ compressFile s hs file cache
89+ | gzipCheckMime set m -> compressFile s hs file cache sendResponse
83− _ -> return res
90+ _ -> sendResponse res
84− _ -> return $ compressE set res
91+ _ -> compressE set res sendResponse
85− else return res
92+ else sendResponse res
87 enc = fromMaybe [] $ (splitCommas . S8.unpack)
94 enc = fromMaybe [] $ (splitCommas . S8.unpack)
88 `fmap` lookup "Accept-Encoding" (requestHeaders env)
95 `fmap` lookup "Accept-Encoding" (requestHeaders env)
@@ -90,22 +97,38 @@ gzip set app env = do
90 isMSIE6 = "MSIE 6" `S.isInfixOf` ua
97 isMSIE6 = "MSIE 6" `S.isInfixOf` ua
91 isEncoded res = isJust $ lookup "Content-Encoding" $ responseHeaders res
98 isEncoded res = isJust $ lookup "Content-Encoding" $ responseHeaders res
93−compressFile :: Status -> [Header] -> FilePath -> FilePath -> IO Response
100+compressFile :: Status -> [Header] -> FilePath -> FilePath -> (Response -> IO a) -> IO a
94−compressFile s hs file cache = do
101+compressFile s hs file cache sendResponse = do
95 e <- doesFileExist tmpfile
102 e <- doesFileExist tmpfile
97 then onSucc
104 then onSucc
99 createDirectoryIfMissing True cache
106 createDirectoryIfMissing True cache
101− try $ runResourceT $ CB.sourceFile file
108+ IO.withBinaryFile file IO.ReadMode $ \inH ->
102− C.$$ CZ.gzip C.=$ CB.sinkFile tmpfile
109+ IO.withBinaryFile tmpfile IO.WriteMode $ \outH -> do
110+ deflate <- Z.initDeflate 7 $ Z.WindowBits 31
111+ -- FIXME this code should write to a temporary file, then
112+ -- rename to the final file
113+ let goPopper popper = fix $ \loop -> do
116+ Z.PRDone -> return ()
120+ Z.PRError e -> throwIO e
122+ bs <- S.hGetSome inH defaultChunkSize
123+ unless (S.null bs) $ do
124+ Z.feedDeflate deflate bs >>= goPopper
126+ goPopper $ Z.finishDeflate deflate
103− either onErr (const onSucc) x
127+ either onErr (const onSucc) (x :: Either SomeException ()) -- FIXME bad! don't catch all exceptions like that!
105− onSucc = return $ ResponseFile s (fixHeaders hs) tmpfile Nothing
129+ onSucc = sendResponse $ responseFile s (fixHeaders hs) tmpfile Nothing
107− onErr :: SomeException -> IO Response
108− onErr = const $ return $ ResponseFile s hs file Nothing -- FIXME log the error message
131+ onErr _ = sendResponse $ responseFile s hs file Nothing -- FIXME log the error message
110 tmpfile = cache ++ '/' : map safe file
133 tmpfile = cache ++ '/' : map safe file
@@ -118,18 +141,42 @@ compressFile s hs file cache = do
119compressE :: GzipSettings
142compressE :: GzipSettings
120 -> Response
143 -> Response
121− -> Response
144+ -> (Response -> IO a)
122−compressE set res =
146+compressE set res sendResponse =
123 case lookup "content-type" hs of
147 case lookup "content-type" hs of
124 Just m | gzipCheckMime set m ->
148 Just m | gzipCheckMime set m ->
125 let hs' = fixHeaders hs
149 let hs' = fixHeaders hs
126− in ResponseSource s hs' $ \f -> wb $ \b -> f $
150+ in wb $ \body -> sendResponse $ responseStream s hs' $ \sendChunk flush -> do
127− b C.$= builderToByteStringFlush
151+ (blazeRecv, blazeFinish) <- B.newBlazeRecv B.defaultStrategy
128− C.$= CZ.compressFlush 1 (CZ.WindowBits 31)
152+ deflate <- Z.initDeflate 1 (Z.WindowBits 31)
153+ let sendBuilder builder = do
154+ popper <- blazeRecv builder
157+ unless (S.null bs) $ do
160+ sendBS bs = Z.feedDeflate deflate bs >>= deflatePopper
162+ sendBuilder Blaze.flush
163+ deflatePopper $ Z.flushDeflate deflate
165+ deflatePopper popper = fix $ \loop -> do
168+ Z.PRDone -> return ()
129− C.$= CL.map (fmap fromByteString)
170+ sendChunk $ fromByteString bs'
172+ Z.PRError e -> throwIO e
174+ body sendBuilder flushBuilder
175+ sendBuilder Blaze.flush
176+ deflatePopper $ Z.finishDeflate deflate
130− _ -> res
177+ _ -> sendResponse res
132− (s, hs, wb) = responseToSource res
179+ (s, hs, wb) = responseToStream res
134-- Remove Content-Length header, since we will certainly have a
181-- Remove Content-Length header, since we will certainly have a
135-- different length after gzip compression.
182-- different length after gzip compression.