packages feed

mathblog-0.6: src/MB/Startup.hs

{-# LANGUAGE CPP, ScopedTypeVariables #-}
module MB.Startup
    ( startupConfigFromEnv
    , canonicalizeStartupConfig
    , versionString
#ifdef TESTING
    , Flag(..)
    , startupConfig
    , baseDirEnvName
    , baseDirParamName
    , htmlOutputDirEnvName
    , htmlDirParamName
    , getDataDirFlag
#endif
    )
where

import Prelude hiding (catch)
import Control.Exception
import Control.Monad
    ( mplus
    , when
    )
import System.Exit
    ( exitFailure
    )
import System.Environment
    ( getEnvironment
    , getArgs
    )
import System.Directory
import System.FilePath
import System.Console.GetOpt
import Network.Socket (HostName, PortNumber)

import MB.Types

data Flag = Listen (Maybe String)
          | DataDir FilePath
          | InitDataDirectory
          | ForceRegenerate
          | HtmlOutputDir FilePath
          | ConfigFile FilePath
          | BaseUrl String
          | Version
          | Help
            deriving (Eq)

getDataDirFlag :: [Flag] -> Maybe FilePath
getDataDirFlag [] = Nothing
getDataDirFlag (DataDir p:_) = Just p
getDataDirFlag (_:fs) = getDataDirFlag fs

getListenFlag :: [Flag] -> Maybe (Maybe String)
getListenFlag [] = Nothing
getListenFlag (Listen s:_) = Just s
getListenFlag (_:fs) = getListenFlag fs

getBaseUrlFlag :: [Flag] -> Maybe String
getBaseUrlFlag [] = Nothing
getBaseUrlFlag (BaseUrl s:_) = Just s
getBaseUrlFlag (_:fs) = getBaseUrlFlag fs

getHtmlOutputDirFlag :: [Flag] -> Maybe FilePath
getHtmlOutputDirFlag [] = Nothing
getHtmlOutputDirFlag (HtmlOutputDir p:_) = Just p
getHtmlOutputDirFlag (_:fs) = getHtmlOutputDirFlag fs

getConfigFilePath :: [Flag] -> Maybe FilePath
getConfigFilePath [] = Nothing
getConfigFilePath (ConfigFile p:_) = Just p
getConfigFilePath (_:fs) = getConfigFilePath fs

options :: [OptDescr Flag]
options = [ Option ['d'] [baseDirParamName] (ReqArg DataDir "PATH")
                       $ "Path where blog input data files (configuration, posts,\n" ++
                         "etc.) are located.  If this is not specified,\n" ++
                         baseDirEnvName ++ " must be set in the environment."

          , Option ['i'] ["init"] (NoArg InitDataDirectory)
                       $ "Initialize the blog data directory."
          , Option ['l'] ["listen"] (OptArg Listen "HOST:PORT")
                       $ "Make mb poll periodically and regenerate your\n" ++
                             "blog content when something changes.  This is\n" ++
                             "useful if you want to run a local web server\n" ++
                             "to view your posts as you're writing them.  Spawns\n" ++
                             "an HTTP server on the specified host and port."
          , Option ['b'] ["base-url"] (ReqArg BaseUrl "URL")
                       $ "The base URL to use in URL generation.  Overrides the\n" ++
                             "'baseUrl' configuration setting."
          , Option ['f'] ["force"] (NoArg ForceRegenerate)
                       "Force a rebuild of all blog content."
          , Option ['o'] [htmlDirParamName] (ReqArg HtmlOutputDir "PATH")
                       $ "The directory where HTML and image output should be\n" ++
                       "written.  If this is not specified, " ++ htmlOutputDirEnvName ++
                       "\nmust be set in the environment."
          , Option ['c'] ["config-file"] (ReqArg ConfigFile "FILENAME")
                       $ "Use the specified config file instead of 'blog.cfg'\n" ++
                         "in the data directory.  This path must be relative\n" ++
                         "to the data directory."
          , Option ['v'] ["version"] (NoArg Version)
                       "The version of this mathblog installation."
          , Option ['h'] ["help"] (NoArg Help)
                       "This help output."
          ]

versionString :: String
versionString = "mathblog version 0.6"

-- |Inspect the program environment to create a startup configuration.
-- If the configuration information is invalid or absent, this will
-- automatically emit usage info and exit the program.
startupConfigFromEnv :: IO StartupConfig
startupConfigFromEnv = do
  env  <- getEnvironment
  args <- getArgs

  let (flags, _, _) = getOpt Permute options args

  when (Version `elem` flags) $
           (putStrLn versionString >> exitFailure)

  when (Help `elem` flags) $
           (usage >> exitFailure)

  case startupConfig flags env of
    Nothing -> usage >> exitFailure
    Just conf -> do
      when (head (dataDirectory conf) /= '/') $
           do
             putStrLn $ baseDirEnvName ++ " or " ++
                          "--" ++ baseDirParamName ++ "=<path>" ++
                                   " must specify an absolute path"
             exitFailure

      return conf

parseListenString :: String -> Maybe (HostName, PortNumber)
parseListenString s =
    case break (== ':') s of
      ([], _) -> Nothing
      (_, []) -> Nothing
      (h, ':':p) -> case reads p :: [(Int, String)] of
                  [] -> Nothing
                  ((i, []):_) -> Just (h, toEnum i)
                  _ -> Nothing
      _ -> Nothing

-- |Create a startup configuration from the specified arguments list
-- and environment variable list.  Returns Nothing if the required
-- information was absent.
startupConfig :: [Flag] -> [(String, String)] -> Maybe StartupConfig
startupConfig flags env = do
  let doInit = InitDataDirectory `elem` flags
      force = ForceRegenerate `elem` flags

  listen <- case getListenFlag flags of
              -- If the listen flag is present, capture the value.
              Just f -> return $ Just f
              Nothing -> return Nothing

  d <- getDataDirFlag flags `mplus` (lookup baseDirEnvName env)
  o <- getHtmlOutputDirFlag flags `mplus` (lookup htmlOutputDirEnvName env)
  c <- getConfigFilePath flags `mplus` (Just $ d </> "blog.cfg")
  let b = getBaseUrlFlag flags

  lAddr <- case listen of
             Nothing -> return Nothing
             -- If the listen flag was given, then it must parse
             -- successfully.
             Just Nothing -> return $ Just ("localhost", 8000)
             Just (Just s) ->
                 (return . Just) =<< parseListenString s

  return $ StartupConfig { dataDirectory = d
                         , initDataDirectory = doInit
                         , forceRegeneration = force
                         , htmlOutputDirectory = o
                         , configFilePath = c
                         , listenAddr = lAddr
                         , overrideBaseUrl = b
                         }

canonicalizeStartupConfig :: StartupConfig -> IO StartupConfig
canonicalizeStartupConfig conf = do
  let dataDirName = takeFileName $ dataDirectory conf
      outputName = takeFileName $ htmlOutputDirectory conf

  d' <- (canonicalizePath $ takeDirectory $ dataDirectory conf) `catch`
        \(_::SomeException) -> do
          putStrLn $ "Invalid data directory: " ++ dataDirectory conf
          exitFailure

  o' <- (canonicalizePath $ takeDirectory $ htmlOutputDirectory conf) `catch`
        \(_::SomeException) -> do
          putStrLn $ "Invalid output directory: " ++ htmlOutputDirectory conf
          exitFailure

  return $ conf { dataDirectory = d' </> dataDirName
                , htmlOutputDirectory = o' </> outputName
                }

baseDirEnvName :: String
baseDirEnvName = "MB_DATA_DIR"

baseDirParamName :: String
baseDirParamName = "data-dir"

htmlOutputDirEnvName :: String
htmlOutputDirEnvName = "MB_OUTPUT_DIR"

htmlDirParamName :: String
htmlDirParamName = "output-dir"

usage :: IO ()
usage = do
  let h = concat [ "Usage: mb [args] (or set " ++ baseDirEnvName ++
                   " and " ++ htmlOutputDirEnvName ++ " in the environment)\n\n"
                 , "mb is a tool for creating and managing a mathematically-inclined\n"
                 , "weblog.\n"
                 ]
  putStrLn $ usageInfo h options