From 36da990afc3f9c5b1c4ce95e45821b2f13e169e5 Mon Sep 17 00:00:00 2001 From: Johannes Schilling <dario@deaktualisierung.org> Date: Mon, 4 Jan 2016 18:40:25 +0100 Subject: [PATCH] implement first part of database stuff --- myOwnPad.cabal | 3 ++ server/Server.hs | 8 ++- src/Pad/DB.hs | 128 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 src/Pad/DB.hs diff --git a/myOwnPad.cabal b/myOwnPad.cabal index 43a21e8..8c8f86a 100644 --- a/myOwnPad.cabal +++ b/myOwnPad.cabal @@ -17,6 +17,7 @@ library Pad.NetProtocol.Types Pad.NetProtocol.Instances Pad.NetProtocol.Parse + Pad.DB default-language: Haskell2010 other-extensions: InstanceSigs @@ -27,6 +28,7 @@ library , attoparsec , word8 , time + , postgresql-simple executable myOwnPad-server hs-source-dirs: server @@ -46,6 +48,7 @@ executable myOwnPad-server , wai-websockets , warp ghc-options: -O3 -threaded + , postgresql-simple executable myOwnPad-client hs-source-dirs: client diff --git a/server/Server.hs b/server/Server.hs index e44b84a..8810453 100644 --- a/server/Server.hs +++ b/server/Server.hs @@ -19,14 +19,17 @@ import System.Directory import Control.Concurrent import Control.Exception +import Database.PostgreSQL.Simple as PSQL hiding (connect) + import Pad.NetProtocol import Pad.NetProtocol.Types import Pad.NetProtocol.Instances import Pad.NetProtocol.Parse -import Pad.Server.Types +import Pad.Server.Types hiding (PadId(..)) +import Pad.DB -- idee: websockets können kein "remote addr" -> wie identifizieren? -- -> im http-teil ne ID generieren und dem client als protocol geben @@ -40,6 +43,9 @@ import Pad.Server.Types main :: IO () main = do + sqlConn <- connect -- TODO: config is hardcoded in Pad.DB + --createInitialTables sqlConn + eventloopChan <- newChan :: IO (Chan PadMessage) let app = WAI.websocketsOr WS.defaultConnectionOptions (wsApp eventloopChan) httpApp forkIO $ eventloop eventloopChan [] diff --git a/src/Pad/DB.hs b/src/Pad/DB.hs new file mode 100644 index 0000000..868f6b8 --- /dev/null +++ b/src/Pad/DB.hs @@ -0,0 +1,128 @@ +{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-} +module Pad.DB where + + +import Database.PostgreSQL.Simple +import Database.PostgreSQL.Simple.FromRow +import Database.PostgreSQL.Simple.ToRow +import Database.PostgreSQL.Simple.FromField +import Database.PostgreSQL.Simple.ToField + +import Data.Time.LocalTime +import qualified Data.ByteString as BS + + +-- for the Eq ZonedTime instance +import Pad.NetProtocol.Instances + +-- TODO: make this configurable .. +dbConnString :: BS.ByteString +dbConnString = "user=pad dbname=pad password=iRnwYqr3ENjki" + +connect :: IO Connection +connect = connectPostgreSQL dbConnString + + + +{- + - evtl. is des eigentlich ne blöde idee des hier zu machen.. sollen + - leute gfälligst getan haben bzw. share/setupdb.sh verwenden + - --} +createInitialTables :: Connection -> IO () +createInitialTables conn = withTransaction conn $ do + -- the type annotation is just so that there's no ambiguity and i + -- don't have to create an instance + conn `execute_` "CREATE TABLE PAD_CREATE_LOCK(\ + \pad_name VARCHAR PRIMARY KEY,\ + \remote_addr VARCHAR,\ + \client_id VARCHAR,\ + \lock_start TIMESTAMPTZ)" + -- TODO: autoremove (timestamp/date, mode) + conn `execute_` "CREATE TABLE PAD(\ + \id SERIAL PRIMARY KEY,\ + \name VARCHAR NOT NULL UNIQUE,\ + \latest_rev INTEGER NOT NULL,\ + \password VARCHAR,\ + \ctime TIMESTAMPTZ NOT NULL,\ + \atime TIMESTAMPTZ NOT NULL)" + conn `execute_` "CREATE INDEX ON PAD (name)" + return () + + +newtype PadId = PadId BS.ByteString deriving (Show, Eq) +newtype ClientAddr = ClientAddr BS.ByteString deriving (Show, Eq) +newtype ClientId = ClientId BS.ByteString deriving (Show, Eq) + +data PadCreationLock = PadCreationLock PadId ClientAddr ClientId ZonedTime deriving (Show, Eq) + + +-- hs -> psql for struct fields +instance FromField PadId where + fromField f bs = PadId <$> fromField f bs + +-- psql -> hs for struct fields +instance ToField PadId where + toField (PadId bs) = toField bs + +instance FromField ClientAddr where + fromField f bs = ClientAddr <$> fromField f bs +instance ToField ClientAddr where + toField (ClientAddr bs) = toField bs + +instance FromField ClientId where + fromField f bs = ClientId <$> fromField f bs +instance ToField ClientId where + toField (ClientId bs) = toField bs + + +-- hs -> psql for structs +instance ToRow PadCreationLock where + toRow (PadCreationLock pi ca ci ts) = [toField pi, toField ca, toField ci, toField ts] + +-- psql -> hs for structs +instance FromRow PadCreationLock where + fromRow = PadCreationLock <$> field <*> field <*> field <*> field + + +{- the semantics of this is as follows: + - 1. client connects to /p/<padid> + - 2. if this is the first time anyone tried that padid, the client is + - shown the pad-creation(password, public, timeout)-dialogue + - 3. when showing that dialogue, the client has a 2-minute "lock" on + - that padid, meaning other clients that try this padid will get a + - information-page telling them to wait for creation + - 4. the lock is cleared in the pad creation transaction + - + - if the pad already exists, returns 'Nothing' + - otherwise returns the acquired lock object + - --} +getCreationLock :: Connection -> PadId -> IO (Maybe PadCreationLock) +getCreationLock conn padId = do + print "getCreationLock" + print padId + + withTransaction conn $ do + [Only (n :: Integer)] <- query conn "SELECT COUNT(*) FROM pad WHERE name=?" [padId] + -- while we're at it.. + execute_ conn "DELETE FROM pad_create_lock WHERE current_timestamp - lock_start > interval '00:02:00'" + case n of + 0 -> do + print "doesn't exist yet, checking creation lock" + (lock_exists :: [PadCreationLock]) <- query conn "SELECT * FROM pad_create_lock WHERE pad_name=?" (Only padId) + + case lock_exists of + [] -> do + print "doesn't exist yet, building creation lock" + [lock] <- query conn "INSERT INTO pad_create_lock (pad_name, lock_start, client_id, remote_addr) VALUES (?, current_timestamp, ?, ?) RETURNING *" (padId, ("cid" :: BS.ByteString), ("rar" :: BS.ByteString)) + return $ Just lock + [lock@(PadCreationLock pi ca ci ts)] -> do + print $ "exists already, held by " ++ (show ci) + return $ Just lock + 1 -> do + print "exists, returning 'Nothing'" + return Nothing + _ -> do + print $ "n too large(" ++ show n ++ "), something went wrong" + return Nothing + + return Nothing -- GitLab