{-# LANGUAGE TemplateHaskell, QuasiQuotes, OverloadedStrings, MagicHash, CPP #-}
module WaiAppStatic.Storage.Embedded.TH(
Etag
, EmbeddableEntry(..)
, mkSettings
) where
import Data.ByteString.Builder.Extra (byteStringInsert)
import Codec.Compression.GZip (compress)
import Data.ByteString.Unsafe (unsafePackAddressLen)
import Data.Either (lefts, rights)
import GHC.Exts (Int(..))
import Language.Haskell.TH
import Network.Mime (MimeType, defaultMimeLookup)
import System.IO.Unsafe (unsafeDupablePerformIO)
import WaiAppStatic.Types
import WaiAppStatic.Storage.Filesystem (defaultWebAppSettings)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
#if !MIN_VERSION_template_haskell(2, 8, 0)
import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString.Lazy.Char8 as BL8
#endif
import qualified Data.HashMap.Strict as M
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Network.Wai as W
type Etag = T.Text
data EmbeddableEntry = EmbeddableEntry {
EmbeddableEntry -> Text
eLocation :: T.Text
, EmbeddableEntry -> ByteString
eMimeType :: MimeType
, EmbeddableEntry -> Either (Text, ByteString) ExpQ
eContent :: Either (Etag, BL.ByteString) ExpQ
}
data EmbeddedEntry = EmbeddedEntry {
EmbeddedEntry -> Text
embLocation :: !T.Text
, EmbeddedEntry -> ByteString
embMime :: !MimeType
, EmbeddedEntry -> ByteString
embEtag :: !B.ByteString
, EmbeddedEntry -> Bool
embCompressed :: !Bool
, EmbeddedEntry -> ByteString
embContent :: !B.ByteString
}
data ReloadEntry = ReloadEntry {
ReloadEntry -> Text
reloadLocation :: !T.Text
, ReloadEntry -> ByteString
reloadMime :: !MimeType
, ReloadEntry -> IO (Text, ByteString)
reloadContent :: IO (T.Text, BL.ByteString)
}
bytestringE :: B.ByteString -> ExpQ
#if MIN_VERSION_template_haskell(2, 8, 0)
bytestringE :: ByteString -> ExpQ
bytestringE ByteString
b = [| unsafeDupablePerformIO (unsafePackAddressLen (I# $lenE) $ctE) |]
where
lenE :: ExpQ
lenE = forall (m :: * -> *). Quote m => Lit -> m Exp
litE forall a b. (a -> b) -> a -> b
$ Integer -> Lit
intPrimL forall a b. (a -> b) -> a -> b
$ forall a. Integral a => a -> Integer
toInteger forall a b. (a -> b) -> a -> b
$ ByteString -> Int
B.length ByteString
b
ctE :: ExpQ
ctE = forall (m :: * -> *). Quote m => Lit -> m Exp
litE forall a b. (a -> b) -> a -> b
$ [Word8] -> Lit
stringPrimL forall a b. (a -> b) -> a -> b
$ ByteString -> [Word8]
B.unpack ByteString
b
#else
bytestringE b =
[| B8.pack $s |]
where
s = litE $ stringL $ B8.unpack b
#endif
bytestringLazyE :: BL.ByteString -> ExpQ
#if MIN_VERSION_template_haskell(2, 8, 0)
bytestringLazyE :: ByteString -> ExpQ
bytestringLazyE ByteString
b = [| unsafeDupablePerformIO (unsafePackAddressLen (I# $lenE) $ctE) |]
where
lenE :: ExpQ
lenE = forall (m :: * -> *). Quote m => Lit -> m Exp
litE forall a b. (a -> b) -> a -> b
$ Integer -> Lit
intPrimL forall a b. (a -> b) -> a -> b
$ forall a. Integral a => a -> Integer
toInteger forall a b. (a -> b) -> a -> b
$ ByteString -> Int64
BL.length ByteString
b
ctE :: ExpQ
ctE = forall (m :: * -> *). Quote m => Lit -> m Exp
litE forall a b. (a -> b) -> a -> b
$ [Word8] -> Lit
stringPrimL forall a b. (a -> b) -> a -> b
$ ByteString -> [Word8]
BL.unpack ByteString
b
#else
bytestringLazyE b =
[| B8.pack $s |]
where
s = litE $ stringL $ BL8.unpack b
#endif
mkEntry :: EmbeddableEntry -> ExpQ
mkEntry :: EmbeddableEntry -> ExpQ
mkEntry (EmbeddableEntry Text
loc ByteString
mime (Left (Text
etag, ByteString
ct))) =
[| Left $ EmbeddedEntry (T.pack $locE)
$(bytestringE mime)
$(bytestringE $ T.encodeUtf8 etag)
(1 == I# $compressedE)
$(bytestringLazyE ct')
|]
where
locE :: ExpQ
locE = forall (m :: * -> *). Quote m => Lit -> m Exp
litE forall a b. (a -> b) -> a -> b
$ String -> Lit
stringL forall a b. (a -> b) -> a -> b
$ Text -> String
T.unpack Text
loc
(Bool
compressed, ByteString
ct') = ByteString -> ByteString -> (Bool, ByteString)
tryCompress ByteString
mime ByteString
ct
compressedE :: ExpQ
compressedE = forall (m :: * -> *). Quote m => Lit -> m Exp
litE forall a b. (a -> b) -> a -> b
$ Integer -> Lit
intPrimL forall a b. (a -> b) -> a -> b
$ if Bool
compressed then Integer
1 else Integer
0
mkEntry (EmbeddableEntry Text
loc ByteString
mime (Right ExpQ
expr)) =
[| Right $ ReloadEntry (T.pack $locE)
$(bytestringE mime)
$expr
|]
where
locE :: ExpQ
locE = forall (m :: * -> *). Quote m => Lit -> m Exp
litE forall a b. (a -> b) -> a -> b
$ String -> Lit
stringL forall a b. (a -> b) -> a -> b
$ Text -> String
T.unpack Text
loc
embeddedToFile :: EmbeddedEntry -> File
embeddedToFile :: EmbeddedEntry -> File
embeddedToFile EmbeddedEntry
entry = File
{ fileGetSize :: Integer
fileGetSize = forall a b. (Integral a, Num b) => a -> b
fromIntegral forall a b. (a -> b) -> a -> b
$ ByteString -> Int
B.length forall a b. (a -> b) -> a -> b
$ EmbeddedEntry -> ByteString
embContent EmbeddedEntry
entry
, fileToResponse :: Status -> ResponseHeaders -> Response
fileToResponse = \Status
s ResponseHeaders
h ->
let h' :: ResponseHeaders
h' = if EmbeddedEntry -> Bool
embCompressed EmbeddedEntry
entry
then ResponseHeaders
h forall a. [a] -> [a] -> [a]
++ [(HeaderName
"Content-Encoding", ByteString
"gzip")]
else ResponseHeaders
h
in Status -> ResponseHeaders -> Builder -> Response
W.responseBuilder Status
s ResponseHeaders
h' forall a b. (a -> b) -> a -> b
$ ByteString -> Builder
byteStringInsert forall a b. (a -> b) -> a -> b
$ EmbeddedEntry -> ByteString
embContent EmbeddedEntry
entry
, fileName :: Piece
fileName = Text -> Piece
unsafeToPiece forall a b. (a -> b) -> a -> b
$ EmbeddedEntry -> Text
embLocation EmbeddedEntry
entry
, fileGetHash :: IO (Maybe ByteString)
fileGetHash = forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$ if ByteString -> Bool
B.null (EmbeddedEntry -> ByteString
embEtag EmbeddedEntry
entry)
then forall a. Maybe a
Nothing
else forall a. a -> Maybe a
Just forall a b. (a -> b) -> a -> b
$ EmbeddedEntry -> ByteString
embEtag EmbeddedEntry
entry
, fileGetModified :: Maybe EpochTime
fileGetModified = forall a. Maybe a
Nothing
}
reloadToFile :: ReloadEntry -> IO File
reloadToFile :: ReloadEntry -> IO File
reloadToFile ReloadEntry
entry = do
(Text
etag, ByteString
ct) <- ReloadEntry -> IO (Text, ByteString)
reloadContent ReloadEntry
entry
let etag' :: ByteString
etag' = Text -> ByteString
T.encodeUtf8 Text
etag
forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$ File
{ fileGetSize :: Integer
fileGetSize = forall a b. (Integral a, Num b) => a -> b
fromIntegral forall a b. (a -> b) -> a -> b
$ ByteString -> Int64
BL.length ByteString
ct
, fileToResponse :: Status -> ResponseHeaders -> Response
fileToResponse = \Status
s ResponseHeaders
h -> Status -> ResponseHeaders -> ByteString -> Response
W.responseLBS Status
s ResponseHeaders
h ByteString
ct
, fileName :: Piece
fileName = Text -> Piece
unsafeToPiece forall a b. (a -> b) -> a -> b
$ ReloadEntry -> Text
reloadLocation ReloadEntry
entry
, fileGetHash :: IO (Maybe ByteString)
fileGetHash = forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$ if Text -> Bool
T.null Text
etag then forall a. Maybe a
Nothing else forall a. a -> Maybe a
Just ByteString
etag'
, fileGetModified :: Maybe EpochTime
fileGetModified = forall a. Maybe a
Nothing
}
filemapToSettings :: M.HashMap T.Text (MimeType, IO File) -> StaticSettings
filemapToSettings :: HashMap Text (ByteString, IO File) -> StaticSettings
filemapToSettings HashMap Text (ByteString, IO File)
mfiles = (String -> StaticSettings
defaultWebAppSettings String
"")
{ ssLookupFile :: Pieces -> IO LookupResult
ssLookupFile = Pieces -> IO LookupResult
lookupFile
, ssGetMimeType :: File -> IO ByteString
ssGetMimeType = forall {m :: * -> *}. Monad m => File -> m ByteString
lookupMime
}
where
piecesToFile :: Pieces -> Text
piecesToFile Pieces
p = Text -> [Text] -> Text
T.intercalate Text
"/" forall a b. (a -> b) -> a -> b
$ forall a b. (a -> b) -> [a] -> [b]
map Piece -> Text
fromPiece Pieces
p
lookupFile :: Pieces -> IO LookupResult
lookupFile [] = forall (m :: * -> *) a. Monad m => a -> m a
return LookupResult
LRNotFound
lookupFile Pieces
p =
case forall k v. (Eq k, Hashable k) => k -> HashMap k v -> Maybe v
M.lookup (Pieces -> Text
piecesToFile Pieces
p) HashMap Text (ByteString, IO File)
mfiles of
Maybe (ByteString, IO File)
Nothing -> forall (m :: * -> *) a. Monad m => a -> m a
return LookupResult
LRNotFound
Just (ByteString
_,IO File
act) -> File -> LookupResult
LRFile forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> IO File
act
lookupMime :: File -> m ByteString
lookupMime (File { fileName :: File -> Piece
fileName = Piece
p }) =
case forall k v. (Eq k, Hashable k) => k -> HashMap k v -> Maybe v
M.lookup (Piece -> Text
fromPiece Piece
p) HashMap Text (ByteString, IO File)
mfiles of
Just (ByteString
mime,IO File
_) -> forall (m :: * -> *) a. Monad m => a -> m a
return ByteString
mime
Maybe (ByteString, IO File)
Nothing -> forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$ Text -> ByteString
defaultMimeLookup forall a b. (a -> b) -> a -> b
$ Piece -> Text
fromPiece Piece
p
entriesToSt :: [Either EmbeddedEntry ReloadEntry] -> StaticSettings
entriesToSt :: [Either EmbeddedEntry ReloadEntry] -> StaticSettings
entriesToSt [Either EmbeddedEntry ReloadEntry]
entries = HashMap Text (ByteString, IO File)
hmap seq :: forall a b. a -> b -> b
`seq` HashMap Text (ByteString, IO File) -> StaticSettings
filemapToSettings HashMap Text (ByteString, IO File)
hmap
where
embFiles :: [(Text, (ByteString, IO File))]
embFiles = [ (EmbeddedEntry -> Text
embLocation EmbeddedEntry
e, (EmbeddedEntry -> ByteString
embMime EmbeddedEntry
e, forall (m :: * -> *) a. Monad m => a -> m a
return forall a b. (a -> b) -> a -> b
$ EmbeddedEntry -> File
embeddedToFile EmbeddedEntry
e)) | EmbeddedEntry
e <- forall a b. [Either a b] -> [a]
lefts [Either EmbeddedEntry ReloadEntry]
entries]
reloadFiles :: [(Text, (ByteString, IO File))]
reloadFiles = [ (ReloadEntry -> Text
reloadLocation ReloadEntry
r, (ReloadEntry -> ByteString
reloadMime ReloadEntry
r, ReloadEntry -> IO File
reloadToFile ReloadEntry
r)) | ReloadEntry
r <- forall a b. [Either a b] -> [b]
rights [Either EmbeddedEntry ReloadEntry]
entries]
hmap :: HashMap Text (ByteString, IO File)
hmap = forall k v. (Eq k, Hashable k) => [(k, v)] -> HashMap k v
M.fromList forall a b. (a -> b) -> a -> b
$ [(Text, (ByteString, IO File))]
embFiles forall a. [a] -> [a] -> [a]
++ [(Text, (ByteString, IO File))]
reloadFiles
mkSettings :: IO [EmbeddableEntry] -> ExpQ
mkSettings :: IO [EmbeddableEntry] -> ExpQ
mkSettings IO [EmbeddableEntry]
action = do
[EmbeddableEntry]
entries <- forall a. IO a -> Q a
runIO IO [EmbeddableEntry]
action
[| entriesToSt $(listE $ map mkEntry entries) |]
shouldCompress :: MimeType -> Bool
shouldCompress :: ByteString -> Bool
shouldCompress ByteString
m = ByteString
"text/" ByteString -> ByteString -> Bool
`B.isPrefixOf` ByteString
m Bool -> Bool -> Bool
|| ByteString
m forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [ByteString]
extra
where
extra :: [ByteString]
extra = [ ByteString
"application/json"
, ByteString
"application/javascript"
, ByteString
"application/ecmascript"
]
tryCompress :: MimeType -> BL.ByteString -> (Bool, BL.ByteString)
tryCompress :: ByteString -> ByteString -> (Bool, ByteString)
tryCompress ByteString
mime ByteString
ct
| ByteString -> Bool
shouldCompress ByteString
mime = (Bool
c, ByteString
ct')
| Bool
otherwise = (Bool
False, ByteString
ct)
where
compressed :: ByteString
compressed = ByteString -> ByteString
compress ByteString
ct
c :: Bool
c = ByteString -> Int64
BL.length ByteString
compressed forall a. Ord a => a -> a -> Bool
< ByteString -> Int64
BL.length ByteString
ct
ct' :: ByteString
ct' = if Bool
c then ByteString
compressed else ByteString
ct