keter/Keter/Postgres.hs

152 lines
4.9 KiB
Haskell
Raw Normal View History

2012-05-06 20:09:27 +04:00
{-# LANGUAGE OverloadedStrings #-}
2012-05-08 16:04:46 +04:00
{-# LANGUAGE RecordWildCards #-}
2012-05-14 13:26:20 +04:00
{-# LANGUAGE NoImplicitPrelude #-}
2012-05-06 20:09:27 +04:00
module Keter.Postgres
( -- * Types
Appname
, DBInfo (..)
, Postgres
2012-05-08 16:04:46 +04:00
-- ** Settings
, Settings
, setupDBInfo
2012-05-06 20:09:27 +04:00
-- * Functions
, load
, getInfo
) where
2012-05-14 13:26:20 +04:00
import Keter.Prelude
import qualified Prelude as P
2012-05-06 20:09:27 +04:00
import qualified Data.Text as T
import Data.Yaml
import qualified Data.Map as Map
import Control.Monad (forever, mzero, replicateM)
import qualified Control.Monad.Trans.State as S
import Control.Monad.Trans.Class (lift)
import Control.Applicative ((<$>), (<*>))
import qualified System.Random as R
2012-05-14 13:26:20 +04:00
import Data.Text.Lazy.Builder (toLazyText)
2012-05-06 20:09:27 +04:00
import qualified Data.Text.Lazy as TL
import System.Process (readProcess)
2012-05-08 16:04:46 +04:00
data Settings = Settings
2012-05-14 13:26:20 +04:00
{ setupDBInfo :: DBInfo -> P.IO ()
2012-05-08 16:04:46 +04:00
-- ^ How to create the given user/database. Default: uses the @psql@
-- command line tool and @sudo -u postgres@.
}
instance Default Settings where
def = Settings
{ setupDBInfo = \DBInfo{..} -> do
let sql = toLazyText $
2012-05-14 13:26:20 +04:00
"CREATE USER " ++ fromText dbiUser ++
" PASSWORD '" ++ fromText dbiPass ++
"';\nCREATE DATABASE " ++ fromText dbiName ++
" OWNER " ++ fromText dbiUser ++
2012-05-08 16:04:46 +04:00
";"
_ <- readProcess "sudo" ["-u", "postgres", "psql"] $ TL.unpack sql
return ()
}
2012-05-06 20:09:27 +04:00
-- | Name of the application. Should just be the basename of the application
-- file.
type Appname = Text
-- | Information on an individual PostgreSQL database.
data DBInfo = DBInfo
{ dbiName :: Text
, dbiUser :: Text
, dbiPass :: Text
}
deriving Show
randomDBI :: R.StdGen -> (DBInfo, R.StdGen)
randomDBI =
S.runState (DBInfo <$> rt <*> rt <*> rt)
where
rt = T.pack <$> replicateM 10 (S.state $ R.randomR ('a', 'z'))
instance ToJSON DBInfo where
toJSON (DBInfo n u p) = object
[ "name" .= n
, "user" .= u
, "pass" .= p
]
instance FromJSON DBInfo where
parseJSON (Object o) = DBInfo
<$> o .: "name"
<*> o .: "user"
<*> o .: "pass"
parseJSON _ = mzero
-- | Abstract type allowing access to config information via 'getInfo'
newtype Postgres = Postgres
2012-05-14 13:26:20 +04:00
{ getInfo :: Appname -> KIO (Either SomeException DBInfo)
2012-05-06 20:09:27 +04:00
-- ^ Get information on an individual app\'s database information. If no
-- information exists, it will create a random database, add it to the
-- config file, and return it.
}
2012-05-14 13:26:20 +04:00
data Command = GetConfig Appname (Either SomeException DBInfo -> KIO ())
2012-05-06 20:09:27 +04:00
-- | Load a set of existing connections from a config file. If the file does
-- not exist, it will be created. Any newly created databases will
-- automatically be saved to this file.
2012-05-14 13:26:20 +04:00
load :: Settings -> FilePath -> KIO (Either SomeException Postgres)
2012-05-08 16:04:46 +04:00
load Settings{..} fp = do
2012-05-14 13:26:20 +04:00
mdb <- liftIO $ do
createTree $ directory fp
e <- isFile fp
2012-05-06 20:09:27 +04:00
if e
2012-05-14 13:26:20 +04:00
then decodeFile $ toString fp
2012-05-06 20:09:27 +04:00
else return $ Just Map.empty
2012-05-14 13:26:20 +04:00
case mdb of
Left e -> return $ Left e
Right Nothing -> return $ Left $ toException $ CannotParsePostgres fp
Right (Just db0) -> go (db0 :: Map.Map Appname DBInfo)
where
go db0 = do
chan <- newChan
g0 <- newStdGen
forkKIO $ flip S.evalStateT (db0, g0) $ forever $ loop chan
return $ Right $ Postgres $ \appname -> do
x <- newEmptyMVar
writeChan chan $ GetConfig appname $ putMVar x
takeMVar x
tmpfp = fp <.> "tmp"
loop chan = do
GetConfig appname f <- lift $ readChan chan
2012-05-06 20:09:27 +04:00
(db, g) <- S.get
dbi <-
case Map.lookup appname db of
2012-05-14 13:26:20 +04:00
Just dbi -> return $ Right dbi
2012-05-06 20:09:27 +04:00
Nothing -> do
let (dbi', g') = randomDBI g
let dbi = dbi'
2012-08-22 20:58:16 +04:00
{ dbiName = sanitize appname ++ dbiName dbi'
, dbiUser = sanitize appname ++ dbiUser dbi'
2012-05-06 20:09:27 +04:00
}
2012-05-14 13:26:20 +04:00
ex <- lift $ liftIO $ setupDBInfo dbi
case ex of
Left e -> return $ Left e
Right () -> do
let db' = Map.insert appname dbi db
ey <- lift $ liftIO $ do
encodeFile (toString tmpfp) db'
rename tmpfp fp
case ey of
Left e -> return $ Left e
Right () -> do
S.put (db', g')
return $ Right dbi
2012-05-06 20:09:27 +04:00
lift $ f dbi
2012-08-22 20:58:16 +04:00
sanitize = T.map sanitize'
sanitize' c
| 'A' <= c && c <= 'Z' = c
| 'a' <= c && c <= 'z' = c
| '0' <= c && c <= '9' = c
| otherwise = '_'