forked from snowdriftcoop/snowdrift
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathApplication.hs
325 lines (256 loc) · 11.6 KB
/
Application.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE TupleSections #-}
module Application
( makeApplication
, getApplicationDev
, makeFoundation
) where
import Import
import Settings
-- import Yesod.Auth
import Yesod.Default.Config
import Yesod.Default.Main
import Yesod.Default.Handlers
import Network.Wai.Middleware.RequestLogger
( mkRequestLogger, outputFormat, OutputFormat (..), IPAddrSource (..), destination
)
import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
import qualified Database.Persist
-- import Database.Persist.Sql (runSqlPool)
import Network.HTTP.Client.Conduit (newManager)
import Control.Monad.Logger (runLoggingT, runStderrLoggingT)
import Control.Concurrent (forkIO, threadDelay)
import System.Log.FastLogger (newStdoutLoggerSet, defaultBufSize, flushLogStr)
import Network.Wai.Logger (clockDateCacher)
import Data.Default (def)
import Yesod.Core.Types (loggerSet, Logger (Logger))
import Control.Monad.Trans.Resource
import System.Directory
import Database.Persist.Postgresql (pgConnStr, withPostgresqlConn)
import qualified Data.List as L
import Data.Text as T
import qualified Data.Text.IO as T
import Version
-- Import all relevant handler modules here.
-- Don't forget to add new modules to your cabal file!
import Handler.Home
import Handler.User
import Handler.Widget
import Handler.Project
import Handler.Invitation
import Handler.Invite
import Handler.UpdateShares
import Handler.Volunteer
import Handler.Contact
import Handler.Who
import Handler.PostLogin
import Handler.ToU
import Handler.Privacy
import Handler.Messages
import Handler.Application
import Handler.Applications
import Handler.JsLicense
import Handler.MarkdownTutorial
import Handler.UserBalance
import Handler.UserPledges
import Handler.Wiki
import Handler.Discussion
import Handler.Tags
import Handler.Tickets
import Handler.RepoFeed
import Handler.BuildFeed
import Widgets.Navbar
import Data.ByteString (ByteString)
import System.Posix.Env.ByteString
import Control.Monad.Reader
import System.Environment (lookupEnv)
runSql :: MonadSqlPersist m => Text -> m ()
runSql = flip rawExecute [] -- TODO quasiquoter?
version :: (Text, Text)
version = $(mkVersion)
-- This line actually creates our YesodDispatch instance. It is the second half
-- of the call to mkYesodData which occurs in Foundation.hs. Please see the
-- comments there for more details.
mkYesodDispatch "App" resourcesApp
-- probably not thread safe
withEnv :: (MonadIO m) => ByteString -> ByteString -> m a -> m a
withEnv k v action = do
original <- liftIO $ getEnv k
liftIO $ setEnv k v True
result <- action
liftIO $ maybe (unsetEnv k) (\ v' -> setEnv k v' True) original
return result
-- This function allocates resources (such as a database connection pool),
-- performs initialization and creates a WAI application. This is also the
-- place to put your migrate statements to have automatic database
-- migrations handled by Yesod.
makeApplication :: AppConfig DefaultEnv Extra -> IO (Application, LogFunc)
makeApplication conf = do
foundation <- makeFoundation conf
-- Initialize the logging middleware
logWare <- mkRequestLogger def
{ outputFormat =
if development
then Detailed True
else Apache FromSocket
, destination = RequestLogger.Logger $ loggerSet $ appLogger foundation
}
-- Create the WAI application and apply middlewares
app <- toWaiAppPlain foundation
let logFunc = messageLoggerSource foundation (appLogger foundation)
return (logWare $ defaultMiddlewaresNoLogging app, logFunc)
-- | Loads up any necessary settings, creates your foundation datatype, and
-- performs some initialization.
makeFoundation :: AppConfig DefaultEnv Extra -> IO App
makeFoundation conf = do
manager <- newManager
s <- staticSite
dbconf <- withYamlEnvironment "config/postgresql.yml" (appEnv conf)
Database.Persist.loadConfig >>=
Database.Persist.applyEnv
p <- Database.Persist.createPoolConfig (dbconf :: Settings.PersistConf)
loggerSet' <- newStdoutLoggerSet defaultBufSize
(getter, updater) <- clockDateCacher
-- If the Yesod logger (as opposed to the request logger middleware) is
-- used less than once a second on average, you may prefer to omit this
-- thread and use "(updater >> getter)" in place of "getter" below. That
-- would update the cache every time it is used, instead of every second.
let updateLoop = do
threadDelay 1000000
updater
flushLogStr loggerSet'
updateLoop
_ <- forkIO updateLoop
let logger = Yesod.Core.Types.Logger loggerSet' getter
foundation = App navbar conf s p manager dbconf logger
-- Perform database migration using our application's logging settings.
case appEnv conf of
Testing -> withEnv "PGDATABASE" "template1" (applyEnv $ persistConfig foundation) >>= \ dbconf' -> do
let runDBNoTransaction (SqlPersistT r) = runReaderT r
options <- maybe [] L.words <$> lookupEnv "SNOWDRIFT_TESTING_OPTIONS"
unless (elem "nodrop" options) $ do
runStderrLoggingT $ runResourceT $ withPostgresqlConn (pgConnStr dbconf') $ runDBNoTransaction $ do
liftIO $ putStrLn "dropping database..."
runSql "DROP DATABASE IF EXISTS snowdrift_test;"
liftIO $ putStrLn "creating database..."
runSql "CREATE DATABASE snowdrift_test WITH TEMPLATE snowdrift_test_template;"
liftIO $ putStrLn "ready."
_ -> return ()
let migration = runResourceT $ do
Database.Persist.runPool dbconf doMigration p
runSqlPool migrateTriggers p
runLoggingT
migration
(messageLoggerSource foundation logger)
now <- getCurrentTime
let (base, diff) = version
runLoggingT
(runResourceT $ Database.Persist.runPool dbconf (insert_ $ Build now base diff) p)
(messageLoggerSource foundation logger)
return foundation
-- for yesod devel
getApplicationDev :: IO (Int, Application)
getApplicationDev =
defaultDevelApp loader (fmap fst . makeApplication)
where
loader = Yesod.Default.Config.loadConfig (configSettings Development)
{ csParseExtra = parseExtra
}
doMigration :: (MonadResource m, MonadBaseControl IO m, MonadIO m, MonadLogger m, MonadThrow m) => SqlPersistT m ()
doMigration = do
$(logInfo) "creating version table"
runSql "CREATE TABLE IF NOT EXISTS \"database_version\" (\"id\" SERIAL PRIMARY KEY UNIQUE, \"last_migration\" INT8 NOT NULL);"
last_migration <- select $ from return
migration_number <- case last_migration of
[] -> insert (DatabaseVersion 0) >> return 0
[Entity _ (DatabaseVersion migration)] -> return migration
_ -> error "multiple entries in DB version table"
unfiltered_migration_files <- liftIO $ getDirectoryContents "migrations"
let migration_files :: [(Int, String)]
migration_files = L.sort
$ L.filter ((> migration_number) . fst)
$ mapMaybe (\ s -> fmap (,s) $ readMaybe =<< L.stripPrefix "migrate" s)
unfiltered_migration_files
forM_ (L.map (("migrations/" <>) . snd) migration_files) $ \ file -> do
$(logWarn) $ "running " <> T.pack file <> "..."
migration <- liftIO $ T.readFile file
runSql migration
let new_last_migration = L.maximum $ migration_number : L.map fst migration_files
update $ flip set [ DatabaseVersionLastMigration =. val new_last_migration ]
migrations <- parseMigration' migrateAll
let (unsafe, safe) = L.partition fst migrations
unless (L.null $ L.map snd safe) $ do
let filename = "migrations/migrate" <> show (new_last_migration + 1)
liftIO $ T.writeFile filename $ T.unlines $ L.map ((`snoc` ';') . snd) safe
$(logWarn) $ "wrote " <> T.pack (show $ L.length safe) <> " safe statements to " <> T.pack filename
mapM_ (runSql . snd) migrations
unless (L.null $ L.map snd unsafe) $ do
let filename = "migrations/migrate.unsafe"
liftIO $ T.writeFile filename $ T.unlines $ L.map ((`snoc` ';') . snd) unsafe
$(logWarn) $ "wrote " <> T.pack (show $ L.length unsafe) <> " safe statements to " <> T.pack filename
error "Some migration steps were unsafe. Aborting."
rolloutStagingWikiPages
rolloutStagingWikiPages :: (MonadBaseControl IO m, MonadIO m, MonadLogger m, MonadResource m, MonadThrow m) => SqlPersistT m ()
rolloutStagingWikiPages = do
pages <- select $ from $ \ page -> do
where_ ( page ^. WikiPageTarget `like` val "_staging_%" )
return page
forM_ pages $ \ (Entity staged_page_id staged_page) -> do
let (Just target) = stripPrefix "_staging_" $ wikiPageTarget staged_page
[ Value page_id ] <- select $ from $ \ page -> do
where_ ( page ^. WikiPageTarget ==. val target )
return $ page ^. WikiPageId
update $ \ edit -> do
set edit [ WikiEditPage =. val page_id ]
where_ ( edit ^. WikiEditPage ==. val staged_page_id )
update $ \ page -> do
set page [ WikiPageContent =. val (wikiPageContent staged_page) ]
where_ ( page ^. WikiPageId ==. val page_id )
[ Value last_staged_edit_edit ] <- select $ from $ \ last_staged_edit -> do
where_ ( last_staged_edit ^. WikiLastEditPage ==. val staged_page_id )
return $ last_staged_edit ^. WikiLastEditEdit
update $ \ last_edit -> do
set last_edit [ WikiLastEditEdit =. val last_staged_edit_edit ]
where_ ( last_edit ^. WikiLastEditPage ==. val page_id )
delete $ from $ \ last_edit -> where_ ( last_edit ^. WikiLastEditPage ==. val staged_page_id )
delete $ from $ \ page -> where_ ( page ^. WikiPageId ==. val staged_page_id )
migrateTriggers :: (MonadSqlPersist m, MonadBaseControl IO m, MonadThrow m) => m ()
migrateTriggers = runResourceT $ do
runSql $ T.unlines
[ "CREATE OR REPLACE FUNCTION log_role_event_trigger() RETURNS trigger AS $role_event$"
, " BEGIN"
, " IF (TG_OP = 'DELETE') THEN"
, " INSERT INTO role_event (time, \"user\", role, project, added) SELECT now(), OLD.\"user\", OLD.role, OLD.project, 'f';"
, " RETURN OLD;"
, " ELSIF (TG_OP = 'INSERT') THEN"
, " INSERT INTO role_event (time, \"user\", role, project, added) SELECT now(), NEW.\"user\", NEW.role, NEW.project, 't';"
, " RETURN NEW;"
, " END IF;"
, " RETURN NULL;"
, " END;"
, "$role_event$ LANGUAGE plpgsql;"
]
runSql "DROP TRIGGER IF EXISTS role_event ON project_user_role;"
runSql $ T.unlines
[ "CREATE TRIGGER role_event"
, "AFTER INSERT OR DELETE ON project_user_role"
, " FOR EACH ROW EXECUTE PROCEDURE log_role_event_trigger();"
]
runSql $ T.unlines
[ "CREATE OR REPLACE FUNCTION log_doc_event_trigger() RETURNS trigger AS $doc_event$"
, " BEGIN"
, " IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN"
, " INSERT INTO doc_event (time, doc, blessed_version) SELECT now(), NEW.id, NEW.current_version;"
, " RETURN NEW;"
, " END IF;"
, " RETURN NULL;"
, " END;"
, "$doc_event$ LANGUAGE plpgsql;"
]
runSql "DROP TRIGGER IF EXISTS doc_event ON doc;"
runSql $ T.unlines
[ "CREATE TRIGGER doc_event"
, "AFTER INSERT OR DELETE ON doc"
, " FOR EACH ROW EXECUTE PROCEDURE log_doc_event_trigger();"
]
return ()