There were two build issues on GHC 7.4: using atomicModifyIORef' and ByteString.Lazy.toStrict, both of which were missing. These are now fixed. In addition, looking at the IORef code more closely, we want the quite a bit of strictness in the IORef. The common case is that the widget content already exists in the map (every reload of a page will call embedStaticContent), but until we force the map the thunks holding the duplicated generated content will be kept around, leaking memory. This will be the common situation since the vast majority of the time the content already exists. Since the containers module does not have a strict map until 0.5, use unordered containers which does have a strict map.
170 lines
6.8 KiB
Haskell
170 lines
6.8 KiB
Haskell
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE QuasiQuotes #-}
|
|
{-# LANGUAGE TemplateHaskell #-}
|
|
{-# LANGUAGE TupleSections #-}
|
|
{-# LANGUAGE TypeFamilies #-}
|
|
{-# LANGUAGE CPP #-}
|
|
module Yesod.EmbeddedStatic.Internal (
|
|
EmbeddedStatic(..)
|
|
, Route(..)
|
|
, ComputedEntry(..)
|
|
, devEmbed
|
|
, prodEmbed
|
|
, develApp
|
|
, AddStaticContent
|
|
, staticContentHelper
|
|
, widgetSettings
|
|
) where
|
|
|
|
import Control.Applicative ((<$>))
|
|
import Data.IORef
|
|
import Language.Haskell.TH
|
|
import Network.HTTP.Types (Status(..), status404, status200, status304)
|
|
import Network.Mime (MimeType)
|
|
import Network.Wai
|
|
import Network.Wai.Application.Static (defaultWebAppSettings, staticApp)
|
|
import WaiAppStatic.Types
|
|
import Yesod.Core
|
|
( HandlerT
|
|
, ParseRoute(..)
|
|
, RenderRoute(..)
|
|
, Yesod(..)
|
|
, getYesod
|
|
, liftIO
|
|
)
|
|
import qualified Data.ByteString.Lazy as BL
|
|
import qualified Data.Text as T
|
|
import qualified Data.Text.Encoding as T
|
|
import qualified Data.HashMap.Strict as M
|
|
import qualified WaiAppStatic.Storage.Embedded as Static
|
|
|
|
import Yesod.Static (base64md5)
|
|
import Yesod.EmbeddedStatic.Types
|
|
|
|
#if !MIN_VERSION_base(4,6,0)
|
|
-- copied from base
|
|
atomicModifyIORef' :: IORef a -> (a -> (a,b)) -> IO b
|
|
atomicModifyIORef' ref f = do
|
|
b <- atomicModifyIORef ref
|
|
(\x -> let (a, b) = f x
|
|
in (a, a `seq` b))
|
|
b `seq` return b
|
|
#endif
|
|
|
|
-- | The subsite for the embedded static file server.
|
|
data EmbeddedStatic = EmbeddedStatic {
|
|
stApp :: !Application
|
|
, widgetFiles :: !(IORef (M.HashMap T.Text File))
|
|
}
|
|
|
|
instance RenderRoute EmbeddedStatic where
|
|
data Route EmbeddedStatic = EmbeddedResourceR [T.Text] [(T.Text,T.Text)]
|
|
| EmbeddedWidgetR T.Text
|
|
deriving (Eq, Show, Read)
|
|
renderRoute (EmbeddedResourceR x y) = ("res":x, y)
|
|
renderRoute (EmbeddedWidgetR h) = (["widget",h], [])
|
|
instance ParseRoute EmbeddedStatic where
|
|
parseRoute (("res":x), y) = Just $ EmbeddedResourceR x y
|
|
parseRoute (["widget",h], _) = Just $ EmbeddedWidgetR h
|
|
parseRoute _ = Nothing
|
|
|
|
-- | At compile time, one of these is created for every 'Entry' created by
|
|
-- the generators. The cLink is a template haskell expression of type @Route EmbeddedStatic@.
|
|
data ComputedEntry = ComputedEntry {
|
|
cHaskellName :: Maybe Name -- ^ Optional haskell name to create a variable for the route
|
|
, cStEntry :: Static.EmbeddableEntry -- ^ The entry to be embedded into the executable
|
|
, cLink :: ExpQ -- ^ The route for this entry
|
|
}
|
|
|
|
mkStr :: String -> ExpQ
|
|
mkStr = litE . stringL
|
|
|
|
-- | Create a 'ComputedEntry' for development mode, reloading the content on every request.
|
|
devEmbed :: Entry -> IO ComputedEntry
|
|
devEmbed e = return computed
|
|
where
|
|
st = Static.EmbeddableEntry {
|
|
Static.eLocation = "res/" `T.append` T.pack (ebLocation e)
|
|
, Static.eMimeType = ebMimeType e
|
|
, Static.eContent = Right [| $(ebDevelReload e) >>= \c ->
|
|
return (T.pack (base64md5 c), c) |]
|
|
}
|
|
link = [| EmbeddedResourceR (T.splitOn (T.pack "/") $ T.pack $(mkStr $ ebLocation e)) [] |]
|
|
computed = ComputedEntry (ebHaskellName e) st link
|
|
|
|
-- | Create a 'ComputedEntry' for production mode, hashing and embedding the content into the executable.
|
|
prodEmbed :: Entry -> IO ComputedEntry
|
|
prodEmbed e = do
|
|
ct <- ebProductionContent e
|
|
let hash = base64md5 ct
|
|
link = [| EmbeddedResourceR (T.splitOn (T.pack "/") $ T.pack $(mkStr $ ebLocation e))
|
|
[(T.pack "etag", T.pack $(mkStr hash))] |]
|
|
st = Static.EmbeddableEntry {
|
|
Static.eLocation = "res/" `T.append` T.pack (ebLocation e)
|
|
, Static.eMimeType = ebMimeType e
|
|
, Static.eContent = Left (T.pack hash, ct)
|
|
}
|
|
return $ ComputedEntry (ebHaskellName e) st link
|
|
|
|
tryExtraDevelFiles :: [[T.Text] -> IO (Maybe (MimeType, BL.ByteString))] -> Application
|
|
tryExtraDevelFiles [] _ = return $ responseLBS status404 [] ""
|
|
tryExtraDevelFiles (f:fs) r = do
|
|
mct <- liftIO $ f $ drop 1 $ pathInfo r -- drop the initial "res"
|
|
case mct of
|
|
Nothing -> tryExtraDevelFiles fs r
|
|
Just (mime, ct) -> do
|
|
let hash = T.encodeUtf8 $ T.pack $ base64md5 ct
|
|
let headers = [ ("Content-Type", mime)
|
|
, ("ETag", hash)
|
|
]
|
|
case lookup "If-None-Match" (requestHeaders r) of
|
|
Just h | hash == h -> return $ responseLBS status304 headers ""
|
|
_ -> return $ responseLBS status200 headers ct
|
|
|
|
-- | Helper to create the development application at runtime
|
|
develApp :: StaticSettings -> [[T.Text] -> IO (Maybe (MimeType, BL.ByteString))] -> Application
|
|
develApp settings extra req = do
|
|
resp <- staticApp settings {ssMaxAge = NoMaxAge} req
|
|
if statusCode (responseStatus resp) == 404
|
|
then tryExtraDevelFiles extra req
|
|
else return resp
|
|
|
|
-- | The type of 'addStaticContent'
|
|
type AddStaticContent site = T.Text -> T.Text -> BL.ByteString
|
|
-> HandlerT site IO (Maybe (Either T.Text (Route site, [(T.Text, T.Text)])))
|
|
|
|
-- | Helper for embedStaticContent and embedLicensedStaticContent.
|
|
staticContentHelper :: Yesod site
|
|
=> (site -> EmbeddedStatic)
|
|
-> (Route EmbeddedStatic -> Route site)
|
|
-> (BL.ByteString -> Either a BL.ByteString)
|
|
-> AddStaticContent site
|
|
staticContentHelper getStatic staticR minify ext _ ct = do
|
|
wIORef <- widgetFiles . getStatic <$> getYesod
|
|
let hash = T.pack $ base64md5 ct
|
|
hash' = Just $ T.encodeUtf8 hash
|
|
filename = T.concat [hash, ".", ext]
|
|
content = case ext of
|
|
"js" -> either (const ct) id $ minify ct
|
|
_ -> ct
|
|
file = File
|
|
{ fileGetSize = fromIntegral $ BL.length content
|
|
, fileToResponse = \s h -> responseLBS s h content
|
|
, fileName = unsafeToPiece filename
|
|
, fileGetHash = return hash'
|
|
, fileGetModified = Nothing
|
|
}
|
|
liftIO $ atomicModifyIORef' wIORef $ \m ->
|
|
(M.insertWith (\old _ -> old) filename file m, ())
|
|
|
|
return $ Just $ Right (staticR $ EmbeddedWidgetR filename, [])
|
|
|
|
-- | Create a wai-app-static settings based on the IORef inside the EmbeddedStaic site.
|
|
widgetSettings :: EmbeddedStatic -> StaticSettings
|
|
widgetSettings es = (defaultWebAppSettings "") { ssLookupFile = lookupFile }
|
|
where
|
|
lookupFile [_,p] = do -- The first part of the path is "widget"
|
|
m <- readIORef $ widgetFiles es
|
|
return $ maybe LRNotFound LRFile $ M.lookup (fromPiece p) m
|
|
lookupFile _ = return LRNotFound
|