module Foundation.Yesod.StaticContent ( addStaticContent ) where import Import.NoFoundation hiding (addStaticContent) import Foundation.Type import qualified Database.Memcached.Binary.IO as Memcached import qualified Data.ByteString.Lazy as Lazy import qualified Data.ByteString.Base64.URL as Base64 (encodeUnpadded) import Data.ByteArray (convert) import Crypto.Hash (SHAKE256) import Crypto.Hash.Conduit (sinkHash) import Data.Bits (Bits(zeroBits)) import qualified Data.Conduit.Combinators as C addStaticContent :: Text -> Text -> Lazy.ByteString -> HandlerFor UniWorX (Maybe (Either Text (Route UniWorX, [(Text, Text)]))) addStaticContent ext _mime content = do UniWorX{appWidgetMemcached, appSettings'} <- getYesod for ((,) <$> appWidgetMemcached <*> appWidgetMemcachedConf appSettings') $ \(mConn, WidgetMemcachedConf{ widgetMemcachedConf = MemcachedConf { memcachedExpiry }, widgetMemcachedBaseUrl }) -> do let expiry = maybe 0 ceiling memcachedExpiry touch = liftIO $ Memcached.touch expiry (encodeUtf8 $ pack fileName) mConn add = liftIO $ Memcached.add zeroBits expiry (encodeUtf8 $ pack fileName) content mConn absoluteLink = unpack widgetMemcachedBaseUrl fileName catchIf Memcached.isKeyNotFound touch . const $ handleIf Memcached.isKeyExists (const $ return ()) add return . Left $ pack absoluteLink where -- Generate a unique filename based on the content itself, this is used -- for deduplication so a collision resistant hash function is required -- -- SHA-3 (SHAKE256) seemed to be a future-proof choice -- -- Length of hash is 144 bits ~~instead of MD5's 128, so as to avoid -- padding after base64-conversion~~ for backwards compatability fileName = (<.> unpack ext) . unpack . decodeUtf8 . Base64.encodeUnpadded . (convert :: Digest (SHAKE256 144) -> ByteString) . runConduitPure $ C.sourceLazy content .| sinkHash