1 module Rakka.SystemConfig
5 , mkSystemConfig -- private
20 , serializeStringPairs
21 , deserializeStringPairs
25 import Codec.Binary.UTF8.String
26 import Control.Arrow.ArrowIO
27 import Control.Concurrent.STM
29 import Control.Monad.Trans
30 import qualified Data.ByteString.Char8 as C8
31 import qualified Data.ByteString.Lazy as L
34 import qualified Data.Map as M
36 import GHC.Conc (unsafeIOToSTM)
38 import qualified Network.HTTP.Lucu.Config as LC
39 import Network.HTTP.Lucu.Utils
40 import Network.HTTP.Lucu hiding (Config)
41 import Network.URI hiding (path)
44 import Subversion.FileSystem
45 import Subversion.FileSystem.Revision
46 import Subversion.FileSystem.Root
47 import Subversion.FileSystem.Transaction
48 import Subversion.Repository
49 import Subversion.Types
50 import System.FilePath.Posix
51 import System.IO.Unsafe
52 import System.Log.Logger
56 logger = "Rakka.SystemConfig"
59 data SystemConfig = SystemConfig {
60 scLucuConf :: !LC.Config
61 , scRepository :: !Repository
62 , scCache :: !(TVar (Map FilePath Dynamic))
66 class (Typeable a, Show a, Eq a) => SysConfValue a where
67 confPath :: a -> FilePath
68 serialize :: a -> String
69 deserialize :: String -> Maybe a
70 defaultValue :: SystemConfig -> a
73 mkSystemConfig :: LC.Config -> Repository -> IO SystemConfig
74 mkSystemConfig lc repos
75 = do cache <- newTVarIO M.empty
78 , scRepository = repos
82 getSysConf :: forall m a. (MonadIO m, SysConfValue a) => SystemConfig -> m a
86 do let path = confPath (undefined :: a)
88 cache <- readTVar (scCache sc)
90 case M.lookup path cache of
91 Just val -> return $ fromJust $ fromDynamic val
92 Nothing -> do val <- unsafeIOToSTM (getSysConf' sc)
93 writeTVar (scCache sc) (M.insert path (toDyn val) cache)
97 getSysConf' :: forall a. SysConfValue a => SystemConfig -> IO a
99 = do let path = fromConfPath $ confPath (undefined :: a)
101 fs <- getRepositoryFS (scRepository sc)
102 rev <- getYoungestRev fs
103 value <- withRevision fs rev
104 $ do exists <- isFile path
107 -> do str <- getFileContentsLBS path
108 return $ Just $ chomp $ decode $ L.unpack str
114 -> case deserialize str of
116 -> do debugM logger ("Got a config value at `" ++ path ++ "': " ++ show val)
119 -> fail ("Got an invalid config value at `" ++ path ++ "': " ++ str)
121 -> do let val = defaultValue sc
122 debugM logger ("Got no config value at `" ++ path ++ "'. Defaulting to " ++ show val)
126 setSysConf :: forall m a. (MonadIO m, SysConfValue a) => SystemConfig -> String -> a -> m StatusCode
127 setSysConf sc userID value
129 do let path = confPath (undefined :: a)
131 current <- getSysConf sc
133 then return NotModified
134 else do atomically $ do cache <- readTVar (scCache sc)
135 writeTVar (scCache sc) (M.delete path cache)
136 setSysConf' sc userID value
139 setSysConf' :: forall a. SysConfValue a => SystemConfig -> String -> a -> IO StatusCode
140 setSysConf' sc userID value
141 = do let path = fromConfPath $ confPath (undefined :: a)
142 str = L.pack $ encode $ serialize value ++ "\n"
143 repos = scRepository sc
144 fs <- getRepositoryFS repos
145 rev <- getYoungestRev fs
150 (Just "Automatic commit by Rakka for systemConfig update")
151 $ do exists <- isFile path
153 $ createValueEntry path
154 applyTextLBS path Nothing str
156 Left _ -> return Conflict
157 Right _ -> do debugM logger ("Set a config value at `" ++ path ++ "': " ++ show value)
160 createValueEntry :: FilePath -> Txn ()
161 createValueEntry path
162 = do createParentDirectories path
165 createParentDirectories :: FilePath -> Txn ()
166 createParentDirectories path
167 = do let parentPath = takeDirectory path
168 kind <- checkPath parentPath
170 NoNode -> do createParentDirectories parentPath
171 makeDirectory parentPath
172 FileNode -> fail ("createParentDirectories: already exists a file: " ++ parentPath)
176 getSysConfA :: (ArrowIO a, SysConfValue c) => SystemConfig -> a b c
177 getSysConfA = arrIO0 . getSysConf
180 setSysConfA :: (ArrowIO a, SysConfValue c) => SystemConfig -> String -> a c StatusCode
181 setSysConfA = (arrIO .) . setSysConf
184 fromConfPath :: FilePath -> FilePath
185 fromConfPath = ("/config" </>)
188 serializeStringPairs :: [(String, String)] -> String
189 serializeStringPairs = joinWith "\n" . map serializePair'
191 serializePair' :: (String, String) -> String
192 serializePair' (a, b) = a ++ " " ++ b
195 deserializeStringPairs :: String -> Maybe [(String, String)]
196 deserializeStringPairs = mapM deserializePair' . lines
198 deserializePair' :: String -> Maybe (String, String)
199 deserializePair' s = case break (== ' ') s of
200 (a, ' ':b) -> Just (a, b)
207 newtype SiteName = SiteName String deriving (Show, Typeable, Eq)
208 instance SysConfValue SiteName where
209 confPath _ = "siteName"
210 serialize (SiteName name) = name
211 deserialize = Just . SiteName
212 defaultValue _ = SiteName "Rakka"
215 newtype BaseURI = BaseURI URI deriving (Show, Typeable, Eq)
216 instance SysConfValue BaseURI where
217 confPath _ = "baseURI"
218 serialize (BaseURI uri) = uriToString id uri ""
219 deserialize uri = fmap BaseURI
220 $ do parsed <- parseURI uri
221 when (uriPath parsed == "" ) (fail undefined)
222 when (last (uriPath parsed) /= '/') (fail undefined)
223 when (uriQuery parsed /= "" ) (fail undefined)
224 when (uriFragment parsed /= "" ) (fail undefined)
227 = let conf = scLucuConf sc
228 host = C8.unpack $ LC.cnfServerHost conf
229 port = unsafePerformIO $
230 do ent <- getServiceByName (LC.cnfServerPort conf) "tcp"
231 return (servicePort ent)
232 -- FIXME: There should be a way to change configurations
233 -- without web interface nor direct repository
236 = "http://" ++ host ++
239 else ':' : show port) ++ "/"
241 BaseURI $ fromJust $ parseURI defaultURI
244 newtype DefaultPage = DefaultPage String deriving (Show, Typeable, Eq)
245 instance SysConfValue DefaultPage where
246 confPath _ = "defaultPage"
247 serialize (DefaultPage name) = name
248 deserialize = Just . DefaultPage
249 defaultValue _ = DefaultPage "MainPage"
252 newtype StyleSheet = StyleSheet String deriving (Show, Typeable, Eq)
253 instance SysConfValue StyleSheet where
254 confPath _ = "styleSheet"
255 serialize (StyleSheet name) = name
256 deserialize = Just . StyleSheet
257 defaultValue _ = StyleSheet "StyleSheet/Default"
260 newtype Languages = Languages (Map LanguageTag LanguageName) deriving (Show, Typeable, Eq)
261 instance SysConfValue Languages where
262 confPath _ = "languages"
263 serialize (Languages langs) = serializeStringPairs (M.toList langs)
264 deserialize = fmap (Languages . M.fromList) . deserializeStringPairs
266 = Languages $ M.fromList [ ("en", "English" )
270 , ("fr", "Français" )
272 , ("gd", "Gàidhlig" )
274 , ("pt", "Português")
279 newtype GlobalLock = GlobalLock Bool deriving (Show, Typeable, Eq)
280 instance SysConfValue GlobalLock where
281 confPath _ = "globalLock"
282 serialize (GlobalLock isLocked)
285 deserialize "*" = Just (GlobalLock True)
286 deserialize "" = Just (GlobalLock False)
287 deserialize _ = Nothing
288 defaultValue _ = GlobalLock False