xstatic 0.1.0 → 0.2.0
raw patch · 3 files changed
+55/−36 lines, 3 filesdep −file-embed
Dependencies removed: file-embed
Files
- CHANGELOG.md +7/−0
- src/XStatic.hs +47/−34
- xstatic.cabal +1/−2
CHANGELOG.md view
@@ -1,5 +1,12 @@ # Changelog +## 0.2.0++- Add xstaticMiddleware.+- Enable StrictData.+- Remove file-embed dependency, use xstatic-th instead.+- Simplify request file path and etag data type.+ ## 0.1.0 - Initial release
src/XStatic.hs view
@@ -1,73 +1,89 @@ {-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE StrictData #-} module XStatic ( -- * xstatic api XStaticFile (..), xstaticApp,+ xstaticMiddleware, - -- * file-embed re-export- embedFile,+ -- * helpers+ isGzip, ) where import Data.Binary.Builder (Builder, fromByteString) import Data.ByteString (ByteString) import Data.ByteString qualified as BS-import Data.ByteString.Char8 (breakEnd, pack)-import Data.ByteString.Char8 qualified as BS8-import Data.FileEmbed (embedFile)+import Data.ByteString.Char8 (pack)+import Data.ByteString.Unsafe qualified as BS (unsafeDrop) import Data.Map.Strict qualified as Map-import Data.Version (Version, showVersion) import Network.HTTP.Types.Header (ResponseHeaders) import Network.HTTP.Types.Status qualified as HTTP import Network.Wai qualified --- | A static file definition+{- | A static file definition.++ Use the @xstatic-th@ or @file-embed@ library to embed a local file.+-} data XStaticFile = XStaticFile- { name :: ByteString- -- ^ The expected request filename. The name must not have any slash '/'.- , content :: ByteString- -- ^ The file content gzip compressed.- , contentVersion :: Version- -- ^ The file version for the etag header.- , contentType :: ByteString- -- ^ The content type, e.g. `text/javascript` or `text/css`.+ { xfPath :: ByteString+ -- ^ The expected request path.+ , xfContent :: ByteString+ -- ^ The file content.+ , xfETag :: ByteString+ -- ^ The etag header value.+ , xfType :: ByteString+ -- ^ The content type, e.g. \"application\/javascript\" or \"text\/css\". } {- | Create a wai application to serve 'XStaticFile'. - The request are served whenever the basename match, ignoring the parent directory or the query string.+ The \"\/xstatic\/\" request path prefix is ignored. -} xstaticApp :: [XStaticFile] -> Network.Wai.Application-xstaticApp xs = \req resp ->- let (_, basename) = breakEnd (== '/') (Network.Wai.rawPathInfo req)- in resp $ case Map.lookup basename files of- Just (builder, headers) ->- let body = case Network.Wai.requestMethod req of- "HEAD" -> mempty- _ -> builder- in Network.Wai.responseBuilder HTTP.status200 headers body- Nothing -> Network.Wai.responseLBS HTTP.status404 mempty mempty+xstaticApp xs = \req resp -> do+ let basePath = Network.Wai.rawPathInfo req+ requestPath+ | "/xstatic/" `BS.isPrefixOf` basePath = BS.unsafeDrop 8 basePath+ | otherwise = basePath++ resp $ case Map.lookup requestPath files of+ Just (builder, headers) ->+ let body = case Network.Wai.requestMethod req of+ "HEAD" -> mempty+ _ -> builder+ in Network.Wai.responseBuilder HTTP.status200 headers body+ Nothing -> Network.Wai.responseLBS HTTP.status404 mempty mempty where files :: Map.Map ByteString (Builder, ResponseHeaders) files = Map.fromList $ map toItem xs toItem :: XStaticFile -> (ByteString, (Builder, ResponseHeaders)) toItem xf =- ( name xf+ ( xfPath xf ,- ( fromByteString $ content xf- , (addGzipHeader $ content xf)+ ( fromByteString $ xfContent xf+ , (addGzipHeader $ xfContent xf) [ ("cache-control", "public, max-age=604800")- , ("content-length", pack . show . BS.length $ content xf)- , ("content-type", contentType xf)+ , ("content-length", pack . show . BS.length $ xfContent xf)+ , ("content-type", xfType xf) , ("connection", "keep-alive")- , ("etag", versionToEtag $ contentVersion xf)+ , ("etag", xfETag xf) , ("keep-alive", "timeout=5, max=100") ] ) ) +-- | Serve 'XStaticFile' using 'xstaticApp' when the provided application returns a 404.+xstaticMiddleware :: [XStaticFile] -> Network.Wai.Middleware+xstaticMiddleware xs app req resp = app req handleAppResp+ where+ staticApp = xstaticApp xs+ handleAppResp appResp = case HTTP.statusCode (Network.Wai.responseStatus appResp) of+ 404 -> staticApp req resp+ _ -> resp appResp+ addGzipHeader :: ByteString -> ResponseHeaders -> ResponseHeaders addGzipHeader fileContent | isGzip fileContent = (("content-encoding", "gzip") :)@@ -75,6 +91,3 @@ isGzip :: ByteString -> Bool isGzip = BS.isPrefixOf "\x1f\x8b\x08" -- the gzip magic number for deflate--versionToEtag :: Version -> ByteString-versionToEtag = BS8.unwords . BS8.split '.' . pack . showVersion
xstatic.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: xstatic-version: 0.1.0+version: 0.2.0 synopsis: Low-Fat static file packaging for Haskell projects description: Low-Fat static file packaging for Haskell projects. category: JavaScript@@ -36,7 +36,6 @@ , binary , bytestring , containers- , file-embed , http-types , wai default-language: Haskell2010