28
28
import Data.Maybe (fromMaybe, isJust)
29
29
import qualified Data.ByteString.Char8 as S8
30
30
import qualified Data.ByteString as S
31
import Data.Default.Class
32
32
import Network.HTTP.Types (Status, Header)
33
33
import Control.Monad.IO.Class (liftIO)
34
import Control.Monad.Trans.Resource (runResourceT)
34
35
import System.Directory (doesFileExist, createDirectoryIfMissing)
35
import qualified Data.Conduit as C
36
import qualified Data.Conduit.Zlib as CZ
37
import qualified Data.Conduit.Binary as CB
38
import qualified Data.Conduit.List as CL
39
import Data.Conduit.Blaze (builderToByteStringFlush)
40
36
import Blaze.ByteString.Builder (fromByteString)
41
37
import Control.Exception (try, SomeException)
42
38
import qualified Data.Set as Set
43
39
import 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)
45
49
data GzipSettings = GzipSettings
46
50
{ gzipFiles :: GzipFiles
74
78
-- * Only compress if the response is above a certain size.
75
79
gzip :: GzipSettings -> Middleware
80
gzip set app env sendResponse = app env $ \res ->
79
ResponseFile{} | gzipFiles set == GzipIgnore -> return res
82
ResponseFile{} | gzipFiles set == GzipIgnore -> sendResponse res
80
83
_ -> if "gzip" `elem` enc && not isMSIE6 && not (isEncoded res)
82
85
case (res, gzipFiles set) of
83
86
(ResponseFile s hs file Nothing, GzipCacheFolder cache) ->
84
87
case lookup "content-type" hs of
86
| gzipCheckMime set m -> liftIO $ compressFile s hs file cache
88
_ -> return $ compressE set res
89
| gzipCheckMime set m -> compressFile s hs file cache sendResponse
91
_ -> compressE set res sendResponse
91
94
enc = fromMaybe [] $ (splitCommas . S8.unpack)
92
95
`fmap` lookup "Accept-Encoding" (requestHeaders env)
94
97
isMSIE6 = "MSIE 6" `S.isInfixOf` ua
95
98
isEncoded res = isJust $ lookup "Content-Encoding" $ responseHeaders res
97
compressFile :: Status -> [Header] -> FilePath -> FilePath -> IO Response
98
compressFile s hs file cache = do
100
compressFile :: Status -> [Header] -> FilePath -> FilePath -> (Response -> IO a) -> IO a
101
compressFile s hs file cache sendResponse = do
99
102
e <- doesFileExist tmpfile
103
106
createDirectoryIfMissing True cache
105
try $ C.runResourceT $ CB.sourceFile file
106
C.$$ CZ.gzip C.=$ CB.sinkFile tmpfile
107
either onErr (const onSucc) x
108
IO.withBinaryFile file IO.ReadMode $ \inH ->
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
127
either onErr (const onSucc) (x :: Either SomeException ()) -- FIXME bad! don't catch all exceptions like that!
109
onSucc = return $ ResponseFile s (fixHeaders hs) tmpfile Nothing
129
onSucc = sendResponse $ responseFile s (fixHeaders hs) tmpfile Nothing
111
onErr :: SomeException -> IO Response
112
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
114
133
tmpfile = cache ++ '/' : map safe file
123
142
compressE :: GzipSettings
144
-> (Response -> IO a)
146
compressE set res sendResponse =
127
147
case lookup "content-type" hs of
128
148
Just m | gzipCheckMime set m ->
129
149
let hs' = fixHeaders hs
130
in ResponseSource s hs' $ \f -> wb $ \b -> f $
131
b C.$= builderToByteStringFlush
132
C.$= CZ.compressFlush 1 (CZ.WindowBits 31)
133
C.$= CL.map (fmap fromByteString)
150
in wb $ \body -> sendResponse $ responseStream s hs' $ \sendChunk flush -> do
151
(blazeRecv, blazeFinish) <- B.newBlazeRecv B.defaultStrategy
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 ()
170
sendChunk $ fromByteString bs'
172
Z.PRError e -> throwIO e
174
body sendBuilder flushBuilder
175
sendBuilder Blaze.flush
176
deflatePopper $ Z.finishDeflate deflate
177
_ -> sendResponse res
136
(s, hs, wb) = responseToSource res
179
(s, hs, wb) = responseToStream res
138
181
-- Remove Content-Length header, since we will certainly have a
139
182
-- different length after gzip compression.