7 module Rakka.SystemConfig
11 , mkSystemConfig -- private
27 , deserializeTextPairs
32 import Control.Applicative
33 import Codec.Binary.UTF8.String
34 import Control.Arrow.ArrowIO
35 import Control.Arrow.Unicode
36 import Control.Concurrent.STM
38 import Control.Monad.Trans
39 import Control.Monad.Unicode
40 import qualified Data.ByteString.Char8 as C8
41 import qualified Data.ByteString.Lazy as L
42 import qualified Data.CaseInsensitive as CI
45 import qualified Data.Map as M
47 import Data.Monoid.Unicode
48 import Data.Text (Text)
49 import qualified Data.Text as T
50 import qualified Data.Text.Encoding as T
51 import GHC.Conc (unsafeIOToSTM)
53 import qualified Network.HTTP.Lucu.Config as LC
54 import Network.HTTP.Lucu hiding (Config)
55 import Network.URI hiding (path)
56 import Prelude.Unicode
59 import Subversion.FileSystem
60 import Subversion.FileSystem.Revision
61 import Subversion.FileSystem.Root
62 import Subversion.FileSystem.Transaction
63 import Subversion.Repository
64 import Subversion.Types
65 import System.FilePath.Posix
66 import System.IO.Unsafe
67 import System.Log.Logger
70 logger = "Rakka.SystemConfig"
73 data SystemConfig = SystemConfig {
74 scLucuConf :: !LC.Config
75 , scRepository :: !Repository
76 , scCache :: !(TVar (Map FilePath Dynamic))
79 class (Typeable α, Show α, Eq α) ⇒ SysConfValue α where
80 confPath ∷ α → FilePath
82 deserialize ∷ Text → Maybe α
83 defaultValue ∷ SystemConfig → α
85 mkSystemConfig :: LC.Config -> Repository -> IO SystemConfig
86 mkSystemConfig lc repos
87 = do cache <- newTVarIO M.empty
90 , scRepository = repos
94 getSysConf ∷ ∀m a. (MonadIO m, SysConfValue a) ⇒ SystemConfig → m a
96 = liftIO $ atomically $
97 do cache ← readTVar (scCache sc)
98 let path = confPath ((⊥) ∷ a)
99 case M.lookup path cache of
100 Just val → pure ∘ fromJust $ fromDynamic val
101 Nothing → do val ← unsafeIOToSTM (getSysConf' sc)
102 writeTVar (scCache sc) (M.insert path (toDyn val) cache)
105 getSysConf' ∷ ∀α. SysConfValue α ⇒ SystemConfig → IO α
107 = do let path = fromConfPath $ confPath ((⊥) ∷ α)
108 fs ← getRepositoryFS (scRepository sc)
109 rev ← getYoungestRev fs
110 value ← withRevision fs rev
111 $ do exists ← isFile path
114 → do str ← getFileContentsLBS path
115 return $ Just $ T.pack $ chomp $ decode $ L.unpack str
120 → case deserialize str of
122 → debugM logger ("Got a config value at `" ⊕ path ⊕ "': " ⊕ show val) ≫
125 → fail ("Got an invalid config value at `" ⊕ path ⊕ "': " ⊕ show str)
127 → do let val = defaultValue sc
128 debugM logger ("Got no config value at `" ⊕ path ⊕ "'. Defaulting to " ⊕ show val)
131 setSysConf :: forall m a. (MonadIO m, SysConfValue a) => SystemConfig -> String -> a -> m StatusCode
132 setSysConf sc userID value
134 do let path = confPath (undefined :: a)
136 current <- getSysConf sc
138 then return NotModified
139 else do atomically $ do cache <- readTVar (scCache sc)
140 writeTVar (scCache sc) (M.delete path cache)
141 setSysConf' sc userID value
144 setSysConf' ∷ ∀α. SysConfValue α ⇒ SystemConfig → String → α → IO StatusCode
145 setSysConf' sc userID value
146 = do let path = fromConfPath $ confPath ((⊥) ∷ α)
147 str = (L.fromChunks ∘ (:[]) ∘ T.encodeUtf8 $ serialize value) ⊕ "\n"
148 repos = scRepository sc
149 fs ← getRepositoryFS repos
150 rev ← getYoungestRev fs
155 (Just "Automatic commit by Rakka for systemConfig update")
156 $ do exists ← isFile path
158 $ createValueEntry path
159 applyTextLBS path Nothing str
161 Left _ → return Conflict
162 Right _ → do debugM logger ("Set a config value at `" ⊕ path ⊕ "': " ⊕ show value)
165 createValueEntry ∷ FilePath → Txn ()
166 createValueEntry path
167 = do createParentDirectories path
170 createParentDirectories ∷ FilePath → Txn ()
171 createParentDirectories path
172 = do let parentPath = takeDirectory path
173 kind ← checkPath parentPath
175 NoNode → createParentDirectories parentPath ≫
176 makeDirectory parentPath
177 FileNode → fail ("createParentDirectories: already exists a file: " ⊕ parentPath)
180 getSysConfA :: (ArrowIO a, SysConfValue c) => SystemConfig -> a b c
181 getSysConfA = arrIO0 . getSysConf
184 setSysConfA :: (ArrowIO a, SysConfValue c) => SystemConfig -> String -> a c StatusCode
185 setSysConfA = (arrIO .) . setSysConf
188 fromConfPath :: FilePath -> FilePath
189 fromConfPath = ("/config" </>)
191 serializeTextPairs ∷ [(Text, Text)] → Text
192 serializeTextPairs = T.intercalate "\n" ∘ (serializePair' <$>)
194 serializePair' ∷ (Text, Text) → Text
195 serializePair' (a, b) = a ⊕ " " ⊕ b
197 serializeMap ∷ (k → Text) → (v → Text) → Map k v → Text
198 serializeMap f g = serializeTextPairs ∘ ((f ⁂ g) <$>) ∘ M.toList
200 deserializeTextPairs ∷ Text → Maybe [(Text, Text)]
201 deserializeTextPairs = mapM deserializePair' ∘ T.lines
203 deserializePair' ∷ Text → Maybe (Text, Text)
204 deserializePair' s = case T.breakOn " " s of
206 | (¬) (T.null b) → Just (a, T.tail b)
209 deserializeMap ∷ Ord k ⇒ (Text → k) → (Text → v) → Text → Maybe (Map k v)
210 deserializeMap f g = (M.fromList ∘ ((f ⁂ g) <$>) <$>) ∘ deserializeTextPairs
212 newtype SiteName = SiteName Text deriving (Show, Typeable, Eq)
213 instance SysConfValue SiteName where
214 confPath _ = "siteName"
215 serialize (SiteName name) = name
216 deserialize = Just . SiteName
217 defaultValue _ = SiteName "Rakka"
219 newtype BaseURI = BaseURI URI deriving (Show, Typeable, Eq)
220 instance SysConfValue BaseURI where
221 confPath _ = "baseURI"
222 serialize (BaseURI uri) = T.pack $ uriToString id uri ""
223 deserialize uri = fmap BaseURI
224 $ do parsed ← parseURI (T.unpack uri)
225 when (uriPath parsed ≡ "" ) mzero
226 when (last (uriPath parsed) ≠ '/') mzero
227 when (uriQuery parsed ≠ "" ) mzero
228 when (uriFragment parsed ≠ "" ) mzero
231 = let conf = scLucuConf sc
232 host = C8.unpack $ LC.cnfServerHost conf
233 port = unsafePerformIO $
234 do ent <- getServiceByName (LC.cnfServerPort conf) "tcp"
235 return (servicePort ent)
236 -- FIXME: There should be a way to change configurations
237 -- without web interface nor direct repository
240 = "http://" ++ host ++
243 else ':' : show port) ++ "/"
245 BaseURI $ fromJust $ parseURI defaultURI
247 newtype DefaultPage = DefaultPage Text deriving (Show, Typeable, Eq)
248 instance SysConfValue DefaultPage where
249 confPath _ = "defaultPage"
250 serialize (DefaultPage name) = name
251 deserialize = Just . DefaultPage
252 defaultValue _ = DefaultPage "MainPage"
254 newtype StyleSheet = StyleSheet Text deriving (Show, Typeable, Eq)
255 instance SysConfValue StyleSheet where
256 confPath _ = "styleSheet"
257 serialize (StyleSheet name) = name
258 deserialize = Just . StyleSheet
259 defaultValue _ = StyleSheet "StyleSheet/Default"
261 newtype Languages = Languages (Map LanguageTag LanguageName) deriving (Show, Typeable, Eq)
262 instance SysConfValue Languages where
263 confPath _ = "languages"
264 serialize (Languages langs) = serializeMap CI.foldedCase id langs
265 deserialize = (Languages <$>) ∘ deserializeMap CI.mk id
267 = Languages $ M.fromList [ ("en", "English" )
271 , ("fr", "Français" )
273 , ("gd", "Gàidhlig" )
275 , ("pt", "Português")
280 newtype GlobalLock = GlobalLock Bool deriving (Show, Typeable, Eq)
281 instance SysConfValue GlobalLock where
282 confPath _ = "globalLock"
283 serialize (GlobalLock isLocked)
286 deserialize "*" = Just (GlobalLock True)
287 deserialize "" = Just (GlobalLock False)
288 deserialize _ = Nothing
289 defaultValue _ = GlobalLock False