packages feed

pansite-0.2.0.0: app/PansiteApp/App.hs

{-|
Module      : PansiteApp.App
Description : Main entrypoint for Pansite application
Copyright   : (C) Richard Cook, 2017-2018
Licence     : MIT
Maintainer  : rcook@rcook.org
Stability   : experimental
Portability : portable
-}

{-# LANGUAGE OverloadedStrings #-}

module PansiteApp.App (appMain) where

import           Control.Monad.IO.Class
import qualified Data.ByteString.Lazy as BL
import qualified Data.HashMap.Strict as HashMap
import           Data.IORef
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import           Network.HTTP.Types
import           Network.Wai
import           Network.Wai.Handler.Warp
import           Network.Wai.Logger
import           Pansite
import           PansiteApp.Build
import           PansiteApp.CommandLine
import           PansiteApp.ConfigInfo
import           PansiteApp.Util
import           PansiteApp.VersionInfo
import           System.Directory
import           System.FilePath

productVersion :: String
productVersion = "Pansite " ++ fullVersionString

runApp :: ApacheLogger -> ServerConfig -> ConfigInfo -> IO ()
runApp logger (ServerConfig port) configInfo = do
    putStrLn $ "Listening on port " ++ show port
    configInfoRef <- newIORef configInfo
    run port (app logger configInfoRef)

app :: ApacheLogger -> IORef ConfigInfo -> Application
app logger configInfoRef req f = do
    -- TODO: Think about concurrent accesses...
    -- TODO: This is a mess..
    oldConfigInfo <- liftIO $ readIORef configInfoRef
    mbConfigInfo <- liftIO $ updateConfigInfo oldConfigInfo
    configInfo <- case mbConfigInfo of
                        Nothing -> return oldConfigInfo
                        Just configInfo' -> do
                            liftIO $ atomicWriteIORef configInfoRef configInfo'
                            return configInfo'

    let (ConfigInfo _ _ (App routes _)) = configInfo

    -- TODO: Let's not rebuild this on every request
    let m = HashMap.fromList (map (\(Route ps targetPath) -> (map Text.pack ps, targetPath)) routes)

    let requestPath = pathInfo req
    putStrLn $ "requestPath=" ++ show requestPath

    case HashMap.lookup requestPath m of
        Just targetPath -> do
            liftIO $ logger req status200 (Just 0)

            build configInfo targetPath

            putStrLn $ "Read from " ++ targetPath

            -- TODO: Ugh. Let's make this less hacky. It works for now though.
            let (rawDataAction, contentType) = case (takeExtension targetPath) of
                                                ".css" -> (makeUtf8Response targetPath, "text/css; charset=utf-8")
                                                ".docx" -> (makeRawResponse targetPath, "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
                                                ".html" -> (makeUtf8Response targetPath, "text/html; charset=utf-8")
                                                _ -> (makeUtf8Response targetPath, "text/plain; charset=utf-8")

            rawData <- rawDataAction
            f $ responseLBS status200 [(hContentType, contentType)] rawData
        Nothing -> f $ responseLBS status404 [(hContentType, "text/plain")] "404 - Not Found"

makeUtf8Response :: FilePath -> IO BL.ByteString
makeUtf8Response targetPath = do
    -- TODO: Eliminate multiple encoding redundancy!
    content <- Text.pack <$> readFileUtf8 targetPath
    return $ BL.fromStrict (Text.encodeUtf8 content)

makeRawResponse :: FilePath -> IO BL.ByteString
makeRawResponse = BL.readFile

mkAppPaths :: FilePath -> FilePath -> IO AppPaths
mkAppPaths appYamlPath outputDir = do
    appYamlPath' <- canonicalizePath appYamlPath
    let appDir = takeDirectory appYamlPath'
    outputDir' <- canonicalizePath $ appDir </> outputDir
    let cacheDir = outputDir' </> "cache"
        shakeDir = outputDir' </> "shake"
    return $ AppPaths appYamlPath' appDir cacheDir shakeDir

appMain :: IO ()
appMain = parseCommand >>= handleCommand
    where
        handleCommand (RunCommand serverConfig appYamlPath outputDir) = withStdoutLogger $ \logger -> do
            appPaths <- mkAppPaths appYamlPath outputDir
            configInfo <- readConfigInfo appPaths
            runApp logger serverConfig configInfo
        handleCommand VersionCommand = putStrLn productVersion