on master help sign in
renamed files, directories, and content
commitf7a61d4e9af0b4e6880cf07879e15ff41c9b0043 authorhsloan <ishaqsloan@gmail.com> dateTue 22 Aug 2017, 15:28 (9 years ago) parentcc77d3c
4 files changed+351
obelisk-asset/README.md+1 obelisk-asset/obelisk-asset.cabal+33 obelisk-asset/src/Obelisk/Asset/Accept.hs+212 obelisk-asset/src/Obelisk/Asset/Serve.hs+105
obelisk-asset/README.mdnew file+1
@@ -0,0 +1 @@
1+# obelisk-asset
obelisk-asset/obelisk-asset.cabalnew file+33
@@ -0,0 +1,33 @@
1+Name: obelisk-asset
2+Version: 0.1
3+Synopsis: Project Synopsis Here
4+Description: Project Description Here
5+License: AllRightsReserved
6+Author: Author
7+Maintainer: maintainer@example.com
8+Stability: Experimental
9+Category: Web
10+Build-type: Simple
11+Cabal-version: >=1.2
12+
13+library
14+ hs-source-dirs: src
15+
16+ build-depends:
17+ base
18+ , attoparsec
19+ , bytestring
20+ , containers
21+ , directory
22+ , filepath
23+ , snap
24+ , snap-core
25+ , text
26+ , transformers
27+ , unix
28+ exposed-modules:
29+ Obelisk.Asset.Accept
30+ Obelisk.Asset.Serve
31+
32+ other-extensions: TemplateHaskell
33+ ghc-options: -threaded -Wall -Werror -fno-warn-unused-do-bind -fwarn-tabs -funbox-strict-fields -O2 -fprof-auto-calls -rtsopts
obelisk-asset/src/Obelisk/Asset/Accept.hsnew file+212
@@ -0,0 +1,212 @@
1+{-# LANGUAGE CPP, OverloadedStrings, BangPatterns, ScopedTypeVariables, GeneralizedNewtypeDeriving #-}
2+module Obelisk.Asset.Accept where
3+
4+#ifdef __GLASGOW_HASKELL__
5+#if __GLASGOW_HASKELL__ < 710
6+import Prelude hiding (takeWhile, sequence)
7+import Control.Monad hiding (sequence)
8+import Data.Traversable (sequence)
9+#else
10+import Prelude hiding (takeWhile)
11+import Control.Monad
12+#endif
13+#else
14+import Prelude hiding (takeWhile)
15+import Control.Monad
16+#endif
17+
18+import Control.Applicative
19+import qualified Data.ByteString as BS
20+import Data.Attoparsec.ByteString as A
21+import Data.Attoparsec.ByteString.Char8 as AC8
22+import Data.ByteString (ByteString)
23+import Data.Maybe
24+import Data.Word (Word8)
25+import Data.Monoid
26+import Data.Fixed
27+import Data.Map (Map)
28+import qualified Data.Map as Map
29+import Data.Either
30+import Control.Arrow
31+import Data.List
32+import Data.String
33+import Data.Proxy
34+import Data.Ord
35+
36+newtype Encoding = Encoding { unEncoding :: ByteString } deriving (Show, Read, Eq, Ord, IsString)
37+type QValueResolution = E3 -- This resolution is given by the standard
38+newtype QValue = QValue { unQValue :: Fixed QValueResolution } deriving (Show, Read, Eq, Ord)
39+
40+-- | When no Accept-encoding header is present, prefer identity, then gzip or compress, then anything else available
41+--
42+-- See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
43+missingAcceptableEncodings :: AcceptableEncodings
44+missingAcceptableEncodings = AcceptableEncodings
45+ { _acceptableEncodings_defaultQValue = QValue 0.001
46+ , _acceptableEncodings_byEncoding = Map.fromList
47+ [ (Encoding "identity", QValue 1)
48+ , (Encoding "gzip", QValue 0.5)
49+ , (Encoding "compress", QValue 0.5)
50+ ]
51+ }
52+
53+data AcceptableEncodings
54+ = AcceptableEncodings { _acceptableEncodings_defaultQValue :: QValue
55+ , _acceptableEncodings_byEncoding :: Map Encoding QValue
56+ }
57+ deriving (Show, Read, Eq, Ord)
58+
59+chooseEncoding :: [Encoding] -> AcceptableEncodings -> Maybe Encoding
60+chooseEncoding es ae = fmap snd $ listToMaybe $ sort $ catMaybes $ zipWith f es [(1::Int)..]
61+ where f e n = case encodingQValue e ae of
62+ QValue 0 -> Nothing
63+ q -> Just ((Down q, n), e) -- Choose by quality first (in descending order), then by the server's preference order
64+
65+encodingQValue :: Encoding -> AcceptableEncodings -> QValue
66+encodingQValue e ae = Map.findWithDefault (_acceptableEncodings_defaultQValue ae) e $ _acceptableEncodings_byEncoding ae
67+
68+acceptEncodingBody :: Parser AcceptableEncodings
69+acceptEncodingBody = do
70+ l <- hashRule (Just 1) Nothing $ do
71+ c <- (Nothing <$ literal "*") <|> (Just <$> contentCoding)
72+ mq <- optional $ do
73+ literal ";"
74+ literal "q"
75+ literal "="
76+ qvalue
77+ let q = fromMaybe (QValue 1) mq
78+ return (c, q)
79+ let (stars, specificEncodings) = partitionEithers $ flip map l $ \(c, q) -> case c of
80+ Nothing -> Left q
81+ Just n -> Right (n, q)
82+ starQValue <- case stars of
83+ [] -> return Nothing
84+ [q] -> return $ Just q
85+ _ -> fail "acceptEncodingBody: multiple * values provided"
86+ byEncodingProvided <- sequence $ Map.fromListWithKey (\k _ _ -> fail $ "acceptEncodingBody: encoding " <> show k <> " repeated multiple times") $ map (second return) specificEncodings
87+ let defaultIdentityQValue = fromMaybe (QValue 1) starQValue -- identity has a default qvalue of 1 unless * is given a different qvalue explicitly
88+ defaultQValue = fromMaybe (QValue 0) starQValue
89+ byEncoding = Map.filter (/= defaultQValue) -- Canonicalize: qvalues equal to the default are redundant
90+ . Map.alter (Just . fromMaybe defaultIdentityQValue) "identity" -- Add implicit "identity" encoding, unless it has been explicitly added
91+ $ byEncodingProvided
92+ return $ AcceptableEncodings defaultQValue byEncoding
93+
94+-- | See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9
95+qvalue :: Parser QValue
96+qvalue = do
97+ skipMany lws
98+ q0 <|> q1
99+ where q0 = do
100+ char '0'
101+ decimals <- option [] $ do
102+ char '.'
103+ starRule (Just 0) (Just numAllowedDigits) digit
104+ return $ QValue $ MkFixed $ fromIntegral $ (read ('0' : decimals) :: Int) * 10 ^ (numAllowedDigits - length decimals)
105+ q1 = do
106+ char '1'
107+ option () $ do
108+ char '.'
109+ starRule (Just 0) (Just numAllowedDigits) $ char '0'
110+ return ()
111+ return $ QValue 1
112+ numAllowedDigits :: Int
113+ numAllowedDigits = fromIntegral $ resolution (Proxy :: Proxy QValueResolution)
114+
115+contentCoding :: Parser Encoding
116+contentCoding = Encoding <$> token
117+
118+token :: Parser ByteString
119+token = skipMany lws >> A.takeWhile1 isTokenChar
120+
121+literal :: ByteString -> Parser ()
122+literal s = void $ skipMany lws >> string s
123+
124+isTokenChar :: Word8 -> Bool
125+isTokenChar c = isChar c && not (isCtl c || isSeparator c)
126+
127+isChar :: Word8 -> Bool
128+isChar c = c <= 127
129+
130+isCtl :: Word8 -> Bool
131+isCtl c = c <= 31 || c == 127
132+
133+isSeparator :: Word8 -> Bool
134+isSeparator c = BS.elem c $ spVal `BS.cons` htVal `BS.cons` "()<>@,;:\\\"/[]?={}"
135+
136+-- | A version of 'liftM2' that is strict in the result of its first
137+-- action.
138+liftM2' :: (Monad m) => (a -> b -> c) -> m a -> m b -> m c
139+liftM2' f a b = do
140+ !x <- a
141+ y <- b
142+ return (f x y)
143+{-# INLINE liftM2' #-}
144+
145+-- | See http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.1
146+starRule :: Maybe Int -> Maybe Int -> Parser a -> Parser [a]
147+starRule minNum maxNum element = do
148+ let numMandatory = fromMaybe 0 minNum
149+ mandatoryVals <- replicateM numMandatory element
150+ optionalVals <- case maxNum of
151+ Nothing -> many' element
152+ Just n -> do
153+ let countUpTo 0 _ = return []
154+ countUpTo m a = liftM2' (:) a (countUpTo (pred m) a) <|> return []
155+ countUpTo (n - numMandatory) element
156+ return $ mandatoryVals ++ optionalVals
157+
158+-- | See http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.1
159+-- The spec is a bit ambiguous on whether extra commas and whitespace are permitted before and after the elements; this implementation permits them
160+hashRule :: forall a. Maybe Int -> Maybe Int -> Parser a -> Parser [a]
161+hashRule minNum maxNum element = do
162+ let numMandatory = fromMaybe 0 minNum
163+ sep :: Parser ()
164+ sep = do
165+ skipMany1 $ do
166+ skipMany lws
167+ char ','
168+ return ()
169+ processMandatory :: Bool -> Int -> Parser [a]
170+ processMandatory isInitial 0 = processOptional isInitial $ fmap (subtract numMandatory) maxNum
171+ processMandatory isInitial n = do
172+ if isInitial then void $ optional sep else sep
173+ liftM2' (:) (skipMany lws >> element) $ processMandatory False $ pred n
174+ processOptional :: Bool -> Maybe Int -> Parser [a]
175+ processOptional _ (Just 0) = return []
176+ processOptional isInitial n = (<|> return []) $ do
177+ if isInitial then void $ optional sep else sep
178+ liftM2' (:) (skipMany lws >> element) $ processOptional False $ fmap pred n
179+ result <- processMandatory True numMandatory
180+ _ <- optional sep
181+ skipMany lws
182+ return result
183+
184+-- | Linear whitespace
185+-- See http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
186+lws :: Parser ()
187+lws = do
188+ option () $ cr >> lf
189+ starRule (Just 1) Nothing $ sp <|> ht
190+ return ()
191+
192+-- | See http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
193+cr :: Parser ()
194+cr = void $ word8 13
195+
196+-- | See http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
197+lf :: Parser ()
198+lf = void $ word8 10
199+
200+-- | See http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
201+sp :: Parser ()
202+sp = void $ word8 spVal
203+
204+spVal :: Word8
205+spVal = 32
206+
207+-- | See http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
208+ht :: Parser ()
209+ht = void $ word8 htVal
210+
211+htVal :: Word8
212+htVal = 9
obelisk-asset/src/Obelisk/Asset/Serve.hsnew file+105
@@ -0,0 +1,105 @@
1+{-# LANGUAGE CPP, OverloadedStrings, TemplateHaskell #-}
2+module Obelisk.Asset.Serve where
3+
4+import Obelisk.Asset.Accept
5+
6+import Snap
7+import Snap.Util.FileServe
8+
9+import Control.Applicative
10+import Control.Exception (try, throwIO, catchJust)
11+import Control.Monad
12+import Control.Monad.IO.Class
13+import Data.Attoparsec.ByteString (parseOnly, endOfInput)
14+import Data.ByteString (ByteString)
15+import qualified Data.ByteString as BS
16+import Data.List
17+import Data.Monoid
18+import qualified Data.Text as T
19+import Data.Text.Encoding
20+import System.Directory
21+import System.FilePath
22+import System.IO.Error
23+import System.Posix (getFileStatus, fileSize)
24+
25+cachePermanently :: MonadSnap m => m ()
26+cachePermanently = do
27+ modifyResponse $ setHeader "Cache-Control" "public, max-age=315360000"
28+ modifyResponse $ setHeader "Expires" "Tue, 01 Feb 2050 00:00:00 GMT" --TODO: This should be set to "approximately one year from the time the response is sent"
29+
30+doNotCache :: MonadSnap m => m ()
31+doNotCache = do
32+ modifyResponse $ setHeader "Cache-Control" "no-cache, no-store, must-revalidate"
33+ modifyResponse $ setHeader "Expires" "0"
34+
35+serveAssets :: MonadSnap m => FilePath -> FilePath -> m ()
36+serveAssets = serveAssets' True
37+
38+serveAssetsInPlace :: MonadSnap m => FilePath -> FilePath -> m ()
39+serveAssetsInPlace = serveAssets' False
40+
41+serveAssets' :: MonadSnap m => Bool -> FilePath -> FilePath -> m ()
42+serveAssets' doRedirect base fallback = do
43+ pRaw <- getSafePath
44+ let go p = do
45+ assetType <- liftIO $ try $ BS.readFile $ base </> p </> "type"
46+ case assetType of
47+ Right "immutable" -> do
48+ conditionalOnModification <- getsRequest $ getHeader "If-Modified-Since"
49+ case conditionalOnModification of
50+ Nothing -> do
51+ encodedFiles <- liftM (filter (`notElem` [".", ".."])) $ liftIO $ getDirectoryContents $ base </> p </> "encodings"
52+ availableEncodings <- liftM (map snd . sort) $ forM encodedFiles $ \f -> do
53+ stat <- liftIO $ getFileStatus $ base </> p </> "encodings" </> f
54+ return (fileSize stat, Encoding $ encodeUtf8 $ T.pack f)
55+ acceptEncodingRaw <- getsRequest $ getHeader "Accept-Encoding"
56+ ae <- case acceptEncodingRaw of
57+ Nothing -> return missingAcceptableEncodings
58+ Just aer -> case parseOnly (acceptEncodingBody <* endOfInput) aer of
59+ Right ae -> return ae
60+ Left err -> error err
61+ Just (Encoding e) <- return $ chooseEncoding availableEncodings ae
62+ modifyResponse $ setHeader "Content-Encoding" e . setHeader "Vary" "Accept-Encoding"
63+ if doRedirect then cachePermanently else doNotCache --TODO: Use Etags when not redirecting
64+ let finalFilename = base </> p </> "encodings" </> T.unpack (decodeUtf8 e)
65+ stat <- liftIO $ getFileStatus finalFilename
66+ modifyResponse $ setHeader "Last-Modified" "Thu, 1 Jan 1970 00:00:00 GMT"
67+ modifyResponse $ setResponseCode 200 . setContentLength (fromIntegral $ fileSize stat) . setContentType (fileType defaultMimeTypes p)
68+ sendFile finalFilename
69+ Just _ -> cachePermanently >> modifyResponse (setResponseCode 304)
70+ Right "redirect" -> do
71+ mtarget <- liftIO $ getAssetTarget $ base </> p
72+ case mtarget of
73+ Just target -> if doRedirect
74+ then do
75+ doNotCache
76+ redirect target
77+ else go $ takeDirectory p </> T.unpack (decodeUtf8 target)
78+ Nothing -> serveFile $ fallback </> p
79+ Right unknown -> error $ T.unpack ("serveAssets': Unknown asset " <> decodeUtf8 unknown)
80+ Left err | isDoesNotExistError err -> (doNotCache >> serveFileIfExists (fallback </> p)) <|> do
81+ let (dirname, filename) = splitFileName p
82+ unhashedFilename = drop 1 $ dropWhile (/= '-') filename
83+ if null unhashedFilename then pass else do
84+ doNotCache
85+ serveFileIfExists $ fallback </> dirname </> unhashedFilename
86+ | otherwise -> liftIO $ throwIO err
87+ go $ if "/" `isSuffixOf` pRaw || pRaw == "" then pRaw <> "index.html" else pRaw
88+
89+serveFileIfExists :: MonadSnap m => FilePath -> m ()
90+serveFileIfExists f = do
91+ exists <- liftIO $ doesFileExist f
92+ if exists then serveFile f else pass
93+
94+serveFileIfExistsAs :: MonadSnap m => ByteString -> FilePath -> m ()
95+serveFileIfExistsAs mimeType f = do
96+ exists <- liftIO $ doesFileExist f
97+ if exists then serveFileAs mimeType f else pass
98+
99+getAssetTarget :: FilePath -> IO (Maybe ByteString)
100+getAssetTarget p = catchJust (\e -> if isDoesNotExistErrorType (ioeGetErrorType e) then Just () else Nothing) (liftM Just $ BS.readFile $ p </> "target") (\_ -> return Nothing)
101+
102+getAssetPath :: FilePath -> FilePath -> IO (Maybe FilePath)
103+getAssetPath base p = do
104+ target <- getAssetTarget $ base </> p
105+ return $ fmap ((takeDirectory p </>) . T.unpack . decodeUtf8) target