From 2d962afede980e58997c3ea82dcd24d8899f5b84 Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Sat, 25 Dec 2010 21:26:17 +0200 Subject: [PATCH 01/23] first commit --- README | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 README diff --git a/README b/README new file mode 100644 index 00000000..e69de29b From bc0bfac57b249baa2fd44c9e965d1c106905d4f4 Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Sat, 25 Dec 2010 21:27:03 +0200 Subject: [PATCH 02/23] Initial code import --- LICENSE | 25 ++++ Yesod/Helpers/Static.hs | 257 ++++++++++++++++++++++++++++++++++++++++ runtests.hs | 6 + test/.ignored | 0 test/bar/baz | 0 test/foo | 0 test/tmp/ignored | 0 yesod-static.cabal | 48 ++++++++ 8 files changed, 336 insertions(+) create mode 100644 LICENSE create mode 100644 Yesod/Helpers/Static.hs create mode 100644 runtests.hs create mode 100644 test/.ignored create mode 100644 test/bar/baz create mode 100644 test/foo create mode 100644 test/tmp/ignored create mode 100644 yesod-static.cabal diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..8643e5d8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +The following license covers this documentation, and the source code, except +where otherwise indicated. + +Copyright 2010, Michael Snoyman. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Yesod/Helpers/Static.hs b/Yesod/Helpers/Static.hs new file mode 100644 index 00000000..dccb2a39 --- /dev/null +++ b/Yesod/Helpers/Static.hs @@ -0,0 +1,257 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE MultiParamTypeClasses #-} +--------------------------------------------------------- +-- +-- Module : Yesod.Helpers.Static +-- Copyright : Michael Snoyman +-- License : BSD3 +-- +-- Maintainer : Michael Snoyman +-- Stability : Unstable +-- Portability : portable +-- + +-- | Serve static files from a Yesod app. +-- +-- This is most useful for standalone testing. When running on a production +-- server (like Apache), just let the server do the static serving. +-- +-- In fact, in an ideal setup you'll serve your static files from a separate +-- domain name to save time on transmitting cookies. In that case, you may wish +-- to use 'urlRenderOverride' to redirect requests to this subsite to a +-- separate domain name. +module Yesod.Helpers.Static + ( -- * Subsite + Static (..) + , StaticRoute (..) + -- * Lookup files in filesystem + , fileLookupDir + , staticFiles + -- * Embed files + , mkEmbedFiles + , getStaticHandler + -- * Hashing + , base64md5 +#if TEST + , testSuite +#endif + ) where + +import System.Directory +import Control.Monad +import Data.Maybe (fromMaybe) + +import Yesod.Handler +import Yesod.Content +import Yesod.Core + +import Control.Monad.IO.Class (liftIO) + +import Data.List (intercalate) +import Language.Haskell.TH +import Language.Haskell.TH.Syntax +import Web.Routes + +import qualified Data.ByteString.Lazy as L +import Data.Digest.Pure.MD5 +import qualified Data.ByteString.Base64 +import qualified Data.ByteString.Char8 as S8 +import qualified Data.Serialize + +#if TEST +import Test.Framework (testGroup, Test) +import Test.Framework.Providers.HUnit +import Test.HUnit hiding (Test) +#endif + +-- | A function for looking up file contents. For serving from the file system, +-- see 'fileLookupDir'. +data Static = Static + { staticLookup :: FilePath -> IO (Maybe (Either FilePath Content)) + -- | Mapping from file extension to content type. See 'typeByExt'. + , staticTypes :: [(String, ContentType)] + } + +-- | Manually construct a static route. +-- The first argument is a sub-path to the file being served whereas the second argument is the key value pairs in the query string. +-- For example, +-- > StaticRoute $ StaticR ["thumb001.jpg"] [("foo", "5"), ("bar", "choc")] +-- would generate a url such as 'http://site.com/static/thumb001.jpg?foo=5&bar=choc' +-- The StaticRoute constructor can be used when url's cannot be statically generated at compile-time. +-- E.g. When generating image galleries. +data StaticRoute = StaticRoute [String] [(String, String)] + deriving (Eq, Show, Read) + +type instance Route Static = StaticRoute + +instance YesodSubSite Static master where + getSubSite = Site + { handleSite = \_ (StaticRoute ps _) m -> + case m of + "GET" -> Just $ fmap chooseRep $ getStaticRoute ps + _ -> Nothing + , formatPathSegments = \(StaticRoute x y) -> (x, y) + , parsePathSegments = \x -> Right $ StaticRoute x [] + } + +-- | Lookup files in a specific directory. +-- +-- If you are just using this in combination with the static subsite (you +-- probably are), the handler itself checks that no unsafe paths are being +-- requested. In particular, no path segments may begin with a single period, +-- so hidden files and parent directories are safe. +-- +-- For the second argument to this function, you can just use 'typeByExt'. +fileLookupDir :: FilePath -> [(String, ContentType)] -> Static +fileLookupDir dir = Static $ \fp -> do + let fp' = dir ++ '/' : fp + exists <- doesFileExist fp' + if exists + then return $ Just $ Left fp' + else return Nothing + +-- | Lookup files in a specific directory, and embed them into the haskell source. +-- +-- A variation of fileLookupDir which allows subsites distributed via cabal to include +-- static content. You can still use staticFiles to generate route identifiers. See getStaticHandler +-- for dispatching static content for a subsite. +mkEmbedFiles :: FilePath -> Q Exp +mkEmbedFiles d = do + fs <- qRunIO $ getFileList d + clauses <- mapM (mkClause . intercalate "/") fs + defC <- defaultClause + return $ static $ clauses ++ [defC] + where static clauses = LetE [fun clauses] $ ConE 'Static `AppE` VarE f + f = mkName "f" + fun clauses = FunD f clauses + defaultClause = do + b <- [| return Nothing |] + return $ Clause [WildP] (NormalB b) [] + + mkClause p = do + content <- qRunIO $ readFile $ d ++ '/':p + let pat = LitP $ StringL p + foldAppE = foldl1 AppE + content' = return $ LitE $ StringL $ content + body <- normalB [| return $ Just $ Right $ toContent ($content' :: [Char]) |] + return $ Clause [pat] body [] + +-- | Dispatch static route for a subsite +-- +-- Subsites with static routes can't (yet) define Static routes the same way "master" sites can. +-- Instead of a subsite route: +-- /static StaticR Static getStatic +-- Use a normal route: +-- /static/*Strings StaticR GET +-- +-- Then, define getStaticR something like: +-- getStaticR = getStaticHandler ($(mkEmbedFiles "static") typeByExt) StaticR +-- */ end CPP comment +getStaticHandler :: Static -> (StaticRoute -> Route sub) -> [String] -> GHandler sub y ChooseRep +getStaticHandler static toSubR pieces = do + toMasterR <- getRouteToMaster + toMasterHandler (toMasterR . toSubR) toSub route handler + where route = StaticRoute pieces [] + toSub _ = static + staticSite = getSubSite :: Site (Route Static) (String -> Maybe (GHandler Static y ChooseRep)) + handler = fromMaybe notFound $ handleSite staticSite undefined route "GET" + +getStaticRoute :: [String] + -> GHandler Static master (ContentType, Content) +getStaticRoute fp' = do + Static fl ctypes <- getYesodSub + when (any isUnsafe fp') notFound + let fp = intercalate "/" fp' + content <- liftIO $ fl fp + case content of + Nothing -> notFound + Just (Left fp'') -> do + let ctype = fromMaybe typeOctet $ lookup (ext fp'') ctypes + sendFile ctype fp'' + Just (Right bs) -> do + let ctype = fromMaybe typeOctet $ lookup (ext fp) ctypes + return (ctype, bs) + where + isUnsafe [] = True + isUnsafe ('.':_) = True + isUnsafe _ = False + +notHidden :: FilePath -> Bool +notHidden ('.':_) = False +notHidden "tmp" = False +notHidden _ = True + +getFileList :: FilePath -> IO [[String]] +getFileList = flip go id + where + go :: String -> ([String] -> [String]) -> IO [[String]] + go fp front = do + allContents <- filter notHidden `fmap` getDirectoryContents fp + let fullPath :: String -> String + fullPath f = fp ++ '/' : f + files <- filterM (doesFileExist . fullPath) allContents + let files' = map (front . return) files + dirs <- filterM (doesDirectoryExist . fullPath) allContents + dirs' <- mapM (\f -> go (fullPath f) (front . (:) f)) dirs + return $ concat $ files' : dirs' + +-- | This piece of Template Haskell will find all of the files in the given directory and create Haskell identifiers for them. For example, if you have the files \"static\/style.css\" and \"static\/js\/script.js\", it will essentailly create: +-- +-- > style_css = StaticRoute ["style.css"] [] +-- > js_script_js = StaticRoute ["js/script.js"] [] +staticFiles :: FilePath -> Q [Dec] +staticFiles fp = do + fs <- qRunIO $ getFileList fp + concat `fmap` mapM go fs + where + replace' c + | 'A' <= c && c <= 'Z' = c + | 'a' <= c && c <= 'z' = c + | '0' <= c && c <= '9' = c + | otherwise = '_' + go f = do + let name = mkName $ intercalate "_" $ map (map replace') f + f' <- lift f + let sr = ConE $ mkName "StaticRoute" + hash <- qRunIO $ fmap base64md5 $ L.readFile $ fp ++ '/' : intercalate "/" f + let qs = ListE [TupE [LitE $ StringL hash, ListE []]] + return + [ SigD name $ ConT ''Route `AppT` ConT ''Static + , FunD name + [ Clause [] (NormalB $ sr `AppE` f' `AppE` qs) [] + ] + ] + +#if TEST + +testSuite :: Test +testSuite = testGroup "Yesod.Helpers.Static" + [ testCase "get file list" caseGetFileList + ] + +caseGetFileList :: Assertion +caseGetFileList = do + x <- getFileList "test" + x @?= [["foo"], ["bar", "baz"]] + +#endif + +-- | md5-hashes the given lazy bytestring and returns the hash as +-- base64url-encoded string. +-- +-- This function returns the first 8 characters of the hash. +base64md5 :: L.ByteString -> String +base64md5 = map go + . take 8 + . S8.unpack + . Data.ByteString.Base64.encode + . Data.Serialize.encode + . md5 + where + go '+' = '-' + go '/' = '_' + go c = c diff --git a/runtests.hs b/runtests.hs new file mode 100644 index 00000000..ccd8b724 --- /dev/null +++ b/runtests.hs @@ -0,0 +1,6 @@ +import Test.Framework (defaultMain) + +import Yesod.Helpers.Static + +main :: IO () +main = defaultMain [testSuite] diff --git a/test/.ignored b/test/.ignored new file mode 100644 index 00000000..e69de29b diff --git a/test/bar/baz b/test/bar/baz new file mode 100644 index 00000000..e69de29b diff --git a/test/foo b/test/foo new file mode 100644 index 00000000..e69de29b diff --git a/test/tmp/ignored b/test/tmp/ignored new file mode 100644 index 00000000..e69de29b diff --git a/yesod-static.cabal b/yesod-static.cabal new file mode 100644 index 00000000..05438c12 --- /dev/null +++ b/yesod-static.cabal @@ -0,0 +1,48 @@ +name: yesod-static +version: 0.7.0 +license: BSD3 +license-file: LICENSE +author: Michael Snoyman +maintainer: Michael Snoyman +synopsis: Static file serving subsite for Yesod Web Framework. +category: Web, Yesod +stability: Stable +cabal-version: >= 1.6 +build-type: Simple +homepage: http://docs.yesodweb.com/ + +flag test + description: Build the executable to run unit tests + default: False + +library + build-depends: base >= 4 && < 5 + , yesod-core >= 0.7 && < 0.8 + , base64-bytestring >= 0.1.0.1 && < 0.2 + , pureMD5 >= 2.1.0.3 && < 2.2 + , cereal >= 0.3 && < 0.4 + , bytestring >= 0.9 && < 0.10 + , web-routes >= 0.23 && < 0.24 + , template-haskell + , directory >= 1.0 && < 1.2 + , transformers >= 0.2 && < 0.3 + exposed-modules: Yesod.Helpers.Static + ghc-options: -Wall + +executable runtests + if flag(test) + Buildable: True + cpp-options: -DTEST + build-depends: test-framework, + test-framework-quickcheck2, + test-framework-hunit, + HUnit, + QuickCheck >= 2 && < 3 + else + Buildable: False + ghc-options: -Wall + main-is: runtests.hs + +source-repository head + type: git + location: git://github.com/snoyberg/yesod-static.git From 3b2a3881e6248bd1e54cb5f1bf398c874761cd5d Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Sun, 9 Jan 2011 07:35:57 +0200 Subject: [PATCH 03/23] Change version number --- yesod-static.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yesod-static.cabal b/yesod-static.cabal index 05438c12..0743f29d 100644 --- a/yesod-static.cabal +++ b/yesod-static.cabal @@ -1,5 +1,5 @@ name: yesod-static -version: 0.7.0 +version: 0.0.0 license: BSD3 license-file: LICENSE author: Michael Snoyman From 2db05e205565721b251656c16e6f8bb38a7466c9 Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Thu, 20 Jan 2011 23:55:05 +0200 Subject: [PATCH 04/23] Using wai-app-static (not sure if it will stick yet), does not fully work --- Yesod/Helpers/Static.hs | 85 +++++++++-------------------------------- yesod-static.cabal | 1 + 2 files changed, 18 insertions(+), 68 deletions(-) diff --git a/Yesod/Helpers/Static.hs b/Yesod/Helpers/Static.hs index dccb2a39..1d2fc449 100644 --- a/Yesod/Helpers/Static.hs +++ b/Yesod/Helpers/Static.hs @@ -29,10 +29,8 @@ module Yesod.Helpers.Static Static (..) , StaticRoute (..) -- * Lookup files in filesystem - , fileLookupDir , staticFiles -- * Embed files - , mkEmbedFiles , getStaticHandler -- * Hashing , base64md5 @@ -48,8 +46,10 @@ import Data.Maybe (fromMaybe) import Yesod.Handler import Yesod.Content import Yesod.Core +import Yesod.Request import Control.Monad.IO.Class (liftIO) +import qualified Control.Monad.Trans.Class as Trans import Data.List (intercalate) import Language.Haskell.TH @@ -62,6 +62,9 @@ import qualified Data.ByteString.Base64 import qualified Data.ByteString.Char8 as S8 import qualified Data.Serialize +import Network.Wai.Application.Static + (defaultMimeTypeByExt, StaticSettings (..), staticApp, defaultListing) + #if TEST import Test.Framework (testGroup, Test) import Test.Framework.Providers.HUnit @@ -71,9 +74,8 @@ import Test.HUnit hiding (Test) -- | A function for looking up file contents. For serving from the file system, -- see 'fileLookupDir'. data Static = Static - { staticLookup :: FilePath -> IO (Maybe (Either FilePath Content)) - -- | Mapping from file extension to content type. See 'typeByExt'. - , staticTypes :: [(String, ContentType)] + { staticPrefix :: FilePath + -- FIXME why not just put in a StaticSettings here? } -- | Manually construct a static route. @@ -92,54 +94,21 @@ instance YesodSubSite Static master where getSubSite = Site { handleSite = \_ (StaticRoute ps _) m -> case m of - "GET" -> Just $ fmap chooseRep $ getStaticRoute ps + "GET" -> Just $ do + Static prefix <- getYesodSub + req <- waiRequest + res <- Trans.lift $ staticApp StaticSettings + { ssFolder = prefix + , ssIndices = [] + , ssListing = Just defaultListing + , ssGetMimeType = return . defaultMimeTypeByExt + } req + sendWaiResponse res _ -> Nothing , formatPathSegments = \(StaticRoute x y) -> (x, y) , parsePathSegments = \x -> Right $ StaticRoute x [] } --- | Lookup files in a specific directory. --- --- If you are just using this in combination with the static subsite (you --- probably are), the handler itself checks that no unsafe paths are being --- requested. In particular, no path segments may begin with a single period, --- so hidden files and parent directories are safe. --- --- For the second argument to this function, you can just use 'typeByExt'. -fileLookupDir :: FilePath -> [(String, ContentType)] -> Static -fileLookupDir dir = Static $ \fp -> do - let fp' = dir ++ '/' : fp - exists <- doesFileExist fp' - if exists - then return $ Just $ Left fp' - else return Nothing - --- | Lookup files in a specific directory, and embed them into the haskell source. --- --- A variation of fileLookupDir which allows subsites distributed via cabal to include --- static content. You can still use staticFiles to generate route identifiers. See getStaticHandler --- for dispatching static content for a subsite. -mkEmbedFiles :: FilePath -> Q Exp -mkEmbedFiles d = do - fs <- qRunIO $ getFileList d - clauses <- mapM (mkClause . intercalate "/") fs - defC <- defaultClause - return $ static $ clauses ++ [defC] - where static clauses = LetE [fun clauses] $ ConE 'Static `AppE` VarE f - f = mkName "f" - fun clauses = FunD f clauses - defaultClause = do - b <- [| return Nothing |] - return $ Clause [WildP] (NormalB b) [] - - mkClause p = do - content <- qRunIO $ readFile $ d ++ '/':p - let pat = LitP $ StringL p - foldAppE = foldl1 AppE - content' = return $ LitE $ StringL $ content - body <- normalB [| return $ Just $ Right $ toContent ($content' :: [Char]) |] - return $ Clause [pat] body [] - -- | Dispatch static route for a subsite -- -- Subsites with static routes can't (yet) define Static routes the same way "master" sites can. @@ -160,26 +129,6 @@ getStaticHandler static toSubR pieces = do staticSite = getSubSite :: Site (Route Static) (String -> Maybe (GHandler Static y ChooseRep)) handler = fromMaybe notFound $ handleSite staticSite undefined route "GET" -getStaticRoute :: [String] - -> GHandler Static master (ContentType, Content) -getStaticRoute fp' = do - Static fl ctypes <- getYesodSub - when (any isUnsafe fp') notFound - let fp = intercalate "/" fp' - content <- liftIO $ fl fp - case content of - Nothing -> notFound - Just (Left fp'') -> do - let ctype = fromMaybe typeOctet $ lookup (ext fp'') ctypes - sendFile ctype fp'' - Just (Right bs) -> do - let ctype = fromMaybe typeOctet $ lookup (ext fp) ctypes - return (ctype, bs) - where - isUnsafe [] = True - isUnsafe ('.':_) = True - isUnsafe _ = False - notHidden :: FilePath -> Bool notHidden ('.':_) = False notHidden "tmp" = False diff --git a/yesod-static.cabal b/yesod-static.cabal index 0743f29d..70b3db8f 100644 --- a/yesod-static.cabal +++ b/yesod-static.cabal @@ -26,6 +26,7 @@ library , template-haskell , directory >= 1.0 && < 1.2 , transformers >= 0.2 && < 0.3 + , wai-app-static >= 0.0 && < 0.1 exposed-modules: Yesod.Helpers.Static ghc-options: -Wall From bb0f91e2bae31e07ef70c18682b849e6fa848896 Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Sun, 30 Jan 2011 19:49:18 +0200 Subject: [PATCH 05/23] Works with newest yesod-core --- Yesod/Helpers/Static.hs | 58 ++++++++++++++++++----------------------- yesod-static.cabal | 2 +- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/Yesod/Helpers/Static.hs b/Yesod/Helpers/Static.hs index 1d2fc449..ead0caf5 100644 --- a/Yesod/Helpers/Static.hs +++ b/Yesod/Helpers/Static.hs @@ -28,10 +28,14 @@ module Yesod.Helpers.Static ( -- * Subsite Static (..) , StaticRoute (..) - -- * Lookup files in filesystem + -- * Smart constructor + , static + -- * Template Haskell helpers , staticFiles + {- -- * Embed files , getStaticHandler + -} -- * Hashing , base64md5 #if TEST @@ -41,20 +45,13 @@ module Yesod.Helpers.Static import System.Directory import Control.Monad -import Data.Maybe (fromMaybe) import Yesod.Handler -import Yesod.Content import Yesod.Core -import Yesod.Request - -import Control.Monad.IO.Class (liftIO) -import qualified Control.Monad.Trans.Class as Trans import Data.List (intercalate) import Language.Haskell.TH import Language.Haskell.TH.Syntax -import Web.Routes import qualified Data.ByteString.Lazy as L import Data.Digest.Pure.MD5 @@ -63,7 +60,9 @@ import qualified Data.ByteString.Char8 as S8 import qualified Data.Serialize import Network.Wai.Application.Static - (defaultMimeTypeByExt, StaticSettings (..), staticApp, defaultListing) + ( defaultMimeTypeByExt, StaticSettings (..), staticAppPieces + , defaultListing + ) #if TEST import Test.Framework (testGroup, Test) @@ -71,12 +70,15 @@ import Test.Framework.Providers.HUnit import Test.HUnit hiding (Test) #endif --- | A function for looking up file contents. For serving from the file system, --- see 'fileLookupDir'. -data Static = Static - { staticPrefix :: FilePath - -- FIXME why not just put in a StaticSettings here? - } +newtype Static = Static StaticSettings + +-- | Default value of 'Static' for a given file folder. +-- +-- Does not have index files, uses default directory listings and default mime +-- type list. +static :: FilePath -> Static +static fp = Static $ StaticSettings fp [] (Just defaultListing) + (return . defaultMimeTypeByExt) -- | Manually construct a static route. -- The first argument is a sub-path to the file being served whereas the second argument is the key value pairs in the query string. @@ -90,25 +92,14 @@ data StaticRoute = StaticRoute [String] [(String, String)] type instance Route Static = StaticRoute -instance YesodSubSite Static master where - getSubSite = Site - { handleSite = \_ (StaticRoute ps _) m -> - case m of - "GET" -> Just $ do - Static prefix <- getYesodSub - req <- waiRequest - res <- Trans.lift $ staticApp StaticSettings - { ssFolder = prefix - , ssIndices = [] - , ssListing = Just defaultListing - , ssGetMimeType = return . defaultMimeTypeByExt - } req - sendWaiResponse res - _ -> Nothing - , formatPathSegments = \(StaticRoute x y) -> (x, y) - , parsePathSegments = \x -> Right $ StaticRoute x [] - } +instance RenderRoute StaticRoute where + renderRoute (StaticRoute x y) = (x, y) +instance Yesod master => YesodDispatch Static master where + yesodDispatch (Static set) _ pieces _ _ = + Just $ staticAppPieces set pieces + +{- FIXME -- | Dispatch static route for a subsite -- -- Subsites with static routes can't (yet) define Static routes the same way "master" sites can. @@ -128,6 +119,7 @@ getStaticHandler static toSubR pieces = do toSub _ = static staticSite = getSubSite :: Site (Route Static) (String -> Maybe (GHandler Static y ChooseRep)) handler = fromMaybe notFound $ handleSite staticSite undefined route "GET" +-} notHidden :: FilePath -> Bool notHidden ('.':_) = False diff --git a/yesod-static.cabal b/yesod-static.cabal index 70b3db8f..373ad412 100644 --- a/yesod-static.cabal +++ b/yesod-static.cabal @@ -26,7 +26,7 @@ library , template-haskell , directory >= 1.0 && < 1.2 , transformers >= 0.2 && < 0.3 - , wai-app-static >= 0.0 && < 0.1 + , wai-app-static >= 0.0.1 && < 0.1 exposed-modules: Yesod.Helpers.Static ghc-options: -Wall From 360885de25cf64cc2958040af1304c050497ad4c Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Sat, 5 Feb 2011 20:25:35 +0200 Subject: [PATCH 06/23] Added Setup.lhs --- Setup.lhs | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100755 Setup.lhs diff --git a/Setup.lhs b/Setup.lhs new file mode 100755 index 00000000..06e2708f --- /dev/null +++ b/Setup.lhs @@ -0,0 +1,7 @@ +#!/usr/bin/env runhaskell + +> module Main where +> import Distribution.Simple + +> main :: IO () +> main = defaultMain From a7f95db2a7a2ccb2a503a3dbf52bfc35e896e1b4 Mon Sep 17 00:00:00 2001 From: Ian Duncan Date: Wed, 16 Feb 2011 21:16:47 -0600 Subject: [PATCH 07/23] Fixed staticFiles to prepend an underscore to the generated function name if the filename starts with a digit and to change the first character of a filename's function to lowercase if it is uppercase. --- Yesod/Helpers/Static.hs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Yesod/Helpers/Static.hs b/Yesod/Helpers/Static.hs index ead0caf5..2bf08a2d 100644 --- a/Yesod/Helpers/Static.hs +++ b/Yesod/Helpers/Static.hs @@ -53,6 +53,7 @@ import Data.List (intercalate) import Language.Haskell.TH import Language.Haskell.TH.Syntax +import Data.Char import qualified Data.ByteString.Lazy as L import Data.Digest.Pure.MD5 import qualified Data.ByteString.Base64 @@ -155,7 +156,11 @@ staticFiles fp = do | '0' <= c && c <= '9' = c | otherwise = '_' go f = do - let name = mkName $ intercalate "_" $ map (map replace') f + let adjust [] = "" + adjust str@(x:xs) | isDigit x = '_' : x : xs + | isUpper x = toLower x : xs + | otherwise = str + let name = mkName $ intercalate "_" $ map (adjust . map replace') f f' <- lift f let sr = ConE $ mkName "StaticRoute" hash <- qRunIO $ fmap base64md5 $ L.readFile $ fp ++ '/' : intercalate "/" f From 9dc524f297f9d411bf9fbaa53a1f05e876af4816 Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Thu, 17 Feb 2011 17:38:12 +0200 Subject: [PATCH 08/23] Version bump --- yesod-static.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yesod-static.cabal b/yesod-static.cabal index 373ad412..810426af 100644 --- a/yesod-static.cabal +++ b/yesod-static.cabal @@ -1,5 +1,5 @@ name: yesod-static -version: 0.0.0 +version: 0.0.0.1 license: BSD3 license-file: LICENSE author: Michael Snoyman From 3bff87656c9c6f9e9b3a0deb9d26036006d33508 Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Thu, 7 Apr 2011 22:09:53 +0300 Subject: [PATCH 09/23] yesod-core 0.8 --- Yesod/Helpers/Static.hs | 3 ++- yesod-static.cabal | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Yesod/Helpers/Static.hs b/Yesod/Helpers/Static.hs index 2bf08a2d..127b2535 100644 --- a/Yesod/Helpers/Static.hs +++ b/Yesod/Helpers/Static.hs @@ -59,6 +59,7 @@ import Data.Digest.Pure.MD5 import qualified Data.ByteString.Base64 import qualified Data.ByteString.Char8 as S8 import qualified Data.Serialize +import Data.Text (Text) import Network.Wai.Application.Static ( defaultMimeTypeByExt, StaticSettings (..), staticAppPieces @@ -88,7 +89,7 @@ static fp = Static $ StaticSettings fp [] (Just defaultListing) -- would generate a url such as 'http://site.com/static/thumb001.jpg?foo=5&bar=choc' -- The StaticRoute constructor can be used when url's cannot be statically generated at compile-time. -- E.g. When generating image galleries. -data StaticRoute = StaticRoute [String] [(String, String)] +data StaticRoute = StaticRoute [Text] [(Text, Text)] deriving (Eq, Show, Read) type instance Route Static = StaticRoute diff --git a/yesod-static.cabal b/yesod-static.cabal index 810426af..a6c996cb 100644 --- a/yesod-static.cabal +++ b/yesod-static.cabal @@ -1,5 +1,5 @@ name: yesod-static -version: 0.0.0.1 +version: 0.1.0 license: BSD3 license-file: LICENSE author: Michael Snoyman @@ -9,7 +9,7 @@ category: Web, Yesod stability: Stable cabal-version: >= 1.6 build-type: Simple -homepage: http://docs.yesodweb.com/ +homepage: http://www.yesodweb.com/ flag test description: Build the executable to run unit tests @@ -17,16 +17,16 @@ flag test library build-depends: base >= 4 && < 5 - , yesod-core >= 0.7 && < 0.8 + , yesod-core >= 0.8 && < 0.9 , base64-bytestring >= 0.1.0.1 && < 0.2 , pureMD5 >= 2.1.0.3 && < 2.2 , cereal >= 0.3 && < 0.4 , bytestring >= 0.9 && < 0.10 - , web-routes >= 0.23 && < 0.24 , template-haskell , directory >= 1.0 && < 1.2 , transformers >= 0.2 && < 0.3 - , wai-app-static >= 0.0.1 && < 0.1 + , wai-app-static >= 0.1 && < 0.2 + , text >= 0.5 && < 1.0 exposed-modules: Yesod.Helpers.Static ghc-options: -Wall From c1659239a9d706bfa36d903de8a59937e8a8e027 Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Sun, 10 Apr 2011 21:55:27 +0300 Subject: [PATCH 10/23] Fix staticFiles --- Yesod/Helpers/Static.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Yesod/Helpers/Static.hs b/Yesod/Helpers/Static.hs index 127b2535..e49faf3c 100644 --- a/Yesod/Helpers/Static.hs +++ b/Yesod/Helpers/Static.hs @@ -59,7 +59,7 @@ import Data.Digest.Pure.MD5 import qualified Data.ByteString.Base64 import qualified Data.ByteString.Char8 as S8 import qualified Data.Serialize -import Data.Text (Text) +import Data.Text (Text, pack) import Network.Wai.Application.Static ( defaultMimeTypeByExt, StaticSettings (..), staticAppPieces @@ -162,7 +162,7 @@ staticFiles fp = do | isUpper x = toLower x : xs | otherwise = str let name = mkName $ intercalate "_" $ map (adjust . map replace') f - f' <- lift f + f' <- [|pack $(lift f)|] let sr = ConE $ mkName "StaticRoute" hash <- qRunIO $ fmap base64md5 $ L.readFile $ fp ++ '/' : intercalate "/" f let qs = ListE [TupE [LitE $ StringL hash, ListE []]] From 97ab7ffa498bae85ca450351bcd933082c78900f Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Sun, 10 Apr 2011 21:59:29 +0300 Subject: [PATCH 11/23] Fix staticFiles properly --- Yesod/Helpers/Static.hs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Yesod/Helpers/Static.hs b/Yesod/Helpers/Static.hs index e49faf3c..37d24ee0 100644 --- a/Yesod/Helpers/Static.hs +++ b/Yesod/Helpers/Static.hs @@ -60,6 +60,7 @@ import qualified Data.ByteString.Base64 import qualified Data.ByteString.Char8 as S8 import qualified Data.Serialize import Data.Text (Text, pack) +import Data.Monoid (mempty) import Network.Wai.Application.Static ( defaultMimeTypeByExt, StaticSettings (..), staticAppPieces @@ -162,10 +163,11 @@ staticFiles fp = do | isUpper x = toLower x : xs | otherwise = str let name = mkName $ intercalate "_" $ map (adjust . map replace') f - f' <- [|pack $(lift f)|] + f' <- [|map pack $(lift f)|] let sr = ConE $ mkName "StaticRoute" hash <- qRunIO $ fmap base64md5 $ L.readFile $ fp ++ '/' : intercalate "/" f - let qs = ListE [TupE [LitE $ StringL hash, ListE []]] + pack' <- [|pack|] + qs <- [|[(pack $(lift hash), mempty)]|] return [ SigD name $ ConT ''Route `AppT` ConT ''Static , FunD name From e564147f2d13c78e2c2ed078436f31a7a8a1604c Mon Sep 17 00:00:00 2001 From: Greg Weber Date: Mon, 28 Feb 2011 08:38:09 -0800 Subject: [PATCH 12/23] 2 kinds of static files- different cache headers * Forever - for "static" assets with a hash paremeter * ETag - for "public" assets without a hash parametes --- Yesod/Helpers/Static.hs | 276 +++++++++++++++++++++++++++------------- yesod-static.cabal | 4 + 2 files changed, 195 insertions(+), 85 deletions(-) diff --git a/Yesod/Helpers/Static.hs b/Yesod/Helpers/Static.hs index 37d24ee0..22c28e27 100644 --- a/Yesod/Helpers/Static.hs +++ b/Yesod/Helpers/Static.hs @@ -27,11 +27,16 @@ module Yesod.Helpers.Static ( -- * Subsite Static (..) + , Public (..) , StaticRoute (..) + , PublicRoute (..) -- * Smart constructor , static + , publicProduction + , publicDevel -- * Template Haskell helpers , staticFiles + , publicFiles {- -- * Embed files , getStaticHandler @@ -44,6 +49,7 @@ module Yesod.Helpers.Static ) where import System.Directory +import qualified System.Time import Control.Monad import Yesod.Handler @@ -63,8 +69,11 @@ import Data.Text (Text, pack) import Data.Monoid (mempty) import Network.Wai.Application.Static - ( defaultMimeTypeByExt, StaticSettings (..), staticAppPieces - , defaultListing + ( StaticSettings (..), CacheSettings (..) + , defaultStaticSettings, defaultPublicSettings + , staticAppPieces + , pathFromPieces + , Pieces ) #if TEST @@ -73,15 +82,51 @@ import Test.Framework.Providers.HUnit import Test.HUnit hiding (Test) #endif +-- | generally static assets referenced in html files +-- assets get a checksum query parameter appended for perfect caching +-- * a far future expire date is set +-- * a given asset revision will only ever be downloaded once (if the browser maintains its cache) +-- if you don't want to see a checksum in the url- use Public newtype Static = Static StaticSettings +-- | same as Static, but there is no checksum query parameter appended +-- generally html files and the favicon, but could be any file where you don't want the checksum parameter +-- * the file checksum is used for an ETag. +-- * this form of caching is not as good as the static- the browser can avoid downloading the file, but it always need to send a request with the etag value to the server to see if its copy is up to date +newtype Public = Public StaticSettings -- | Default value of 'Static' for a given file folder. -- -- Does not have index files, uses default directory listings and default mime -- type list. -static :: FilePath -> Static -static fp = Static $ StaticSettings fp [] (Just defaultListing) - (return . defaultMimeTypeByExt) +static :: String -> FilePath -> IO Static +static root fp = do + hashes <- mkHashMap fp + return $ Static $ (defaultStaticSettings (Forever $ isStaticRequest hashes)) { + ssFolder = fp + , ssMkRedirect = \_ newPath -> S8.pack $ root ++ "/" ++ newPath + } + where + isStaticRequest hashes reqf reqh = case M.lookup reqf hashes of + Nothing -> False + Just h -> h == reqh + +-- | no directory listing +public :: String -> FilePath -> CacheSettings -> Public +public root fp cache = Public $ (defaultPublicSettings cache) { + ssFolder = fp + , ssMkRedirect = \_ newPath -> S8.pack $ root ++ "/" ++ newPath + } + +publicProduction :: String -> FilePath -> IO Public +publicProduction root fp = do + etags <- mkPublicProductionEtag fp + return $ public root fp etags + +publicDevel :: String -> FilePath -> IO Public +publicDevel root fp = do + etags <- mkPublicDevelEtag fp + return $ public root fp etags + -- | Manually construct a static route. -- The first argument is a sub-path to the file being served whereas the second argument is the key value pairs in the query string. @@ -92,16 +137,157 @@ static fp = Static $ StaticSettings fp [] (Just defaultListing) -- E.g. When generating image galleries. data StaticRoute = StaticRoute [Text] [(Text, Text)] deriving (Eq, Show, Read) +data PublicRoute = PublicRoute [String] [(String, String)] + deriving (Eq, Show, Read) type instance Route Static = StaticRoute +type instance Route Public = PublicRoute instance RenderRoute StaticRoute where renderRoute (StaticRoute x y) = (x, y) +instance RenderRoute PublicRoute where + renderRoute (PublicRoute x y) = (x, y) instance Yesod master => YesodDispatch Static master where yesodDispatch (Static set) _ pieces _ _ = Just $ staticAppPieces set pieces +instance Yesod master => YesodDispatch Public master where + yesodDispatch (Public set) _ pieces _ _ = + Just $ staticAppPieces set pieces + +notHidden :: FilePath -> Bool +notHidden ('.':_) = False +notHidden "tmp" = False +notHidden _ = True + +getFileListPieces :: FilePath -> IO [[String]] +getFileListPieces = flip go id + where + go :: String -> ([String] -> [String]) -> IO [[String]] + go fp front = do + allContents <- filter notHidden `fmap` getDirectoryContents fp + let fullPath :: String -> String + fullPath f = fp ++ '/' : f + files <- filterM (doesFileExist . fullPath) allContents + let files' = map (front . return) files + dirs <- filterM (doesDirectoryExist . fullPath) allContents + dirs' <- mapM (\f -> go (fullPath f) (front . (:) f)) dirs + return $ concat $ files' : dirs' + +-- | This piece of Template Haskell will find all of the files in the given directory and create Haskell identifiers for them. For example, if you have the files \"static\/style.css\" and \"static\/js\/script.js\", it will essentailly create: +-- +-- > style_css = StaticRoute ["style.css"] [] +-- > js_script_js = StaticRoute ["js/script.js"] [] +staticFiles :: FilePath -> Q [Dec] +staticFiles dir = mkStaticFiles dir StaticSite + +publicFiles :: FilePath -> Q [Dec] +publicFiles dir = mkStaticFiles dir PublicSite + +mkHashMap :: FilePath -> IO (M.Map FilePath S8.ByteString) +mkHashMap dir = do + fs <- getFileListPieces dir + hashAlist fs >>= return . M.fromList + where + hashAlist :: [Pieces] -> IO [(FilePath, S8.ByteString)] + hashAlist fs = mapM hashPair fs + where + hashPair :: Pieces -> IO (FilePath, S8.ByteString) + hashPair pieces = do let file = pathFromPieces dir pieces + h <- base64md5File file + return (file, S8.pack h) + +mkPublicDevelEtag :: FilePath -> IO CacheSettings +mkPublicDevelEtag dir = do + etags <- mkHashMap dir + mtimeVar <- newIORef (M.empty :: M.Map FilePath System.Time.ClockTime) + return $ ETag $ \f -> + case M.lookup f etags of + Nothing -> return Nothing + Just checksum -> do + newt <- getModificationTime f + mtimes <- readIORef mtimeVar + oldt <- case M.lookup f mtimes of + Nothing -> writeIORef mtimeVar (M.insert f newt mtimes) >> return newt + Just ot -> return ot + return $ if newt /= oldt then Nothing else Just checksum + + +mkPublicProductionEtag :: FilePath -> IO CacheSettings +mkPublicProductionEtag dir = do + etags <- mkHashMap dir + return $ ETag $ \f -> return . M.lookup f $ etags + +data StaticSite = StaticSite | PublicSite +mkStaticFiles :: FilePath -> StaticSite -> Q [Dec] +mkStaticFiles fp StaticSite = mkStaticFiles' fp "StaticRoute" True +mkStaticFiles fp PublicSite = mkStaticFiles' fp "PublicRoute" False + +mkStaticFiles' :: FilePath -> -- ^ static directory + String -> -- ^ route constructor "StaticRoute" + Bool -> -- ^ append checksum query parameter + Q [Dec] +mkStaticFiles' fp routeConName makeHash = do + fs <- qRunIO $ getFileListPieces fp + concat `fmap` mapM mkRoute fs + where + replace' c + | 'A' <= c && c <= 'Z' = c + | 'a' <= c && c <= 'z' = c + | '0' <= c && c <= '9' = c + | otherwise = '_' + mkRoute f = do + let name = mkName $ intercalate "_" $ map (map replace') f + f' <- [|map pack $(lift f)|] + let route = mkName routeConName + pack' <- [|pack|] + qs <- if makeHash + then do hash <- qRunIO $ base64md5File $ pathFromPieces fp f + [|[(pack $(lift hash), mempty)]|] + else return $ ListE [] + return + [ SigD name $ ConT route + , FunD name + [ Clause [] (NormalB $ (ConE route) `AppE` f' `AppE` qs) [] + ] + ] + +base64md5File :: FilePath -> IO String +base64md5File file = do + contents <- L.readFile file + return $ base64md5 contents + +#if TEST + +testSuite :: Test +testSuite = testGroup "Yesod.Helpers.Static" + [ testCase "get file list" caseGetFileList + ] + +caseGetFileList :: Assertion +caseGetFileList = do + x <- getFileListPieces "test" + x @?= [["foo"], ["bar", "baz"]] + +#endif + +-- | md5-hashes the given lazy bytestring and returns the hash as +-- base64url-encoded string. +-- +-- This function returns the first 8 characters of the hash. +base64md5 :: L.ByteString -> String +base64md5 = map tr + . take 8 + . S8.unpack + . Data.ByteString.Base64.encode + . Data.Serialize.encode + . md5 + where + tr '+' = '-' + tr '/' = '_' + tr c = c + {- FIXME -- | Dispatch static route for a subsite -- @@ -124,83 +310,3 @@ getStaticHandler static toSubR pieces = do handler = fromMaybe notFound $ handleSite staticSite undefined route "GET" -} -notHidden :: FilePath -> Bool -notHidden ('.':_) = False -notHidden "tmp" = False -notHidden _ = True - -getFileList :: FilePath -> IO [[String]] -getFileList = flip go id - where - go :: String -> ([String] -> [String]) -> IO [[String]] - go fp front = do - allContents <- filter notHidden `fmap` getDirectoryContents fp - let fullPath :: String -> String - fullPath f = fp ++ '/' : f - files <- filterM (doesFileExist . fullPath) allContents - let files' = map (front . return) files - dirs <- filterM (doesDirectoryExist . fullPath) allContents - dirs' <- mapM (\f -> go (fullPath f) (front . (:) f)) dirs - return $ concat $ files' : dirs' - --- | This piece of Template Haskell will find all of the files in the given directory and create Haskell identifiers for them. For example, if you have the files \"static\/style.css\" and \"static\/js\/script.js\", it will essentailly create: --- --- > style_css = StaticRoute ["style.css"] [] --- > js_script_js = StaticRoute ["js/script.js"] [] -staticFiles :: FilePath -> Q [Dec] -staticFiles fp = do - fs <- qRunIO $ getFileList fp - concat `fmap` mapM go fs - where - replace' c - | 'A' <= c && c <= 'Z' = c - | 'a' <= c && c <= 'z' = c - | '0' <= c && c <= '9' = c - | otherwise = '_' - go f = do - let adjust [] = "" - adjust str@(x:xs) | isDigit x = '_' : x : xs - | isUpper x = toLower x : xs - | otherwise = str - let name = mkName $ intercalate "_" $ map (adjust . map replace') f - f' <- [|map pack $(lift f)|] - let sr = ConE $ mkName "StaticRoute" - hash <- qRunIO $ fmap base64md5 $ L.readFile $ fp ++ '/' : intercalate "/" f - pack' <- [|pack|] - qs <- [|[(pack $(lift hash), mempty)]|] - return - [ SigD name $ ConT ''Route `AppT` ConT ''Static - , FunD name - [ Clause [] (NormalB $ sr `AppE` f' `AppE` qs) [] - ] - ] - -#if TEST - -testSuite :: Test -testSuite = testGroup "Yesod.Helpers.Static" - [ testCase "get file list" caseGetFileList - ] - -caseGetFileList :: Assertion -caseGetFileList = do - x <- getFileList "test" - x @?= [["foo"], ["bar", "baz"]] - -#endif - --- | md5-hashes the given lazy bytestring and returns the hash as --- base64url-encoded string. --- --- This function returns the first 8 characters of the hash. -base64md5 :: L.ByteString -> String -base64md5 = map go - . take 8 - . S8.unpack - . Data.ByteString.Base64.encode - . Data.Serialize.encode - . md5 - where - go '+' = '-' - go '/' = '_' - go c = c diff --git a/yesod-static.cabal b/yesod-static.cabal index a6c996cb..fd47681b 100644 --- a/yesod-static.cabal +++ b/yesod-static.cabal @@ -1,5 +1,9 @@ name: yesod-static +<<<<<<< HEAD version: 0.1.0 +======= +version: 0.1.0.0 +>>>>>>> update caching interface license: BSD3 license-file: LICENSE author: Michael Snoyman From 2872c8f597a436845d709cdc9e91c5855f11ce21 Mon Sep 17 00:00:00 2001 From: Greg Weber Date: Sun, 8 May 2011 07:22:56 -0700 Subject: [PATCH 13/23] upgrade to 0.8 --- Yesod/Helpers/Static.hs | 18 +++++++++--------- yesod-static.cabal | 8 +++----- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/Yesod/Helpers/Static.hs b/Yesod/Helpers/Static.hs index 22c28e27..d7141388 100644 --- a/Yesod/Helpers/Static.hs +++ b/Yesod/Helpers/Static.hs @@ -59,7 +59,6 @@ import Data.List (intercalate) import Language.Haskell.TH import Language.Haskell.TH.Syntax -import Data.Char import qualified Data.ByteString.Lazy as L import Data.Digest.Pure.MD5 import qualified Data.ByteString.Base64 @@ -67,13 +66,14 @@ import qualified Data.ByteString.Char8 as S8 import qualified Data.Serialize import Data.Text (Text, pack) import Data.Monoid (mempty) +import qualified Data.Map as M +import Data.IORef (readIORef, newIORef, writeIORef) import Network.Wai.Application.Static ( StaticSettings (..), CacheSettings (..) , defaultStaticSettings, defaultPublicSettings , staticAppPieces , pathFromPieces - , Pieces ) #if TEST @@ -103,7 +103,7 @@ static root fp = do hashes <- mkHashMap fp return $ Static $ (defaultStaticSettings (Forever $ isStaticRequest hashes)) { ssFolder = fp - , ssMkRedirect = \_ newPath -> S8.pack $ root ++ "/" ++ newPath + , ssMkRedirect = \_ newPath -> S8.append (S8.pack (root ++ "/")) newPath } where isStaticRequest hashes reqf reqh = case M.lookup reqf hashes of @@ -114,7 +114,7 @@ static root fp = do public :: String -> FilePath -> CacheSettings -> Public public root fp cache = Public $ (defaultPublicSettings cache) { ssFolder = fp - , ssMkRedirect = \_ newPath -> S8.pack $ root ++ "/" ++ newPath + , ssMkRedirect = \_ newPath -> S8.append (S8.pack (root ++ "/")) newPath } publicProduction :: String -> FilePath -> IO Public @@ -137,7 +137,7 @@ publicDevel root fp = do -- E.g. When generating image galleries. data StaticRoute = StaticRoute [Text] [(Text, Text)] deriving (Eq, Show, Read) -data PublicRoute = PublicRoute [String] [(String, String)] +data PublicRoute = PublicRoute [Text] [(Text, Text)] deriving (Eq, Show, Read) type instance Route Static = StaticRoute @@ -190,11 +190,11 @@ mkHashMap dir = do fs <- getFileListPieces dir hashAlist fs >>= return . M.fromList where - hashAlist :: [Pieces] -> IO [(FilePath, S8.ByteString)] + hashAlist :: [[String]] -> IO [(FilePath, S8.ByteString)] hashAlist fs = mapM hashPair fs where - hashPair :: Pieces -> IO (FilePath, S8.ByteString) - hashPair pieces = do let file = pathFromPieces dir pieces + hashPair :: [String] -> IO (FilePath, S8.ByteString) + hashPair pieces = do let file = pathFromPieces dir (map pack pieces) h <- base64md5File file return (file, S8.pack h) @@ -243,7 +243,7 @@ mkStaticFiles' fp routeConName makeHash = do let route = mkName routeConName pack' <- [|pack|] qs <- if makeHash - then do hash <- qRunIO $ base64md5File $ pathFromPieces fp f + then do hash <- qRunIO $ base64md5File $ pathFromPieces fp (map pack f) [|[(pack $(lift hash), mempty)]|] else return $ ListE [] return diff --git a/yesod-static.cabal b/yesod-static.cabal index fd47681b..99b4e97d 100644 --- a/yesod-static.cabal +++ b/yesod-static.cabal @@ -1,9 +1,5 @@ name: yesod-static -<<<<<<< HEAD version: 0.1.0 -======= -version: 0.1.0.0 ->>>>>>> update caching interface license: BSD3 license-file: LICENSE author: Michael Snoyman @@ -21,6 +17,8 @@ flag test library build-depends: base >= 4 && < 5 + , containers >= 0.4 + , old-time >= 1.0 , yesod-core >= 0.8 && < 0.9 , base64-bytestring >= 0.1.0.1 && < 0.2 , pureMD5 >= 2.1.0.3 && < 2.2 @@ -29,7 +27,7 @@ library , template-haskell , directory >= 1.0 && < 1.2 , transformers >= 0.2 && < 0.3 - , wai-app-static >= 0.1 && < 0.2 + , wai-app-static >= 0.3 && < 0.4 , text >= 0.5 && < 1.0 exposed-modules: Yesod.Helpers.Static ghc-options: -Wall From 691ff7ba4eada93f4eb271dade9bb7f365889cc6 Mon Sep 17 00:00:00 2001 From: Greg Weber Date: Sun, 6 Mar 2011 20:15:56 -0800 Subject: [PATCH 14/23] add .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..849ddff3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist/ From 6f215c7f5fa8e31663143e16319508dfe8c5db59 Mon Sep 17 00:00:00 2001 From: Greg Weber Date: Sun, 8 May 2011 07:41:41 -0700 Subject: [PATCH 15/23] use cabal-test and hspec --- Yesod/Helpers/Static.hs | 22 +--------------------- runtests.hs | 6 ------ {test => tests/data}/.ignored | 0 {test => tests/data}/bar/baz | 0 {test => tests/data}/foo | 0 {test => tests/data}/tmp/ignored | 0 tests/runtests.hs | 17 +++++++++++++++++ yesod-static.cabal | 22 ++++++++++------------ 8 files changed, 28 insertions(+), 39 deletions(-) delete mode 100644 runtests.hs rename {test => tests/data}/.ignored (100%) rename {test => tests/data}/bar/baz (100%) rename {test => tests/data}/foo (100%) rename {test => tests/data}/tmp/ignored (100%) create mode 100644 tests/runtests.hs diff --git a/Yesod/Helpers/Static.hs b/Yesod/Helpers/Static.hs index d7141388..36fa4645 100644 --- a/Yesod/Helpers/Static.hs +++ b/Yesod/Helpers/Static.hs @@ -44,7 +44,7 @@ module Yesod.Helpers.Static -- * Hashing , base64md5 #if TEST - , testSuite + , getFileListPieces #endif ) where @@ -76,12 +76,6 @@ import Network.Wai.Application.Static , pathFromPieces ) -#if TEST -import Test.Framework (testGroup, Test) -import Test.Framework.Providers.HUnit -import Test.HUnit hiding (Test) -#endif - -- | generally static assets referenced in html files -- assets get a checksum query parameter appended for perfect caching -- * a far future expire date is set @@ -258,20 +252,6 @@ base64md5File file = do contents <- L.readFile file return $ base64md5 contents -#if TEST - -testSuite :: Test -testSuite = testGroup "Yesod.Helpers.Static" - [ testCase "get file list" caseGetFileList - ] - -caseGetFileList :: Assertion -caseGetFileList = do - x <- getFileListPieces "test" - x @?= [["foo"], ["bar", "baz"]] - -#endif - -- | md5-hashes the given lazy bytestring and returns the hash as -- base64url-encoded string. -- diff --git a/runtests.hs b/runtests.hs deleted file mode 100644 index ccd8b724..00000000 --- a/runtests.hs +++ /dev/null @@ -1,6 +0,0 @@ -import Test.Framework (defaultMain) - -import Yesod.Helpers.Static - -main :: IO () -main = defaultMain [testSuite] diff --git a/test/.ignored b/tests/data/.ignored similarity index 100% rename from test/.ignored rename to tests/data/.ignored diff --git a/test/bar/baz b/tests/data/bar/baz similarity index 100% rename from test/bar/baz rename to tests/data/bar/baz diff --git a/test/foo b/tests/data/foo similarity index 100% rename from test/foo rename to tests/data/foo diff --git a/test/tmp/ignored b/tests/data/tmp/ignored similarity index 100% rename from test/tmp/ignored rename to tests/data/tmp/ignored diff --git a/tests/runtests.hs b/tests/runtests.hs new file mode 100644 index 00000000..3186a8e2 --- /dev/null +++ b/tests/runtests.hs @@ -0,0 +1,17 @@ +{-# LANGUAGE OverloadedStrings #-} +import Yesod.Helpers.Static + +import Test.Hspec +import Test.Hspec.HUnit () +-- import Test.Hspec.QuickCheck (prop) +import Test.HUnit ((@?=)) + +main :: IO () +main = hspecX specs + +specs :: IO [Spec] +specs = runSpecM $ do + context "get file list" $ do + ti "pieces" $ do + x <- getFileListPieces "tests/data" + x @?= [["foo"], ["bar", "baz"]] diff --git a/yesod-static.cabal b/yesod-static.cabal index 99b4e97d..e9fe1456 100644 --- a/yesod-static.cabal +++ b/yesod-static.cabal @@ -7,7 +7,7 @@ maintainer: Michael Snoyman synopsis: Static file serving subsite for Yesod Web Framework. category: Web, Yesod stability: Stable -cabal-version: >= 1.6 +cabal-version: >= 1.8 build-type: Simple homepage: http://www.yesodweb.com/ @@ -32,17 +32,15 @@ library exposed-modules: Yesod.Helpers.Static ghc-options: -Wall -executable runtests - if flag(test) - Buildable: True - cpp-options: -DTEST - build-depends: test-framework, - test-framework-quickcheck2, - test-framework-hunit, - HUnit, - QuickCheck >= 2 && < 3 - else - Buildable: False +test-suite runtests + hs-source-dirs: tests + main-is: runtests.hs + type: exitcode-stdio-1.0 + cpp-options: -DTEST + build-depends: yesod-static, + base >= 4 && < 5, + hspec, + HUnit ghc-options: -Wall main-is: runtests.hs From 0b6da81f40e0a222410358dd7cd4a91cf351da40 Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Mon, 9 May 2011 06:45:14 +0300 Subject: [PATCH 16/23] Versions --- yesod-static.cabal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yesod-static.cabal b/yesod-static.cabal index e9fe1456..a28b878e 100644 --- a/yesod-static.cabal +++ b/yesod-static.cabal @@ -1,5 +1,5 @@ name: yesod-static -version: 0.1.0 +version: 0.2.0 license: BSD3 license-file: LICENSE author: Michael Snoyman @@ -27,7 +27,7 @@ library , template-haskell , directory >= 1.0 && < 1.2 , transformers >= 0.2 && < 0.3 - , wai-app-static >= 0.3 && < 0.4 + , wai-app-static >= 0.2 && < 0.3 , text >= 0.5 && < 1.0 exposed-modules: Yesod.Helpers.Static ghc-options: -Wall From 1150c7e9b8b5d37f92168a31053d7324cad1f298 Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Fri, 13 May 2011 09:13:20 +0300 Subject: [PATCH 17/23] Fix docs --- Yesod/Helpers/Static.hs | 8 ++++---- yesod-static.cabal | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Yesod/Helpers/Static.hs b/Yesod/Helpers/Static.hs index 36fa4645..9cb3e98f 100644 --- a/Yesod/Helpers/Static.hs +++ b/Yesod/Helpers/Static.hs @@ -218,10 +218,10 @@ mkStaticFiles :: FilePath -> StaticSite -> Q [Dec] mkStaticFiles fp StaticSite = mkStaticFiles' fp "StaticRoute" True mkStaticFiles fp PublicSite = mkStaticFiles' fp "PublicRoute" False -mkStaticFiles' :: FilePath -> -- ^ static directory - String -> -- ^ route constructor "StaticRoute" - Bool -> -- ^ append checksum query parameter - Q [Dec] +mkStaticFiles' :: FilePath -- ^ static directory + -> String -- ^ route constructor "StaticRoute" + -> Bool -- ^ append checksum query parameter + -> Q [Dec] mkStaticFiles' fp routeConName makeHash = do fs <- qRunIO $ getFileListPieces fp concat `fmap` mapM mkRoute fs diff --git a/yesod-static.cabal b/yesod-static.cabal index a28b878e..a1886db0 100644 --- a/yesod-static.cabal +++ b/yesod-static.cabal @@ -1,5 +1,5 @@ name: yesod-static -version: 0.2.0 +version: 0.2.0.1 license: BSD3 license-file: LICENSE author: Michael Snoyman From c42deb74f788eac1ba6ba4c9342941606b7bad2e Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Fri, 27 May 2011 11:15:47 +0300 Subject: [PATCH 18/23] cabal file changes (does not compile) --- Yesod/{Helpers => }/Static.hs | 2 +- yesod-static.cabal | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename Yesod/{Helpers => }/Static.hs (99%) diff --git a/Yesod/Helpers/Static.hs b/Yesod/Static.hs similarity index 99% rename from Yesod/Helpers/Static.hs rename to Yesod/Static.hs index 9cb3e98f..126506f5 100644 --- a/Yesod/Helpers/Static.hs +++ b/Yesod/Static.hs @@ -24,7 +24,7 @@ -- domain name to save time on transmitting cookies. In that case, you may wish -- to use 'urlRenderOverride' to redirect requests to this subsite to a -- separate domain name. -module Yesod.Helpers.Static +module Yesod.Static ( -- * Subsite Static (..) , Public (..) diff --git a/yesod-static.cabal b/yesod-static.cabal index a1886db0..2f88f4b2 100644 --- a/yesod-static.cabal +++ b/yesod-static.cabal @@ -1,5 +1,5 @@ name: yesod-static -version: 0.2.0.1 +version: 0.3.0 license: BSD3 license-file: LICENSE author: Michael Snoyman @@ -27,9 +27,9 @@ library , template-haskell , directory >= 1.0 && < 1.2 , transformers >= 0.2 && < 0.3 - , wai-app-static >= 0.2 && < 0.3 + , wai-app-static >= 0.3 && < 0.4 , text >= 0.5 && < 1.0 - exposed-modules: Yesod.Helpers.Static + exposed-modules: Yesod.Static ghc-options: -Wall test-suite runtests From cf841fb258388466cadb02b19e10ec8a9ba16225 Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Fri, 27 May 2011 11:20:32 +0300 Subject: [PATCH 19/23] Incomplete strict hash code --- Yesod/Static.hs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Yesod/Static.hs b/Yesod/Static.hs index 126506f5..78744955 100644 --- a/Yesod/Static.hs +++ b/Yesod/Static.hs @@ -238,6 +238,7 @@ mkStaticFiles' fp routeConName makeHash = do pack' <- [|pack|] qs <- if makeHash then do hash <- qRunIO $ base64md5File $ pathFromPieces fp (map pack f) + -- FIXME hash <- qRunIO . calcHash $ fp ++ '/' : intercalate "/" f [|[(pack $(lift hash), mempty)]|] else return $ ListE [] return @@ -290,3 +291,10 @@ getStaticHandler static toSubR pieces = do handler = fromMaybe notFound $ handleSite staticSite undefined route "GET" -} + +calcHash :: FilePath -> IO String +calcHash fname = + withBinaryFile fname ReadMode hashHandle + where + hashHandle h = do s <- L.hGetContents h + return $! base64md5 s From 0ed92be3fa6621e4b5906b1dbd5c43a88148fa50 Mon Sep 17 00:00:00 2001 From: Greg Weber Date: Mon, 11 Jul 2011 22:30:55 -0700 Subject: [PATCH 20/23] compile against new wai-app-static Quickly get things to compile- I haven't reviewed anything --- Yesod/Static.hs | 67 ++++++++++++++----------------------------------- 1 file changed, 19 insertions(+), 48 deletions(-) diff --git a/Yesod/Static.hs b/Yesod/Static.hs index 78744955..a9d6d9af 100644 --- a/Yesod/Static.hs +++ b/Yesod/Static.hs @@ -27,16 +27,11 @@ module Yesod.Static ( -- * Subsite Static (..) - , Public (..) , StaticRoute (..) - , PublicRoute (..) -- * Smart constructor , static - , publicProduction - , publicDevel -- * Template Haskell helpers , staticFiles - , publicFiles {- -- * Embed files , getStaticHandler @@ -70,23 +65,15 @@ import qualified Data.Map as M import Data.IORef (readIORef, newIORef, writeIORef) import Network.Wai.Application.Static - ( StaticSettings (..), CacheSettings (..) - , defaultStaticSettings, defaultPublicSettings + ( StaticSettings (..) + , defaultWebAppSettings, defaultFileServerSettings , staticAppPieces - , pathFromPieces + , pathFromRawPieces + , fileSystemLookup + , pieceFromText ) --- | generally static assets referenced in html files --- assets get a checksum query parameter appended for perfect caching --- * a far future expire date is set --- * a given asset revision will only ever be downloaded once (if the browser maintains its cache) --- if you don't want to see a checksum in the url- use Public newtype Static = Static StaticSettings --- | same as Static, but there is no checksum query parameter appended --- generally html files and the favicon, but could be any file where you don't want the checksum parameter --- * the file checksum is used for an ETag. --- * this form of caching is not as good as the static- the browser can avoid downloading the file, but it always need to send a request with the etag value to the server to see if its copy is up to date -newtype Public = Public StaticSettings -- | Default value of 'Static' for a given file folder. -- @@ -95,22 +82,11 @@ newtype Public = Public StaticSettings static :: String -> FilePath -> IO Static static root fp = do hashes <- mkHashMap fp - return $ Static $ (defaultStaticSettings (Forever $ isStaticRequest hashes)) { - ssFolder = fp - , ssMkRedirect = \_ newPath -> S8.append (S8.pack (root ++ "/")) newPath - } - where - isStaticRequest hashes reqf reqh = case M.lookup reqf hashes of - Nothing -> False - Just h -> h == reqh - --- | no directory listing -public :: String -> FilePath -> CacheSettings -> Public -public root fp cache = Public $ (defaultPublicSettings cache) { - ssFolder = fp - , ssMkRedirect = \_ newPath -> S8.append (S8.pack (root ++ "/")) newPath + return $ Static $ defaultWebAppSettings { + ssFolder = fileSystemLookup fp } +{- publicProduction :: String -> FilePath -> IO Public publicProduction root fp = do etags <- mkPublicProductionEtag fp @@ -120,6 +96,7 @@ publicDevel :: String -> FilePath -> IO Public publicDevel root fp = do etags <- mkPublicDevelEtag fp return $ public root fp etags + -} -- | Manually construct a static route. @@ -131,24 +108,15 @@ publicDevel root fp = do -- E.g. When generating image galleries. data StaticRoute = StaticRoute [Text] [(Text, Text)] deriving (Eq, Show, Read) -data PublicRoute = PublicRoute [Text] [(Text, Text)] - deriving (Eq, Show, Read) type instance Route Static = StaticRoute -type instance Route Public = PublicRoute instance RenderRoute StaticRoute where renderRoute (StaticRoute x y) = (x, y) -instance RenderRoute PublicRoute where - renderRoute (PublicRoute x y) = (x, y) instance Yesod master => YesodDispatch Static master where - yesodDispatch (Static set) _ pieces _ _ = - Just $ staticAppPieces set pieces - -instance Yesod master => YesodDispatch Public master where - yesodDispatch (Public set) _ pieces _ _ = - Just $ staticAppPieces set pieces + yesodDispatch (Static set) _ textPieces _ _ = + Just $ staticAppPieces set (map pieceFromText textPieces) notHidden :: FilePath -> Bool notHidden ('.':_) = False @@ -188,11 +156,12 @@ mkHashMap dir = do hashAlist fs = mapM hashPair fs where hashPair :: [String] -> IO (FilePath, S8.ByteString) - hashPair pieces = do let file = pathFromPieces dir (map pack pieces) + hashPair pieces = do let file = pathFromRawPieces dir pieces h <- base64md5File file return (file, S8.pack h) -mkPublicDevelEtag :: FilePath -> IO CacheSettings +{- +mkPublicDevelEtag :: FilePath -> IO StaticSettings mkPublicDevelEtag dir = do etags <- mkHashMap dir mtimeVar <- newIORef (M.empty :: M.Map FilePath System.Time.ClockTime) @@ -208,15 +177,15 @@ mkPublicDevelEtag dir = do return $ if newt /= oldt then Nothing else Just checksum -mkPublicProductionEtag :: FilePath -> IO CacheSettings +mkPublicProductionEtag :: FilePath -> IO StaticSettings mkPublicProductionEtag dir = do etags <- mkHashMap dir return $ ETag $ \f -> return . M.lookup f $ etags +-} data StaticSite = StaticSite | PublicSite mkStaticFiles :: FilePath -> StaticSite -> Q [Dec] mkStaticFiles fp StaticSite = mkStaticFiles' fp "StaticRoute" True -mkStaticFiles fp PublicSite = mkStaticFiles' fp "PublicRoute" False mkStaticFiles' :: FilePath -- ^ static directory -> String -- ^ route constructor "StaticRoute" @@ -237,7 +206,7 @@ mkStaticFiles' fp routeConName makeHash = do let route = mkName routeConName pack' <- [|pack|] qs <- if makeHash - then do hash <- qRunIO $ base64md5File $ pathFromPieces fp (map pack f) + then do hash <- qRunIO $ base64md5File $ pathFromRawPieces fp f -- FIXME hash <- qRunIO . calcHash $ fp ++ '/' : intercalate "/" f [|[(pack $(lift hash), mempty)]|] else return $ ListE [] @@ -292,9 +261,11 @@ getStaticHandler static toSubR pieces = do -} +{- calcHash :: FilePath -> IO String calcHash fname = withBinaryFile fname ReadMode hashHandle where hashHandle h = do s <- L.hGetContents h return $! base64md5 s + -} From 08f284c096fcaac5c16c67c8d233c00395e685e8 Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Fri, 15 Jul 2011 11:45:40 +0300 Subject: [PATCH 21/23] Return to 0.1 API --- Yesod/Static.hs | 43 +++++++++++++++++++++---------------------- yesod-static.cabal | 1 + 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Yesod/Static.hs b/Yesod/Static.hs index a9d6d9af..ede6c47b 100644 --- a/Yesod/Static.hs +++ b/Yesod/Static.hs @@ -30,21 +30,15 @@ module Yesod.Static , StaticRoute (..) -- * Smart constructor , static + -- FIXME add embed -- * Template Haskell helpers , staticFiles - {- - -- * Embed files - , getStaticHandler - -} -- * Hashing , base64md5 -#if TEST - , getFileListPieces -#endif ) where import System.Directory -import qualified System.Time +--import qualified System.Time import Control.Monad import Yesod.Handler @@ -62,15 +56,14 @@ import qualified Data.Serialize import Data.Text (Text, pack) import Data.Monoid (mempty) import qualified Data.Map as M -import Data.IORef (readIORef, newIORef, writeIORef) +--import Data.IORef (readIORef, newIORef, writeIORef) +import Network.Wai (pathInfo) import Network.Wai.Application.Static ( StaticSettings (..) - , defaultWebAppSettings, defaultFileServerSettings - , staticAppPieces - , pathFromRawPieces + , defaultWebAppSettings , fileSystemLookup - , pieceFromText + , staticApp ) newtype Static = Static StaticSettings @@ -79,10 +72,10 @@ newtype Static = Static StaticSettings -- -- Does not have index files, uses default directory listings and default mime -- type list. -static :: String -> FilePath -> IO Static -static root fp = do - hashes <- mkHashMap fp - return $ Static $ defaultWebAppSettings { +static :: FilePath -> Static +static fp = + --hashes <- mkHashMap fp + Static $ defaultWebAppSettings { ssFolder = fileSystemLookup fp } @@ -115,8 +108,8 @@ instance RenderRoute StaticRoute where renderRoute (StaticRoute x y) = (x, y) instance Yesod master => YesodDispatch Static master where - yesodDispatch (Static set) _ textPieces _ _ = - Just $ staticAppPieces set (map pieceFromText textPieces) + yesodDispatch (Static set) _ textPieces _ _ = Just $ + \req -> staticApp set req { pathInfo = textPieces } notHidden :: FilePath -> Bool notHidden ('.':_) = False @@ -142,10 +135,12 @@ getFileListPieces = flip go id -- > style_css = StaticRoute ["style.css"] [] -- > js_script_js = StaticRoute ["js/script.js"] [] staticFiles :: FilePath -> Q [Dec] -staticFiles dir = mkStaticFiles dir StaticSite +staticFiles dir = mkStaticFiles dir +{- publicFiles :: FilePath -> Q [Dec] publicFiles dir = mkStaticFiles dir PublicSite +-} mkHashMap :: FilePath -> IO (M.Map FilePath S8.ByteString) mkHashMap dir = do @@ -184,8 +179,8 @@ mkPublicProductionEtag dir = do -} data StaticSite = StaticSite | PublicSite -mkStaticFiles :: FilePath -> StaticSite -> Q [Dec] -mkStaticFiles fp StaticSite = mkStaticFiles' fp "StaticRoute" True +mkStaticFiles :: FilePath -> Q [Dec] +mkStaticFiles fp = mkStaticFiles' fp "StaticRoute" True mkStaticFiles' :: FilePath -- ^ static directory -> String -- ^ route constructor "StaticRoute" @@ -269,3 +264,7 @@ calcHash fname = hashHandle h = do s <- L.hGetContents h return $! base64md5 s -} + +-- FIXME Greg: Is this correct? Where is this function supposed to be? +pathFromRawPieces :: FilePath -> [String] -> FilePath +pathFromRawPieces = undefined diff --git a/yesod-static.cabal b/yesod-static.cabal index 2f88f4b2..69b9f79c 100644 --- a/yesod-static.cabal +++ b/yesod-static.cabal @@ -28,6 +28,7 @@ library , directory >= 1.0 && < 1.2 , transformers >= 0.2 && < 0.3 , wai-app-static >= 0.3 && < 0.4 + , wai >= 0.4 && < 0.5 , text >= 0.5 && < 1.0 exposed-modules: Yesod.Static ghc-options: -Wall From 2297fc1fb6d71369ff16b14d9ca6656d4a69d1b5 Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Fri, 15 Jul 2011 12:05:54 +0300 Subject: [PATCH 22/23] Added embed, other minor fixes --- Yesod/Static.hs | 30 ++++++++++++++++++++++++++---- yesod-static.cabal | 1 + 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Yesod/Static.hs b/Yesod/Static.hs index ede6c47b..36d1fa8a 100644 --- a/Yesod/Static.hs +++ b/Yesod/Static.hs @@ -30,7 +30,7 @@ module Yesod.Static , StaticRoute (..) -- * Smart constructor , static - -- FIXME add embed + , embed -- * Template Haskell helpers , staticFiles -- * Hashing @@ -40,6 +40,7 @@ module Yesod.Static import System.Directory --import qualified System.Time import Control.Monad +import Data.FileEmbed (embedDir) import Yesod.Handler import Yesod.Core @@ -58,12 +59,18 @@ import Data.Monoid (mempty) import qualified Data.Map as M --import Data.IORef (readIORef, newIORef, writeIORef) import Network.Wai (pathInfo) +import Data.Char (isLower, isDigit) import Network.Wai.Application.Static ( StaticSettings (..) , defaultWebAppSettings , fileSystemLookup , staticApp + , embeddedLookup + , toEmbedded + , pathFromPieces + , toPiece + , fixPathName ) newtype Static = Static StaticSettings @@ -79,6 +86,14 @@ static fp = ssFolder = fileSystemLookup fp } +-- | Produces a 'Static' based on embedding file contents in the executable at +-- compile time. +embed :: FilePath -> Q Exp +embed fp = + [|Static (defaultWebAppSettings + { ssFolder = embeddedLookup (toEmbedded $(embedDir fp)) + })|] + {- publicProduction :: String -> FilePath -> IO Public publicProduction root fp = do @@ -196,7 +211,14 @@ mkStaticFiles' fp routeConName makeHash = do | '0' <= c && c <= '9' = c | otherwise = '_' mkRoute f = do - let name = mkName $ intercalate "_" $ map (map replace') f + let name' = intercalate "_" $ map (map replace') f + name = mkName $ + case () of + () + | null name' -> error "null-named file" + | isDigit (head name') -> '_' : name' + | isLower (head name') -> name' + | otherwise -> '_' : name' f' <- [|map pack $(lift f)|] let route = mkName routeConName pack' <- [|pack|] @@ -252,7 +274,7 @@ getStaticHandler static toSubR pieces = do where route = StaticRoute pieces [] toSub _ = static staticSite = getSubSite :: Site (Route Static) (String -> Maybe (GHandler Static y ChooseRep)) - handler = fromMaybe notFound $ handleSite staticSite undefined route "GET" + handler = fromMaybe notFound $ handleSite staticSite (error "Yesod.Static: getSTaticHandler") route "GET" -} @@ -267,4 +289,4 @@ calcHash fname = -- FIXME Greg: Is this correct? Where is this function supposed to be? pathFromRawPieces :: FilePath -> [String] -> FilePath -pathFromRawPieces = undefined +pathFromRawPieces fp = pathFromPieces fp . map (toPiece . pack . fixPathName) diff --git a/yesod-static.cabal b/yesod-static.cabal index 69b9f79c..006772b1 100644 --- a/yesod-static.cabal +++ b/yesod-static.cabal @@ -30,6 +30,7 @@ library , wai-app-static >= 0.3 && < 0.4 , wai >= 0.4 && < 0.5 , text >= 0.5 && < 1.0 + , file-embed >= 0.0.4.1 && < 0.5 exposed-modules: Yesod.Static ghc-options: -Wall From afe3f11179698f4e28da0e00d0a6404cac3b1c43 Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Tue, 19 Jul 2011 09:42:33 +0300 Subject: [PATCH 23/23] yesod 0.9 --- yesod-static.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yesod-static.cabal b/yesod-static.cabal index 006772b1..3fb2fb43 100644 --- a/yesod-static.cabal +++ b/yesod-static.cabal @@ -19,7 +19,7 @@ library build-depends: base >= 4 && < 5 , containers >= 0.4 , old-time >= 1.0 - , yesod-core >= 0.8 && < 0.9 + , yesod-core >= 0.9 && < 0.10 , base64-bytestring >= 0.1.0.1 && < 0.2 , pureMD5 >= 2.1.0.3 && < 2.2 , cereal >= 0.3 && < 0.4