+{-# LANGUAGE
+ DeriveDataTypeable
+ , OverloadedStrings
+ , ScopedTypeVariables
+ , UnicodeSyntax
+ #-}
module Rakka.SystemConfig
( SystemConfig
, SysConfValue(..)
, getSysConf
, getSysConfA
+
+ , setSysConf
+ , setSysConfA
+
+ , SiteName(..)
+ , BaseURI(..)
+ , DefaultPage(..)
+ , StyleSheet(..)
+ , Languages(..)
+ , GlobalLock(..)
+
+ , serializeTextPairs
+ , deserializeTextPairs
+ , serializeMap
+ , deserializeMap
)
where
-
+import Control.Applicative
+import Codec.Binary.UTF8.String
import Control.Arrow.ArrowIO
+import Control.Arrow.Unicode
+import Control.Concurrent.STM
+import Control.Monad
import Control.Monad.Trans
+import Control.Monad.Unicode
import qualified Data.ByteString.Char8 as C8
+import qualified Data.ByteString.Lazy as L
+import qualified Data.CaseInsensitive as CI
+import Data.Dynamic
+import Data.Map (Map)
+import qualified Data.Map as M
import Data.Maybe
-import Network
+import Data.Monoid.Unicode
+import Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+import GHC.Conc (unsafeIOToSTM)
+import Network.BSD
import qualified Network.HTTP.Lucu.Config as LC
-import Network.URI
+import Network.HTTP.Lucu hiding (Config)
+import Network.URI hiding (path)
+import Prelude.Unicode
+import Rakka.Page
+import Rakka.Utils
+import Subversion.FileSystem
+import Subversion.FileSystem.Revision
+import Subversion.FileSystem.Root
+import Subversion.FileSystem.Transaction
+import Subversion.Repository
+import Subversion.Types
+import System.FilePath.Posix
+import System.IO.Unsafe
+import System.Log.Logger
+
+logger :: String
+logger = "Rakka.SystemConfig"
data SystemConfig = SystemConfig {
- scLucuConf :: !LC.Config
+ scLucuConf :: !LC.Config
+ , scRepository :: !Repository
+ , scCache :: !(TVar (Map FilePath Dynamic))
}
+class (Typeable α, Show α, Eq α) ⇒ SysConfValue α where
+ confPath ∷ α → FilePath
+ serialize ∷ α → Text
+ deserialize ∷ Text → Maybe α
+ defaultValue ∷ SystemConfig → α
+
+mkSystemConfig :: LC.Config -> Repository -> IO SystemConfig
+mkSystemConfig lc repos
+ = do cache <- newTVarIO M.empty
+ return SystemConfig {
+ scLucuConf = lc
+ , scRepository = repos
+ , scCache = cache
+ }
-data SysConfValue
- = SiteName String
- | BaseURI URI
- | DefaultPage String
- | StyleSheet String
+getSysConf ∷ ∀m a. (MonadIO m, SysConfValue a) ⇒ SystemConfig → m a
+getSysConf sc
+ = liftIO $ atomically $
+ do cache ← readTVar (scCache sc)
+ let path = confPath ((⊥) ∷ a)
+ case M.lookup path cache of
+ Just val → pure ∘ fromJust $ fromDynamic val
+ Nothing → do val ← unsafeIOToSTM (getSysConf' sc)
+ writeTVar (scCache sc) (M.insert path (toDyn val) cache)
+ return val
+getSysConf' ∷ ∀α. SysConfValue α ⇒ SystemConfig → IO α
+getSysConf' sc
+ = do let path = fromConfPath $ confPath ((⊥) ∷ α)
+ fs ← getRepositoryFS (scRepository sc)
+ rev ← getYoungestRev fs
+ value ← withRevision fs rev
+ $ do exists ← isFile path
+ case exists of
+ True
+ → do str ← getFileContentsLBS path
+ return $ Just $ T.pack $ chomp $ decode $ L.unpack str
+ False
+ → return Nothing
+ case value of
+ Just str
+ → case deserialize str of
+ Just val
+ → debugM logger ("Got a config value at `" ⊕ path ⊕ "': " ⊕ show val) ≫
+ return val
+ Nothing
+ → fail ("Got an invalid config value at `" ⊕ path ⊕ "': " ⊕ show str)
+ Nothing
+ → do let val = defaultValue sc
+ debugM logger ("Got no config value at `" ⊕ path ⊕ "'. Defaulting to " ⊕ show val)
+ return val
-mkSystemConfig :: LC.Config -> SystemConfig
-mkSystemConfig = SystemConfig
+setSysConf :: forall m a. (MonadIO m, SysConfValue a) => SystemConfig -> String -> a -> m StatusCode
+setSysConf sc userID value
+ = liftIO $
+ do let path = confPath (undefined :: a)
+ current <- getSysConf sc
+ if current == value
+ then return NotModified
+ else do atomically $ do cache <- readTVar (scCache sc)
+ writeTVar (scCache sc) (M.delete path cache)
+ setSysConf' sc userID value
-getSysConf :: MonadIO m => SystemConfig -> SysConfValue -> m SysConfValue
-getSysConf sc key
- = liftIO $ sysConfDefault sc key -- FIXME
+setSysConf' ∷ ∀α. SysConfValue α ⇒ SystemConfig → String → α → IO StatusCode
+setSysConf' sc userID value
+ = do let path = fromConfPath $ confPath ((⊥) ∷ α)
+ str = (L.fromChunks ∘ (:[]) ∘ T.encodeUtf8 $ serialize value) ⊕ "\n"
+ repos = scRepository sc
+ fs ← getRepositoryFS repos
+ rev ← getYoungestRev fs
+ ret ← doReposTxn
+ repos
+ rev
+ userID
+ (Just "Automatic commit by Rakka for systemConfig update")
+ $ do exists ← isFile path
+ unless exists
+ $ createValueEntry path
+ applyTextLBS path Nothing str
+ case ret of
+ Left _ → return Conflict
+ Right _ → do debugM logger ("Set a config value at `" ⊕ path ⊕ "': " ⊕ show value)
+ return Created
+ where
+ createValueEntry ∷ FilePath → Txn ()
+ createValueEntry path
+ = do createParentDirectories path
+ makeFile path
+
+ createParentDirectories ∷ FilePath → Txn ()
+ createParentDirectories path
+ = do let parentPath = takeDirectory path
+ kind ← checkPath parentPath
+ case kind of
+ NoNode → createParentDirectories parentPath ≫
+ makeDirectory parentPath
+ FileNode → fail ("createParentDirectories: already exists a file: " ⊕ parentPath)
+ DirNode → return ()
-getSysConfA :: ArrowIO a => SystemConfig -> SysConfValue -> a b SysConfValue
-getSysConfA = (arrIO0 .) . getSysConf
+getSysConfA :: (ArrowIO a, SysConfValue c) => SystemConfig -> a b c
+getSysConfA = arrIO0 . getSysConf
-{- paths -}
-sysConfPath :: SysConfValue -> FilePath
-sysConfPath (SiteName _) = "/siteName"
-sysConfPath (BaseURI _) = "/baseURI"
-sysConfPath (DefaultPage _) = "/defaultPage"
-sysConfPath (StyleSheet _) = "/styleSheet"
+setSysConfA :: (ArrowIO a, SysConfValue c) => SystemConfig -> String -> a c StatusCode
+setSysConfA = (arrIO .) . setSysConf
-{- marshalling -}
-marshalSysConf :: SysConfValue -> String
-marshalSysConf (SiteName name) = name
-marshalSysConf (BaseURI uri ) = uriToString id uri ""
-marshalSysConf (DefaultPage name) = name
-marshalSysConf (StyleSheet name) = name
+fromConfPath :: FilePath -> FilePath
+fromConfPath = ("/config" </>)
+serializeTextPairs ∷ [(Text, Text)] → Text
+serializeTextPairs = T.intercalate "\n" ∘ (serializePair' <$>)
+ where
+ serializePair' ∷ (Text, Text) → Text
+ serializePair' (a, b) = a ⊕ " " ⊕ b
-{- unmarshalling -}
-unmarshalSysConf :: SysConfValue -> String -> SysConfValue
-unmarshalSysConf (SiteName _) name = SiteName name
-unmarshalSysConf (BaseURI _) uri = BaseURI $ fromJust $ parseURI uri
-unmarshalSysConf (DefaultPage _) name = DefaultPage name
-unmarshalSysConf (StyleSheet _) name = StyleSheet name
+serializeMap ∷ (k → Text) → (v → Text) → Map k v → Text
+serializeMap f g = serializeTextPairs ∘ ((f ⁂ g) <$>) ∘ M.toList
+deserializeTextPairs ∷ Text → Maybe [(Text, Text)]
+deserializeTextPairs = mapM deserializePair' ∘ T.lines
+ where
+ deserializePair' ∷ Text → Maybe (Text, Text)
+ deserializePair' s = case T.breakOn " " s of
+ (a, b)
+ | (¬) (T.null b) → Just (a, T.tail b)
+ _ → Nothing
-{- getting default value -}
-sysConfDefault :: SystemConfig -> SysConfValue -> IO SysConfValue
+deserializeMap ∷ Ord k ⇒ (Text → k) → (Text → v) → Text → Maybe (Map k v)
+deserializeMap f g = (M.fromList ∘ ((f ⁂ g) <$>) <$>) ∘ deserializeTextPairs
-sysConfDefault _ (SiteName _)
- = return $ SiteName "Rakka"
+newtype SiteName = SiteName Text deriving (Show, Typeable, Eq)
+instance SysConfValue SiteName where
+ confPath _ = "siteName"
+ serialize (SiteName name) = name
+ deserialize = Just . SiteName
+ defaultValue _ = SiteName "Rakka"
-sysConfDefault sc (BaseURI _)
- = do let conf = scLucuConf sc
- host = C8.unpack $ LC.cnfServerHost conf
- port = case LC.cnfServerPort conf of
- PortNumber num -> fromIntegral num
-
- defaultURI
+newtype BaseURI = BaseURI URI deriving (Show, Typeable, Eq)
+instance SysConfValue BaseURI where
+ confPath _ = "baseURI"
+ serialize (BaseURI uri) = T.pack $ uriToString id uri ""
+ deserialize uri = fmap BaseURI
+ $ do parsed ← parseURI (T.unpack uri)
+ when (uriPath parsed ≡ "" ) mzero
+ when (last (uriPath parsed) ≠ '/') mzero
+ when (uriQuery parsed ≠ "" ) mzero
+ when (uriFragment parsed ≠ "" ) mzero
+ return parsed
+ defaultValue sc
+ = let conf = scLucuConf sc
+ host = C8.unpack $ LC.cnfServerHost conf
+ port = unsafePerformIO $
+ do ent <- getServiceByName (LC.cnfServerPort conf) "tcp"
+ return (servicePort ent)
+ -- FIXME: There should be a way to change configurations
+ -- without web interface nor direct repository
+ -- modification.
+ defaultURI
= "http://" ++ host ++
(if port == 80
then ""
else ':' : show port) ++ "/"
+ in
+ BaseURI $ fromJust $ parseURI defaultURI
+
+newtype DefaultPage = DefaultPage Text deriving (Show, Typeable, Eq)
+instance SysConfValue DefaultPage where
+ confPath _ = "defaultPage"
+ serialize (DefaultPage name) = name
+ deserialize = Just . DefaultPage
+ defaultValue _ = DefaultPage "MainPage"
+
+newtype StyleSheet = StyleSheet Text deriving (Show, Typeable, Eq)
+instance SysConfValue StyleSheet where
+ confPath _ = "styleSheet"
+ serialize (StyleSheet name) = name
+ deserialize = Just . StyleSheet
+ defaultValue _ = StyleSheet "StyleSheet/Default"
- return $ BaseURI $ fromJust $ parseURI defaultURI
+newtype Languages = Languages (Map LanguageTag LanguageName) deriving (Show, Typeable, Eq)
+instance SysConfValue Languages where
+ confPath _ = "languages"
+ serialize (Languages langs) = serializeMap CI.foldedCase id langs
+ deserialize = (Languages <$>) ∘ deserializeMap CI.mk id
+ defaultValue _
+ = Languages $ M.fromList [ ("en", "English" )
+ , ("es", "Español" )
+ , ("de", "Deutsch" )
+ , ("fi", "Suomi" )
+ , ("fr", "Français" )
+ , ("ga", "Gaeilge" )
+ , ("gd", "Gàidhlig" )
+ , ("ja", "日本語" )
+ , ("pt", "Português")
+ , ("sv", "Svenska" )
+ ]
-sysConfDefault _ (DefaultPage _)
- = return $ DefaultPage "MainPage"
-sysConfDefault _ (StyleSheet _)
- = return $ StyleSheet "StyleSheet/Default"
+newtype GlobalLock = GlobalLock Bool deriving (Show, Typeable, Eq)
+instance SysConfValue GlobalLock where
+ confPath _ = "globalLock"
+ serialize (GlobalLock isLocked)
+ | isLocked = "*"
+ | otherwise = ""
+ deserialize "*" = Just (GlobalLock True)
+ deserialize "" = Just (GlobalLock False)
+ deserialize _ = Nothing
+ defaultValue _ = GlobalLock False