packages feed

webapp-0.0.1: Web/App/Assets.hs

{-# LANGUAGE OverloadedStrings #-}

{-|
Module      : Web.App.Assets
Copyright   : (c) Nathaniel Symer, 2015
License     : MIT
Maintainer  : nate@symer.io
Stability   : experimental
Portability : POSIX

General web app operations related to managing assets.
-}

module Web.App.Assets
(
  loadAsset
)
where
  
import Web.App.Monad
import qualified Web.App.FileCache as FC

import Web.Scotty.Trans as Scotty

import Network.HTTP.Types.Status (Status(..))
import Network.Mime

import System.FilePath
import System.Directory

import Control.Monad
import Control.Monad.IO.Class

import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TL
import qualified Data.Text.Lazy.Builder as TL

import qualified Text.CSS.Parse as CSS (parseNestedBlocks)
import qualified Text.CSS.Render as CSS (renderNestedBlocks)
import qualified Text.Jasmine as JS (minifym)

-- | Loads an asset from the 'FileCache' associated with the 'WebAppM' monad.
loadAsset :: (ScottyError e, WebAppState s) => FilePath -- ^ 'FilePath' relative to @assets/@ to load
                                            -> ActionT e (WebAppM s) ()
loadAsset assetsPath = do
  cache <- getCache
  exists <- liftIO $ doesFileExist relPath
  if not exists
    then doesntExist $ B.pack relPath
    else loadFromCache cache
  where
    mimetype = TL.fromStrict . T.decodeUtf8 . defaultMimeLookup . T.pack . takeFileName $ assetsPath
    relPath = "assets/" ++ assetsPath
    doesntExist pth = status . Status 404 $ mconcat ["File ", pth, " does not exist."]
    loadFromCache cache = (liftIO $ FC.lookup cache assetsPath) >>= (f cache)
    f _     (Just (Left err)) = status . Status 500 $ B.pack err
    f _     (Just (Right (cached, md5))) = do
      setHeader "Content-Type" $ mconcat [mimetype, "; charset=utf-8"]
      Scotty.addHeader "Vary" "Accept-Encoding"
      Scotty.setHeader "Content-Encoding" "gzip" -- files in FileCaches are gzipped
      Scotty.header "If-None-Match" >>= h . maybe False (== md5')
      where
        md5' = TL.decodeUtf8 $ BL.fromStrict md5
        h True = Scotty.status $ Status 304 ""
        h False = do
          Scotty.setHeader "ETag" md5'
          Scotty.raw $ BL.fromStrict cached
    f cache Nothing = do
      void $ liftIO $ FC.register' cache assetsPath (g mimetype)
      loadFromCache cache
    builderToBS = BL.toStrict . TL.encodeUtf8 . TL.toLazyText
    g "application/javascript" = fmap BL.toStrict . JS.minifym . BL.fromStrict
    g "text/css" = fmap (builderToBS . CSS.renderNestedBlocks) . CSS.parseNestedBlocks . T.decodeUtf8
    g _ = Right