{-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -- | Redis backend for "Network.Wai.Middleware.Cache". -- -- This backend uses "Database.Redis.Pile" for low-lewel operations. Main -- drawback of this backend is the fact that whole response -- will be consumed in memory. -- -- > cache -- > (redisBackend -- > -- use defaults, DB 0 and "myprefix" -- > R.defaultConnectInfo 0 -- > "myprefix" -- prefix for caching -- > Nothing -- no expiration -- > ["mytag"] -- one cache tag -- > (rawPathInfo) -- URL path as key -- > lookupETag -- And find "If-None-Match" -- > ) app -- our app module Network.Wai.Middleware.Cache.Redis ( -- * Cache backend redisBackend, ) where import Control.Monad.IO.Class (liftIO) import qualified Control.Arrow as A (first) import Data.Maybe (fromMaybe) import Data.ByteString (ByteString) import Data.ByteString.Lazy (fromChunks) import Data.CaseInsensitive (original, mk, CI(..)) import Data.Conduit (ResourceT, runResourceT, ($$), ($=), Flush(..)) import qualified Data.Conduit.List as CL import Data.Conduit.Blaze (builderToByteStringFlush) import Network.Wai (Request(..), Response(..), responseSource, responseLBS) import Network.Wai.Middleware.Cache (CacheBackend) import Network.HTTP.Types (Status(..)) import qualified Database.Redis as R import Database.Redis.Pile (pile) -- | Redis backend for "Network.Wai.Middleware.Cache". redisBackend :: R.ConnectInfo -- ^ Redis connection info. -> Integer -- ^ Redis DB Index. -> ByteString -- ^ Cache prefix for key and tags. -- See "Database.Redis.Pile" for details. -> Integer -- ^ Cache TTL. Use @Zero (0)@ for no expiration. -> [ByteString] -- ^ Cache Tags. See "Database.Redis.Pile" for details. -> (Request -> ByteString) -- ^ Cache key extraction function. -> (Request -> Maybe ByteString) -- ^ @ETag@ value extraction. To extract @If-None-Match@ header -- use 'lookupETag'. Use @(const Nothing)@ for block -- @304@-responses. -> CacheBackend redisBackend cInfo db cachePrefix ttl tags keyFn eTagFn app req = do cachedResponse <- liftIO $ do conn <- R.connect cInfo R.runRedis conn $ do _ <- R.select db pile cachePrefix key eTag $ liftIO . runResourceT $ do res <- app req (h, d) <- buildCachedResponse res return (d, h, tags, ttl) parseCachedResponse cachedResponse where (key, eTag) = (keyFn req, eTagFn req) ---------------------------------------------------------------------------- -- Internal ---------------------------------------------------------------------------- -- | Cached response type CachedResponse = (Int, ByteString, [(ByteString, ByteString)], [ByteString]) buildCachedResponse :: Response -> ResourceT IO (ByteString, CachedResponse) buildCachedResponse res = do bodyChunks <- bsrc $= builderToByteStringFlush $= CL.map fromChunk $$ CL.consume let bodyHash = fromMaybe "" $ lookup "etag" hs let stripHeaders = map (A.first original) hs return (bodyHash, (sc, sm, stripHeaders, bodyChunks)) where (Status sc sm, hs, bsrc) = responseSource res fromChunk (Chunk a) = a fromChunk Flush = "" -- | Parse cached response parseCachedResponse :: Maybe CachedResponse -> ResourceT IO (Maybe Response) parseCachedResponse Nothing = return Nothing parseCachedResponse (Just (sc, sm, hs, bodyChunks)) = do let wrappedHeaders = map (A.first mk) hs return $ Just $ responseLBS (Status sc sm) wrappedHeaders $ fromChunks bodyChunks