packages feed

barley-0.3: src/Main.hs

-- Copyright 2010 Google Inc.
-- 
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
-- 
--      http://www.apache.org/licenses/LICENSE-2.0
-- 
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.

module Main (main) where

import Barley.Loader
import Barley.Project
import Barley.Utils
import Control.Monad.IO.Class
import qualified Data.ByteString.Char8 as C
import qualified Data.Map as M
import Data.Maybe
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Prelude hiding (init, mod)
import Snap.Http.Server
import Snap.Types
import System.Directory (doesDirectoryExist, doesFileExist,
            getCurrentDirectory)
import System.Environment
import System.Exit
import System.FilePath ((<.>), (</>), takeExtension)
import System.Posix.Files (getFileStatus, modificationTime)
import Text.Html hiding ((</>), address, content, start)

main :: IO ()
main = do
    args <- parseArgs
    case args of
        Just ("start", pd) -> start pd
        Just ("init", pd) -> init True pd
        Just ("run", pd) -> run pd
        Just (cmd, _) -> do putStrLn $ "unknown command: " ++ cmd
                            exitFailure
        Nothing -> do putStrLn "Usage: barley <command> [project dir]"
                      exitFailure

parseArgs :: IO (Maybe (String, ProjectDir))
parseArgs = do
    args <- getArgs
    case args of
        [] -> return Nothing
        [cmd] -> return $ Just (cmd, CurrentDir)
        [cmd,fp] -> return $ Just (cmd, ProjectDir fp)
        _ -> putStrLn "Too many arguments." >> return Nothing

-- | Create a project directory structure and run the web server.
start :: ProjectDir -> IO ()
start pd = init False pd >> run pd

-- | Run the web server.
run :: ProjectDir -> IO ()
run pd = do
    enter pd
    let address = "*"
        port = 8080
        config = foldr ($) emptyConfig
            [ addListen (ListenHttp (C.pack address) port)
            , setAccessLog (Just "access.log")
            , setErrorLog (Just "error.log")
            ]
    putStrLn $ "Running on http://localhost:" ++ show port ++ "/"
    httpServe config genericHandler


type CompiledTemplate = Either String (Snap ())
-- | Compile a template to either an error string, or a Snap action
compileTemplate :: FilePath -> IO CompiledTemplate
compileTemplate filename = compileAndLoadFirst filename templateEntryPoints
  where
    templateEntryPoints =
        [ entryPoint "handler" id
        , entryPoint "page" htmlResult
        , entryPoint "output" stringResult
        ]
  
-- | Run a compiled template as a Snap action.
runTemplate :: FilePath -> CompiledTemplate -> Snap ()
runTemplate filename compilation = do
    either errorResult id $ compilation
  where
    errorResult errs = liftIO (errorHtml errs filename) >>= htmlResult

htmlResult :: Html -> Snap ()
htmlResult html = do
    modifyResponse $ setContentType (C.pack "text/html; charset=UTF-8")
    writeBS $ (T.encodeUtf8 . T.pack) $ renderHtml html
        -- warning: renderHTML wraps an additional HTML element around the
        -- content (for some ungodly reason)

stringResult :: String -> Snap ()
stringResult s = do
    modifyResponse $ setContentType (C.pack "text/plain; charset=UTF-8")
    writeBS $ (T.encodeUtf8 . T.pack) s

serveTemplate :: FilePath -> Snap ()
serveTemplate filename = do
    compilation <- liftIO $ compileTemplate filename
    compileOnly <- getParam (C.pack "__compile_only")
    case C.unpack `fmap` compileOnly of
        Just c | c == "1"  -> stringResult $ either id (const "OK") compilation
               | otherwise -> errorBadRequest
        Nothing            -> runTemplate filename compilation

data TypeInfo = TI { tiMimeType :: String, tiMaxAge :: Maybe Int }

extToTypeInfo :: M.Map String TypeInfo
extToTypeInfo = M.fromList
    [ (".css",  srcType "text/css")
    , (".gif",  imgType "image/gif")
    , (".html", srcType "text/html")
    , (".jpeg", imgType "image/jpeg")
    , (".jpg",  imgType "image/jpeg")
    , (".js",   srcType "application/javascript")
    , (".json", srcType "application/json")
    , (".pdf",  imgType "application/pdf")
    , (".png",  imgType "image/png")
    , (".svg",  imgType "image/svg+xml")
    , (".txt",  srcType "text/plain")
    , (".xhtml",srcType "application/xhtml+xml")
    , (".xml",  srcType "application/xml")
    ]
  where
    srcType s = TI s (Just 30)   -- revalidate things we might edit every 30 seconds
    imgType s = TI s (Just 3600) -- images are stable for at least an hour

defTypeInfo :: TypeInfo
defTypeInfo = TI "application/octet-stream" Nothing

serveStatic :: FilePath -> Snap ()
serveStatic filename = do
    fstat <- liftIO $ getFileStatus filename
    processModificationTime (modificationTime fstat) (tiMaxAge typeInfo)
    modifyResponse $ setContentType (C.pack $ tiMimeType typeInfo)
    sendFile filename
  where
    typeInfo = M.findWithDefault defTypeInfo extension extToTypeInfo
    extension = takeExtension filename

        
-- | Given a URL, render the corresponding template.
genericHandler :: Snap ()
genericHandler = do
    mpath <- (legalPath . C.unpack . rqPathInfo) `fmap` getRequest
    case mpath of
        Nothing -> errorNotFound
        Just relpath -> do
            cwd <- liftIO getCurrentDirectory
            let fullpath = cwd </> relpath
            routeWhenIO (doesFileExist fullpath)      $ serveStatic fullpath
            routeWhenIO (doesDirectoryExist fullpath) $ serveIndex fullpath
            serveTemplateIfExists $ fullpath <.> "hs"
  where
    serveIndex :: FilePath -> Snap ()
    serveIndex fullpath = serveTemplateIfExists $ fullpath </> "index.hs"
    serveTemplateIfExists :: FilePath -> Snap ()
    serveTemplateIfExists tmplpath = do
        routeWhenIO (doesFileExist tmplpath) $ serveTemplate tmplpath

-- | Given a list of errors and a template, create an HTML page that
-- displays the errors.
errorHtml :: String -> FilePath -> IO Html
errorHtml errs filename = do
    content <- readFile filename
    length content `seq` return ()
    let html = thehtml <<
               body << [
                   thediv ! [theclass "errors"] << [
                        h2 << "Errors",
                        pre << errs
                        ],
                   pre ! [theclass "sourcefile"] << content
               ]
    return html