Static file serving: extensible mime-type dictionary

This commit is contained in:
Michael Snoyman 2010-06-30 21:33:32 +03:00
parent 6ce79d673f
commit e32c5b9a53
3 changed files with 39 additions and 19 deletions

View File

@ -13,6 +13,7 @@ module Yesod
, module Yesod.Formable , module Yesod.Formable
, Application , Application
, liftIO , liftIO
, mempty
) where ) where
#if TEST #if TEST
@ -33,3 +34,4 @@ import Yesod.Handler hiding (runHandler)
import Network.Wai (Application) import Network.Wai (Application)
import Yesod.Hamlet import Yesod.Hamlet
import "transformers" Control.Monad.IO.Class (liftIO) import "transformers" Control.Monad.IO.Class (liftIO)
import Data.Monoid (mempty)

View File

@ -85,9 +85,14 @@ data Content = ContentFile FilePath
-> a -> a
-> IO (Either a a)) -> IO (Either a a))
-- | Zero-length enumerator.
emptyContent :: Content emptyContent :: Content
emptyContent = ContentEnum $ \_ -> return . Right emptyContent = ContentEnum $ \_ -> return . Right
-- | Anything which can be converted into 'Content'. Most of the time, you will
-- want to use the 'ContentEnum' constructor. An easier approach will be to use
-- a pre-defined 'toContent' function, such as converting your data into a lazy
-- bytestring and then calling 'toContent' on that.
class ToContent a where class ToContent a where
toContent :: a -> Content toContent :: a -> Content
@ -140,6 +145,9 @@ instance HasReps ChooseRep where
instance HasReps () where instance HasReps () where
chooseRep = defChooseRep [(typePlain, const $ return $ toContent "")] chooseRep = defChooseRep [(typePlain, const $ return $ toContent "")]
instance HasReps (ContentType, Content) where
chooseRep = const . return
instance HasReps [(ContentType, Content)] where instance HasReps [(ContentType, Content)] where
chooseRep a cts = return $ chooseRep a cts = return $
case filter (\(ct, _) -> go ct `elem` map go cts) a of case filter (\(ct, _) -> go ct `elem` map go cts) a of
@ -218,19 +226,20 @@ typeOctet = "application/octet-stream"
simpleContentType :: String -> String simpleContentType :: String -> String
simpleContentType = fst . span (/= ';') simpleContentType = fst . span (/= ';')
-- | Determine a mime-type based on the file extension. -- | A default extension to mime-type dictionary.
typeByExt :: String -> ContentType typeByExt :: [(String, ContentType)]
typeByExt "jpg" = typeJpeg typeByExt =
typeByExt "jpeg" = typeJpeg [ ("jpg", typeJpeg)
typeByExt "js" = typeJavascript , ("jpeg", typeJpeg)
typeByExt "css" = typeCss , ("js", typeJavascript)
typeByExt "html" = typeHtml , ("css", typeCss)
typeByExt "png" = typePng , ("html", typeHtml)
typeByExt "gif" = typeGif , ("png", typePng)
typeByExt "txt" = typePlain , ("gif", typeGif)
typeByExt "flv" = typeFlv , ("txt", typePlain)
typeByExt "ogv" = typeOgv , ("flv", typeFlv)
typeByExt _ = typeOctet , ("ogv", typeOgv)
]
-- | Get a file extension (everything after last period). -- | Get a file extension (everything after last period).
ext :: String -> String ext :: String -> String

View File

@ -38,6 +38,7 @@ module Yesod.Helpers.Static
import System.Directory import System.Directory
import Control.Monad import Control.Monad
import Data.Maybe (fromMaybe)
import Yesod import Yesod
import Data.List (intercalate) import Data.List (intercalate)
@ -51,7 +52,11 @@ import Test.HUnit hiding (Test)
-- | A function for looking up file contents. For serving from the file system, -- | A function for looking up file contents. For serving from the file system,
-- see 'fileLookupDir'. -- see 'fileLookupDir'.
data Static = Static (FilePath -> IO (Maybe (Either FilePath Content))) data Static = Static
{ staticLookup :: FilePath -> IO (Maybe (Either FilePath Content))
-- | Mapping from file extension to content type. See 'typeByExt'.
, staticTypes :: [(String, ContentType)]
}
mkYesodSub "Static" [] [$parseRoutes| mkYesodSub "Static" [] [$parseRoutes|
*Strings StaticRoute GET *Strings StaticRoute GET
@ -63,7 +68,7 @@ mkYesodSub "Static" [] [$parseRoutes|
-- probably are), the handler itself checks that no unsafe paths are being -- probably are), the handler itself checks that no unsafe paths are being
-- requested. In particular, no path segments may begin with a single period, -- requested. In particular, no path segments may begin with a single period,
-- so hidden files and parent directories are safe. -- so hidden files and parent directories are safe.
fileLookupDir :: FilePath -> Static fileLookupDir :: FilePath -> [(String, ContentType)] -> Static
fileLookupDir dir = Static $ \fp -> do fileLookupDir dir = Static $ \fp -> do
let fp' = dir ++ '/' : fp let fp' = dir ++ '/' : fp
exists <- doesFileExist fp' exists <- doesFileExist fp'
@ -72,16 +77,20 @@ fileLookupDir dir = Static $ \fp -> do
else return Nothing else return Nothing
getStaticRoute :: [String] getStaticRoute :: [String]
-> GHandler Static master [(ContentType, Content)] -> GHandler Static master (ContentType, Content)
getStaticRoute fp' = do getStaticRoute fp' = do
Static fl <- getYesodSub Static fl ctypes <- getYesodSub
when (any isUnsafe fp') notFound when (any isUnsafe fp') notFound
let fp = intercalate "/" fp' let fp = intercalate "/" fp'
content <- liftIO $ fl fp content <- liftIO $ fl fp
case content of case content of
Nothing -> notFound Nothing -> notFound
Just (Left fp'') -> sendFile (typeByExt $ ext fp'') fp'' Just (Left fp'') -> do
Just (Right bs) -> return [(typeByExt $ ext fp, bs)] 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 where
isUnsafe [] = True isUnsafe [] = True
isUnsafe ('.':_) = True isUnsafe ('.':_) = True